/**
 * 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 {Snapshot, TimelineData} from '../types';
import type {
  Interaction,
  Point,
  Rect,
  Size,
  Surface,
  ViewRefs,
} from '../view-base';

import {positioningScaleFactor, timestampToPosition} from './utils/positioning';
import {
  intersectionOfRects,
  rectContainsPoint,
  rectEqualToRect,
  View,
} from '../view-base';
import {BORDER_SIZE, COLORS, SNAPSHOT_SCRUBBER_SIZE} from './constants';

type OnHover = (node: Snapshot | null) => void;

export class SnapshotsView extends View {
  _hoverLocation: Point | null = null;
  _intrinsicSize: Size;
  _profilerData: TimelineData;

  onHover: OnHover | null = null;

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

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

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

  draw(context: CanvasRenderingContext2D) {
    const snapshotHeight = this._profilerData.snapshotHeight;
    const {visibleArea} = this;

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

    const y = visibleArea.origin.y;

    let x = visibleArea.origin.x;

    // Rather than drawing each snapshot where it occurred,
    // draw them at fixed intervals and just show the nearest one.
    while (x < visibleArea.origin.x + visibleArea.size.width) {
      const snapshot = this._findClosestSnapshot(x);
      if (snapshot === null) {
        // This shold never happen.
        break;
      }

      const scaledHeight = snapshotHeight;
      const scaledWidth = (snapshot.width * snapshotHeight) / snapshot.height;

      const imageRect: Rect = {
        origin: {
          x,
          y,
        },
        size: {width: scaledWidth, height: scaledHeight},
      };

      // Lazily create and cache Image objects as we render a snapsho for the first time.
      if (snapshot.image === null) {
        const img = (snapshot.image = new Image());
        img.onload = () => {
          this._drawSnapshotImage(context, snapshot, imageRect);
        };
        img.src = snapshot.imageSource;
      } else {
        this._drawSnapshotImage(context, snapshot, imageRect);
      }

      x += scaledWidth + BORDER_SIZE;
    }

    const hoverLocation = this._hoverLocation;
    if (hoverLocation !== null) {
      const scrubberWidth = SNAPSHOT_SCRUBBER_SIZE + BORDER_SIZE * 2;
      const scrubberOffset = scrubberWidth / 2;

      context.fillStyle = COLORS.SCRUBBER_BORDER;
      context.fillRect(
        hoverLocation.x - scrubberOffset,
        visibleArea.origin.y,
        scrubberWidth,
        visibleArea.size.height,
      );

      context.fillStyle = COLORS.SCRUBBER_BACKGROUND;
      context.fillRect(
        hoverLocation.x - scrubberOffset + BORDER_SIZE,
        visibleArea.origin.y,
        SNAPSHOT_SCRUBBER_SIZE,
        visibleArea.size.height,
      );
    }
  }

  handleInteraction(interaction: Interaction, viewRefs: ViewRefs) {
    switch (interaction.type) {
      case 'mousemove':
      case 'wheel-control':
      case 'wheel-meta':
      case 'wheel-plain':
      case 'wheel-shift':
        this._updateHover(interaction.payload.location, viewRefs);
        break;
    }
  }

  _drawSnapshotImage(
    context: CanvasRenderingContext2D,
    snapshot: Snapshot,
    imageRect: Rect,
  ) {
    const visibleArea = this.visibleArea;

    // Prevent snapshot from visibly overflowing its container when clipped.
    // View clips by default, but since this view may draw async (on Image load) we re-clip.
    const shouldClip = !rectEqualToRect(imageRect, visibleArea);
    if (shouldClip) {
      const clippedRect = intersectionOfRects(imageRect, visibleArea);
      context.save();
      context.beginPath();
      context.rect(
        clippedRect.origin.x,
        clippedRect.origin.y,
        clippedRect.size.width,
        clippedRect.size.height,
      );
      context.closePath();
      context.clip();
    }

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

    // $FlowFixMe[incompatible-call] Flow doesn't know about the 9 argument variant of drawImage()
    context.drawImage(
      snapshot.image,

      // Image coordinates
      0,
      0,

      // Native image size
      snapshot.width,
      snapshot.height,

      // Canvas coordinates
      imageRect.origin.x + BORDER_SIZE,
      imageRect.origin.y + BORDER_SIZE,

      // Scaled image size
      imageRect.size.width - BORDER_SIZE * 2,
      imageRect.size.height - BORDER_SIZE * 2,
    );

    if (shouldClip) {
      context.restore();
    }
  }

  _findClosestSnapshot(x: number): Snapshot | null {
    const frame = this.frame;
    const scaleFactor = positioningScaleFactor(
      this._intrinsicSize.width,
      frame,
    );

    const snapshots = this._profilerData.snapshots;

    let startIndex = 0;
    let stopIndex = snapshots.length - 1;
    while (startIndex <= stopIndex) {
      const currentIndex = Math.floor((startIndex + stopIndex) / 2);
      const snapshot = snapshots[currentIndex];
      const {timestamp} = snapshot;

      const snapshotX = Math.floor(
        timestampToPosition(timestamp, scaleFactor, frame),
      );

      if (x < snapshotX) {
        stopIndex = currentIndex - 1;
      } else {
        startIndex = currentIndex + 1;
      }
    }

    return snapshots[stopIndex] || null;
  }

  /**
   * @private
   */
  _updateHover(location: Point, viewRefs: ViewRefs) {
    const {onHover, visibleArea} = this;
    if (!onHover) {
      return;
    }

    if (!rectContainsPoint(location, visibleArea)) {
      if (this._hoverLocation !== null) {
        this._hoverLocation = null;

        this.setNeedsDisplay();
      }

      onHover(null);
      return;
    }

    const snapshot = this._findClosestSnapshot(location.x);
    if (snapshot !== null) {
      this._hoverLocation = location;

      onHover(snapshot);
    } else {
      this._hoverLocation = null;

      onHover(null);
    }

    // Any time the mouse moves within the boundaries of this view, we need to re-render.
    // This is because we draw a scrubbing bar that shows the location corresponding to the current tooltip.
    this.setNeedsDisplay();
  }
}