import * as React from 'react';

import { LatencyTracker } from 'mweb/common/latency/latencyTracker';
import { LatencyReporter } from 'mweb/common/latency/latencyReporter';
import {
  contextTypeValidator,
  ContextTypes,
} from 'mweb/common/utils/contextTypeValidator';

export interface LatencyTrackedPropsComponent {
  isInteractive: {
    (): boolean;
  };
}

interface LatencyTrackedEventComponent {
  latencyEventName: string;
  latencyEventTarget: any;
}

type LatencyTrackedComponent =
  | LatencyTrackedPropsComponent
  | LatencyTrackedEventComponent;

type BroadLatencyTrackedComponent = LatencyTrackedPropsComponent &
  LatencyTrackedEventComponent;

interface LatencyTrackedComponentClass<P>
  extends React.ExtendableComponentClass<P, LatencyTrackedComponent> {
  displayName: string;
}

interface BroadLatencyTrackedComponentClass<P>
  extends React.ExtendableComponentClass<P, BroadLatencyTrackedComponent> {
  displayName: string;
}

export interface LatencyTrackerContext {
  // parent latencyTracker will be undefined for root tracker
  latencyTracker: LatencyTracker | undefined;
  latencyReporter: LatencyReporter;
  latencyPageCounter: number;
}

export interface LatencyTrackerChildContext
  extends Pick<LatencyTrackerContext, 'latencyTracker'> {}

export function withLatencyTracker<P>(
  ComponentClass: LatencyTrackedComponentClass<P>,
): React.ComponentClass<P> {
  if (!process.env.BROWSER) {
    return ComponentClass;
  }

  /**
   * All of the typing stuff going on here is allow TS to properly discriminate
   * between the different shapes that are acceptable for latency tracking
   * (isInteractive vs latencyEvent + latencyEventTarget etc). TS doesn't
   * like working with unions like that (as it can't discriminate), so we
   * use the union in the parameters to enforce proper either-or shaping, but
   * then we recast to an intersection here so that we can work with the class
   * and reference its properties with appropriate JS-style safety checks. This
   * results in a much better external interface at the cost of some minor
   * additional internal complexity.
   */
  let BroadClass = ComponentClass as BroadLatencyTrackedComponentClass<P>;

  return class TrackedComponent extends BroadClass {
    static childContextTypes: ContextTypes<LatencyTrackerChildContext> = {
      latencyTracker: contextTypeValidator,
    };

    static contextTypes: ContextTypes<LatencyTrackerContext> = {
      latencyTracker: contextTypeValidator,
      latencyPageCounter: contextTypeValidator,
      latencyReporter: contextTypeValidator,
    };

    context: LatencyTrackerContext;
    latencyTracker: LatencyTracker;
    latencyName: string;

    constructor(props: P, context: LatencyTrackerContext) {
      super(props, context);

      this.latencyTracker = new LatencyTracker(
        this.latencyName || ComponentClass.displayName,
        context.latencyTracker,
        context.latencyReporter,
      );
    }

    // event.target may not be the same as latencyEventTarget for image events
    // this is because of the way <picture>, <img>, and <source> work together
    latencyEventListener: EventListener = e => {
      if (e && e.target && (e.target as HTMLElement).tagName === 'IMG') {
        this.latencyReportInteractiveImage(
          (e.target as HTMLImageElement).currentSrc,
        );
      } else {
        this.latencyReportInteractive();
      }

      if (e && e.target) {
        e.target.removeEventListener(
          this.latencyEventName,
          this.latencyEventListener,
        );
      } else if (this.latencyEventTarget) {
        this.latencyEventTarget.removeEventListener(
          this.latencyEventName,
          this.latencyEventListener,
        );
      }
    };

    latencyWatchForEvent(): void {
      if (
        this.latencyEventTarget.tagName === 'IMG' &&
        this.latencyEventTarget.complete
      ) {
        this.latencyReportInteractiveImage(
          this.latencyEventTarget.currentSrc,
          true,
        );
      } else {
        this.latencyEventTarget.addEventListener(
          this.latencyEventName,
          this.latencyEventListener,
        );
      }
    }

    componentDidMount(): void {
      if (super.componentDidMount) {
        super.componentDidMount();
      }

      if (!this.isInteractive) {
        if (this.latencyEventTarget) {
          this.latencyWatchForEvent();
        }
      } else if (this.isInteractive()) {
        this.latencyReportInteractive(true);
      }
    }

    componentWillUpdate(nextProps: P, nextState: any, nextContext: any): void {
      if (super.componentWillUpdate) {
        super.componentWillUpdate(nextProps, nextState, nextContext);
      }

      if (this.context.latencyPageCounter !== nextContext.latencyPageCounter) {
        this.latencyTracker.resetForNextPage();
      }
    }

    componentDidUpdate(prevProps: P, prevState: any, prevContext: any): void {
      if (super.componentDidUpdate) {
        super.componentDidUpdate(prevProps, prevState, prevContext);
      }

      if (
        !this.latencyTracker.isInteractive &&
        this.isInteractive &&
        this.isInteractive()
      ) {
        this.latencyReportInteractive();
      }
    }

    componentWillUnmount(): void {
      if (super.componentWillUnmount) {
        super.componentWillUnmount();
      }

      this.latencyTracker.destroy();
    }

    latencyReportInteractive(onMount: boolean = false): void {
      this.latencyTracker.reportInteractive(onMount);
    }

    latencyReportInteractiveImage(
      imgSrc: string,
      onMount: boolean = false,
    ): void {
      this.latencyTracker.reportInteractiveImage(imgSrc, onMount);
    }

    getChildContext = (): LatencyTrackerChildContext => {
      return {
        latencyTracker: this.latencyTracker,
      };
    };

    render():
      | string
      | number
      | Boolean
      | React.ReactNode
      | React.ReactPortal
      | JSX.Element
      | JSX.Element[] {
      return super.render();
    }
  };
}
