/**
 * Copyright (c) Facebook, Inc. and its 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 {
  Flamechart,
  FlamechartStackFrame,
  FlamechartStackLayer,
  InternalModuleSourceToRanges,
} from '../types';
import type {
  Interaction,
  MouseMoveInteraction,
  Rect,
  Size,
  ViewRefs,
} from '../view-base';

import {
  BackgroundColorView,
  Surface,
  View,
  layeredLayout,
  rectContainsPoint,
  intersectionOfRects,
  rectIntersectsRect,
  verticallyStackedLayout,
} from '../view-base';
import {isInternalModule} from './utils/moduleFilters';
import {
  durationToWidth,
  positioningScaleFactor,
  timestampToPosition,
} from './utils/positioning';
import {drawText} from './utils/text';
import {
  COLORS,
  FLAMECHART_FRAME_HEIGHT,
  COLOR_HOVER_DIM_DELTA,
  BORDER_SIZE,
} from './constants';
import {ColorGenerator, dimmedColor, hslaColorToString} from './utils/colors';

// Source: https://source.chromium.org/chromium/chromium/src/+/master:out/Debug/gen/devtools/timeline/TimelineUIUtils.js;l=2109;drc=fb32e928d79707a693351b806b8710b2f6b7d399
const colorGenerator = new ColorGenerator(
  {min: 30, max: 330},
  {min: 50, max: 80, count: 3},
  85,
);
colorGenerator.setColorForID('', {h: 43.6, s: 45.8, l: 90.6, a: 100});

function defaultHslaColorForStackFrame({scriptUrl}: FlamechartStackFrame) {
  return colorGenerator.colorForID(scriptUrl ?? '');
}

function defaultColorForStackFrame(stackFrame: FlamechartStackFrame): string {
  const color = defaultHslaColorForStackFrame(stackFrame);
  return hslaColorToString(color);
}

function hoverColorForStackFrame(stackFrame: FlamechartStackFrame): string {
  const color = dimmedColor(
    defaultHslaColorForStackFrame(stackFrame),
    COLOR_HOVER_DIM_DELTA,
  );
  return hslaColorToString(color);
}

class FlamechartStackLayerView extends View {
  /** Layer to display */
  _stackLayer: FlamechartStackLayer;

  /** A set of `stackLayer`'s frames, for efficient lookup. */
  _stackFrameSet: Set<FlamechartStackFrame>;

  _internalModuleSourceToRanges: InternalModuleSourceToRanges;

  _intrinsicSize: Size;

  _hoveredStackFrame: FlamechartStackFrame | null = null;
  _onHover: ((node: FlamechartStackFrame | null) => void) | null = null;

  constructor(
    surface: Surface,
    frame: Rect,
    stackLayer: FlamechartStackLayer,
    internalModuleSourceToRanges: InternalModuleSourceToRanges,
    duration: number,
  ) {
    super(surface, frame);
    this._stackLayer = stackLayer;
    this._stackFrameSet = new Set(stackLayer);
    this._internalModuleSourceToRanges = internalModuleSourceToRanges;
    this._intrinsicSize = {
      width: duration,
      height: FLAMECHART_FRAME_HEIGHT,
    };
  }

  desiredSize() {
    return this._intrinsicSize;
  }

  setHoveredFlamechartStackFrame(
    hoveredStackFrame: FlamechartStackFrame | null,
  ) {
    if (this._hoveredStackFrame === hoveredStackFrame) {
      return; // We're already hovering over this frame
    }

    // Only care about frames displayed by this view.
    const stackFrameToSet =
      hoveredStackFrame && this._stackFrameSet.has(hoveredStackFrame)
        ? hoveredStackFrame
        : null;
    if (this._hoveredStackFrame === stackFrameToSet) {
      return; // Resulting state is unchanged
    }
    this._hoveredStackFrame = stackFrameToSet;
    this.setNeedsDisplay();
  }

  draw(context: CanvasRenderingContext2D) {
    const {
      frame,
      _stackLayer,
      _hoveredStackFrame,
      _intrinsicSize,
      visibleArea,
    } = this;

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

    const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);

    for (let i = 0; i < _stackLayer.length; i++) {
      const stackFrame = _stackLayer[i];
      const {name, timestamp, duration} = stackFrame;

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

      const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
      const nodeRect: Rect = {
        origin: {x, y: frame.origin.y},
        size: {
          width: Math.floor(width - BORDER_SIZE),
          height: Math.floor(FLAMECHART_FRAME_HEIGHT - BORDER_SIZE),
        },
      };
      if (!rectIntersectsRect(nodeRect, visibleArea)) {
        continue; // Not in view
      }

      const showHoverHighlight = _hoveredStackFrame === _stackLayer[i];

      let textFillStyle;
      if (isInternalModule(this._internalModuleSourceToRanges, stackFrame)) {
        context.fillStyle = showHoverHighlight
          ? COLORS.INTERNAL_MODULE_FRAME_HOVER
          : COLORS.INTERNAL_MODULE_FRAME;
        textFillStyle = COLORS.INTERNAL_MODULE_FRAME_TEXT;
      } else {
        context.fillStyle = showHoverHighlight
          ? hoverColorForStackFrame(stackFrame)
          : defaultColorForStackFrame(stackFrame);
        textFillStyle = COLORS.TEXT_COLOR;
      }

      const drawableRect = intersectionOfRects(nodeRect, visibleArea);
      context.fillRect(
        drawableRect.origin.x,
        drawableRect.origin.y,
        drawableRect.size.width,
        drawableRect.size.height,
      );

      drawText(name, context, nodeRect, drawableRect, {
        fillStyle: textFillStyle,
      });
    }

    // Render bottom border.
    const borderFrame: Rect = {
      origin: {
        x: frame.origin.x,
        y: frame.origin.y + FLAMECHART_FRAME_HEIGHT - BORDER_SIZE,
      },
      size: {
        width: frame.size.width,
        height: BORDER_SIZE,
      },
    };
    if (rectIntersectsRect(borderFrame, visibleArea)) {
      const borderDrawableRect = intersectionOfRects(borderFrame, visibleArea);
      context.fillStyle = COLORS.PRIORITY_BORDER;
      context.fillRect(
        borderDrawableRect.origin.x,
        borderDrawableRect.origin.y,
        borderDrawableRect.size.width,
        borderDrawableRect.size.height,
      );
    }
  }

  /**
   * @private
   */
  _handleMouseMove(interaction: MouseMoveInteraction, viewRefs: ViewRefs) {
    const {_stackLayer, frame, _intrinsicSize, _onHover, visibleArea} = this;
    const {location} = interaction.payload;
    if (!_onHover || !rectContainsPoint(location, visibleArea)) {
      return;
    }

    // Find the node being hovered over.
    const scaleFactor = positioningScaleFactor(_intrinsicSize.width, frame);
    let startIndex = 0;
    let stopIndex = _stackLayer.length - 1;
    while (startIndex <= stopIndex) {
      const currentIndex = Math.floor((startIndex + stopIndex) / 2);
      const flamechartStackFrame = _stackLayer[currentIndex];
      const {timestamp, duration} = flamechartStackFrame;

      const x = Math.floor(timestampToPosition(timestamp, scaleFactor, frame));
      const width = durationToWidth(duration, scaleFactor);

      // Don't show tooltips for nodes that are too small to render at this zoom level.
      if (Math.floor(width - BORDER_SIZE) >= 1) {
        if (x <= location.x && x + width >= location.x) {
          this.currentCursor = 'context-menu';
          viewRefs.hoveredView = this;
          _onHover(flamechartStackFrame);
          return;
        }
      }

      if (x > location.x) {
        stopIndex = currentIndex - 1;
      } else {
        startIndex = currentIndex + 1;
      }
    }

    _onHover(null);
  }

  _didGrab: boolean = false;

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

export class FlamechartView extends View {
  _flamechartRowViews: FlamechartStackLayerView[] = [];

  /** Container view that vertically stacks flamechart rows */
  _verticalStackView: View;

  _hoveredStackFrame: FlamechartStackFrame | null = null;
  _onHover: ((node: FlamechartStackFrame | null) => void) | null = null;

  constructor(
    surface: Surface,
    frame: Rect,
    flamechart: Flamechart,
    internalModuleSourceToRanges: InternalModuleSourceToRanges,
    duration: number,
  ) {
    super(surface, frame, layeredLayout);
    this.setDataAndUpdateSubviews(
      flamechart,
      internalModuleSourceToRanges,
      duration,
    );
  }

  setDataAndUpdateSubviews(
    flamechart: Flamechart,
    internalModuleSourceToRanges: InternalModuleSourceToRanges,
    duration: number,
  ) {
    const {surface, frame, _onHover, _hoveredStackFrame} = this;

    // Clear existing rows on data update
    if (this._verticalStackView) {
      this.removeAllSubviews();
      this._flamechartRowViews = [];
    }

    this._verticalStackView = new View(surface, frame, verticallyStackedLayout);
    this._flamechartRowViews = flamechart.map(stackLayer => {
      const rowView = new FlamechartStackLayerView(
        surface,
        frame,
        stackLayer,
        internalModuleSourceToRanges,
        duration,
      );
      this._verticalStackView.addSubview(rowView);

      // Update states
      rowView._onHover = _onHover;
      rowView.setHoveredFlamechartStackFrame(_hoveredStackFrame);
      return rowView;
    });

    // Add a plain background view to prevent gaps from appearing between flamechartRowViews.
    this.addSubview(new BackgroundColorView(surface, frame));
    this.addSubview(this._verticalStackView);
  }

  setHoveredFlamechartStackFrame(
    hoveredStackFrame: FlamechartStackFrame | null,
  ) {
    this._hoveredStackFrame = hoveredStackFrame;
    this._flamechartRowViews.forEach(rowView =>
      rowView.setHoveredFlamechartStackFrame(hoveredStackFrame),
    );
  }

  setOnHover(onHover: (node: FlamechartStackFrame | null) => void) {
    this._onHover = onHover;
    this._flamechartRowViews.forEach(rowView => (rowView._onHover = onHover));
  }

  desiredSize() {
    // Ignore the wishes of the background color view
    const intrinsicSize = this._verticalStackView.desiredSize();
    return {
      ...intrinsicSize,
      // Collapsed by default
      maxInitialHeight: 0,
    };
  }

  /**
   * @private
   */
  _handleMouseMove(interaction: MouseMoveInteraction) {
    const {_onHover, visibleArea} = this;
    if (!_onHover) {
      return;
    }

    const {location} = interaction.payload;
    if (!rectContainsPoint(location, visibleArea)) {
      // Clear out any hovered flamechart stack frame
      _onHover(null);
    }
  }

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