import { logger } from 'tachyon-logger';
import maxBy from 'lodash-es/maxBy';

import { LatencyReporter } from 'mweb/common/latency/latencyReporter';

interface MaxInteractiveTimeData {
  time: number;
  // images don't have an end mark but instead use performance timing directly
  mark: string | undefined;
}

export enum LatencyState {
  NotInteractive = 'not',
  SelfInteractive = 'self',
  SelfAndSubtreeInteractive = 'selfAndSub',
}

export class LatencyTracker {
  static pageCounter: number = 0;
  static componentCounter: number = 0;

  latencyID: string;
  status: LatencyState = LatencyState.NotInteractive;
  children: LatencyTracker[] = [];
  startMark: string;
  // images don't have an end mark but instead use performance timing directly
  endMark: string | undefined;
  interactiveTime: number | undefined;
  trackingID: number; // this is to satisfy the spec but isn't used for anything

  get componentName(): string {
    return `${this.latencyID}_${this.componentID}`;
  }

  get measure(): string {
    return `${this.componentName}_measure`;
  }

  get nextChildID(): string {
    return `${this.latencyID}.${this.children.length}`;
  }

  get isInteractive(): boolean {
    return this.status !== LatencyState.NotInteractive;
  }

  get areAllChildrenInteractive(): boolean {
    return this.children.reduce(
      (acc, child) =>
        acc && child.status === LatencyState.SelfAndSubtreeInteractive,
      true,
    );
  }

  get maxInteractiveTime(): MaxInteractiveTimeData | undefined {
    const times = this.children.map(child => child.maxInteractiveTime);
    if (this.interactiveTime) {
      times.push({
        time: this.interactiveTime,
        mark: this.endMark,
      });
    }

    return maxBy(times, 'time');
  }

  constructor(
    public componentID: string,
    private parent: LatencyTracker | undefined,
    private reporter: LatencyReporter,
  ) {
    this.trackingID = LatencyTracker.getNextComponentNumber();
    if (parent) {
      this.latencyID = parent.registerChild(this);
      this.setStartMark();
      reporter.signalComponentInit(
        this.getClientTimeFromMark(this.startMark),
        this.componentName,
        this.trackingID,
        parent.componentName,
        parent.trackingID,
      );
    } else {
      this.latencyID = `Page${LatencyTracker.pageCounter}_0`;
      this.setStartMark();
    }
  }

  static getNextComponentNumber(): number {
    return LatencyTracker.componentCounter++;
  }

  setStartMark(): void {
    if (LatencyTracker.pageCounter === 0) {
      this.startMark = 'domLoading';
    } else {
      this.startMark = `${this.componentName}_init`;
      window.performance.mark(this.startMark);
    }
  }

  reportInteractive(onMount: boolean = false): void {
    if (this.status === LatencyState.NotInteractive) {
      this.status = LatencyState.SelfInteractive;

      if (onMount && LatencyTracker.pageCounter === 0) {
        this.endMark = 'domInteractive';
      } else {
        this.endMark = `${this.componentName}_interactive`;
        window.performance.mark(this.endMark);
      }
      this.interactiveTime = this.getClientTimeFromMark(this.endMark);

      if (this.parent) {
        window.performance.measure(this.measure, this.startMark, this.endMark);
        const measure = window.performance.getEntriesByName(
          this.measure,
          'measure',
        )[0];
        this.reporter.signalComponentInteractive(
          this.interactiveTime,
          measure.duration,
          this.componentName,
          this.trackingID,
          this.parent.componentName,
          this.parent.trackingID,
        );
      }

      this.reportUpstream();
    } else {
      logger.warn(`${this.componentName} tried to report interactive twice`);
    }
  }

  reportInteractiveImage(imgSrc: string, onMount: boolean = false): void {
    const measure = window.performance.getEntriesByName(imgSrc, 'resource')[0];

    if (measure && this.status === LatencyState.NotInteractive && this.parent) {
      this.status = LatencyState.SelfInteractive;

      // images that have already been loaded are instantly available
      // for in-app navigation, we have to rebase this mark on pageTimeOrigin
      this.interactiveTime = Math.max(
        measure.duration +
          measure.startTime -
          this.reporter.getPageTimeOriginOffset(),
        0,
      );

      this.reporter.signalComponentInteractive(
        this.interactiveTime,
        measure.duration,
        this.componentName,
        this.trackingID,
        this.parent.componentName,
        this.parent.trackingID,
      );

      this.reportUpstream();
    } else {
      this.reportInteractive(onMount);
    }
  }

  registerChild(child: LatencyTracker): string {
    const nextID = this.nextChildID;
    this.children.push(child);
    return nextID;
  }

  unregisterChild(child: LatencyTracker): void {
    this.children = this.children.filter(c => c !== child);
  }

  reportInteractiveChild(): void {
    this.reportUpstream();
  }

  reportUpstream(): void {
    if (
      this.status === LatencyState.SelfInteractive &&
      this.areAllChildrenInteractive
    ) {
      this.status = LatencyState.SelfAndSubtreeInteractive;

      if (this.parent) {
        this.parent.reportInteractiveChild();
      } else {
        const interactive = this.maxInteractiveTime!; // must exist if areAllChildrenInteractive and component is interactive
        window.performance.measure(
          `Page${LatencyTracker.pageCounter}`,
          this.startMark,
          interactive.mark || this.endMark, // images don't have an endmark
        );
        this.reporter.signalPageInteractive(interactive.time);
      }
    }
  }

  destroy(): void {
    if (this.parent) {
      this.parent.unregisterChild(this);
    }

    this.children = [];
  }

  // this is not a recursive tree traversal because we don't want to reset components that are unmounting
  resetForNextPage(): void {
    this.status = LatencyState.NotInteractive;
    this.children = [];
    this.endMark = undefined;
    this.interactiveTime = undefined;

    if (this.parent) {
      this.latencyID = this.parent.registerChild(this);
      this.setStartMark();
    } else {
      LatencyTracker.pageCounter += 1;
      this.latencyID = `Page${LatencyTracker.pageCounter}_0`;
      this.setStartMark();
      this.reporter.signalRenderingRoute(LatencyTracker.pageCounter);
    }
  }

  getClientTimeFromMark(markName: string): number {
    let measureName: string;
    switch (markName) {
      case 'domLoading':
        measureName = `time_to_${this.componentName}_init`;
        break;
      case 'domInteractive':
        measureName = `time_to_${this.componentName}_interactive`;
        break;
      default:
        measureName = `time_to_${markName}`;
    }
    window.performance.measure(
      measureName,
      this.reporter.pageTimeOriginMark,
      markName,
    );
    const measure = window.performance.getEntriesByName(
      measureName,
      'measure',
    )[0];
    return measure.duration;
  }
}
