/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {ReactComponentMeasure, TimelineData, ViewState} from '../types';
import type {
  Interaction,
  IntrinsicSize,
  MouseMoveInteraction,
  Rect,
  ViewRefs,
} from '../view-base';

import {
  durationToWidth,
  positioningScaleFactor,
  positionToTimestamp,
  timestampToPosition,
} from './utils/positioning';
import {drawText} from './utils/text';
import {formatDuration} from '../utils/formatting';
import {
  View,
  Surface,
  rectContainsPoint,
  rectIntersectsRect,
  intersectionOfRects,
} from '../view-base';
import {BORDER_SIZE, COLORS, NATIVE_EVENT_HEIGHT} from './constants';

const ROW_WITH_BORDER_HEIGHT = NATIVE_EVENT_HEIGHT + BORDER_SIZE;

export class ComponentMeasuresView extends View {
  _cachedSearchMatches: Map<string, boolean>;
  _cachedSearchRegExp: RegExp | null = null;
  _hoveredComponentMeasure: ReactComponentMeasure | null = null;
  _intrinsicSize: IntrinsicSize;
  _profilerData: TimelineData;
  _viewState: ViewState;

  onHover: ((event: ReactComponentMeasure | null) => void) | null = null;

  constructor(
    surface: Surface,
    frame: Rect,
    profilerData: TimelineData,
    viewState: ViewState,
  ) {
    super(surface, frame);

    this._profilerData = profilerData;
    this._viewState = viewState;

    this._cachedSearchMatches = new Map();
    this._cachedSearchRegExp = null;

    viewState.onSearchRegExpStateChange(() => {
      this.setNeedsDisplay();
    });

    this._intrinsicSize = {
      width: profilerData.duration,
      height: ROW_WITH_BORDER_HEIGHT,
    };
  }

  desiredSize(): IntrinsicSize {
    return this._intrinsicSize;
  }

  setHoveredEvent(hoveredEvent: ReactComponentMeasure | null) {
    if (this._hoveredComponentMeasure === hoveredEvent) {
      return;
    }
    this._hoveredComponentMeasure = hoveredEvent;
    this.setNeedsDisplay();
  }

  /**
   * Draw a single `ReactComponentMeasure` as a box/span with text inside of it.
   */
  _drawSingleReactComponentMeasure(
    context: CanvasRenderingContext2D,
    rect: Rect,
    componentMeasure: ReactComponentMeasure,
    scaleFactor: number,
    showHoverHighlight: boolean,
  ): boolean {
    const {frame} = this;
    const {componentName, duration, timestamp, type, warning} =
      componentMeasure;

    const xStart = timestampToPosition(timestamp, scaleFactor, frame);
    const xStop = timestampToPosition(timestamp + duration, scaleFactor, frame);
    const componentMeasureRect: Rect = {
      origin: {
        x: xStart,
        y: frame.origin.y,
      },
      size: {width: xStop - xStart, height: NATIVE_EVENT_HEIGHT},
    };
    if (!rectIntersectsRect(componentMeasureRect, rect)) {
      return false; // Not in view
    }

    const width = durationToWidth(duration, scaleFactor);
    if (width < 1) {
      return false; // Too small to render at this zoom level
    }

    let textFillStyle = ((null: any): string);
    let typeLabel = ((null: any): string);

    const drawableRect = intersectionOfRects(componentMeasureRect, rect);
    context.beginPath();
    if (warning !== null) {
      context.fillStyle = showHoverHighlight
        ? COLORS.WARNING_BACKGROUND_HOVER
        : COLORS.WARNING_BACKGROUND;
    } else {
      switch (type) {
        case 'render':
          context.fillStyle = showHoverHighlight
            ? COLORS.REACT_RENDER_HOVER
            : COLORS.REACT_RENDER;
          textFillStyle = COLORS.REACT_RENDER_TEXT;
          typeLabel = 'rendered';
          break;
        case 'layout-effect-mount':
          context.fillStyle = showHoverHighlight
            ? COLORS.REACT_LAYOUT_EFFECTS_HOVER
            : COLORS.REACT_LAYOUT_EFFECTS;
          textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
          typeLabel = 'mounted layout effect';
          break;
        case 'layout-effect-unmount':
          context.fillStyle = showHoverHighlight
            ? COLORS.REACT_LAYOUT_EFFECTS_HOVER
            : COLORS.REACT_LAYOUT_EFFECTS;
          textFillStyle = COLORS.REACT_LAYOUT_EFFECTS_TEXT;
          typeLabel = 'unmounted layout effect';
          break;
        case 'passive-effect-mount':
          context.fillStyle = showHoverHighlight
            ? COLORS.REACT_PASSIVE_EFFECTS_HOVER
            : COLORS.REACT_PASSIVE_EFFECTS;
          textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
          typeLabel = 'mounted passive effect';
          break;
        case 'passive-effect-unmount':
          context.fillStyle = showHoverHighlight
            ? COLORS.REACT_PASSIVE_EFFECTS_HOVER
            : COLORS.REACT_PASSIVE_EFFECTS;
          textFillStyle = COLORS.REACT_PASSIVE_EFFECTS_TEXT;
          typeLabel = 'unmounted passive effect';
          break;
      }
    }

    let isMatch = false;
    const cachedSearchRegExp = this._cachedSearchRegExp;
    if (cachedSearchRegExp !== null) {
      const cachedSearchMatches = this._cachedSearchMatches;
      const cachedValue = cachedSearchMatches.get(componentName);
      if (cachedValue != null) {
        isMatch = cachedValue;
      } else {
        isMatch = componentName.match(cachedSearchRegExp) !== null;
        cachedSearchMatches.set(componentName, isMatch);
      }
    }

    if (isMatch) {
      context.fillStyle = COLORS.SEARCH_RESULT_FILL;
    }

    context.fillRect(
      drawableRect.origin.x,
      drawableRect.origin.y,
      drawableRect.size.width,
      drawableRect.size.height,
    );

    const label = `${componentName} ${typeLabel} - ${formatDuration(duration)}`;

    drawText(label, context, componentMeasureRect, drawableRect, {
      fillStyle: textFillStyle,
    });

    return true;
  }

  draw(context: CanvasRenderingContext2D) {
    const {
      frame,
      _profilerData: {componentMeasures},
      _hoveredComponentMeasure,
      visibleArea,
    } = this;

    const searchRegExp = this._viewState.searchRegExp;
    if (this._cachedSearchRegExp !== searchRegExp) {
      this._cachedSearchMatches = new Map();
      this._cachedSearchRegExp = searchRegExp;
    }

    context.fillStyle = COLORS.BACKGROUND;
    context.fillRect(
      visibleArea.origin.x,
      visibleArea.origin.y,
      visibleArea.size.width,
      visibleArea.size.height,
    );

    // Draw events
    const scaleFactor = positioningScaleFactor(
      this._intrinsicSize.width,
      frame,
    );

    let didDrawMeasure = false;
    componentMeasures.forEach(componentMeasure => {
      didDrawMeasure =
        this._drawSingleReactComponentMeasure(
          context,
          visibleArea,
          componentMeasure,
          scaleFactor,
          componentMeasure === _hoveredComponentMeasure,
        ) || didDrawMeasure;
    });

    if (!didDrawMeasure) {
      drawText(
        '(zoom or pan to see React components)',
        context,
        visibleArea,
        visibleArea,
        {fillStyle: COLORS.TEXT_DIM_COLOR, textAlign: 'center'},
      );
    }

    context.fillStyle = COLORS.PRIORITY_BORDER;
    context.fillRect(
      visibleArea.origin.x,
      visibleArea.origin.y + ROW_WITH_BORDER_HEIGHT - BORDER_SIZE,
      visibleArea.size.width,
      BORDER_SIZE,
    );
  }

  /**
   * @private
   */
  _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    const {frame, _intrinsicSize, onHover, visibleArea} = this;
    if (!onHover) {
      return;
    }

    const {location} = interaction.payload;
    if (!rectContainsPoint(location, visibleArea)) {
      onHover(null);
      return;
    }

    const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    const hoverTimestamp = positionToTimestamp(location.x, scaleFactor, frame);

    const componentMeasures = this._profilerData.componentMeasures;
    for (let index = componentMeasures.length - 1; index >= 0; index--) {
      const componentMeasure = componentMeasures[index];
      const {duration, timestamp} = componentMeasure;

      if (
        hoverTimestamp >= timestamp &&
        hoverTimestamp <= timestamp + duration
      ) {
        this.currentCursor = 'context-menu';
        viewRefs.hoveredView = this;
        onHover(componentMeasure);
        return;
      }
    }

    onHover(null);
  }

  handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    switch (interaction.type) {
      case 'mousemove':
        this._handleMouseMove(interaction, viewRefs);
        break;
    }
  }
}