// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/no-imperative-dom-api */

import * as Platform from '../../../../core/platform/platform.js';
import * as Geometry from '../../../../models/geometry/geometry.js';
import * as VisualLogging from '../../../visual_logging/visual_logging.js';
import * as UI from '../../legacy.js';

import type {AnimationTimingModel} from './AnimationTimingModel.js';
import {BezierUI} from './BezierUI.js';
import {CSSLinearEasingModel, type Point} from './CSSLinearEasingModel.js';

const DOUBLE_CLICK_DELAY = 500;

interface Params {
  container: Element;
  bezier: Geometry.CubicBezier;
  onBezierChange: (bezier: Geometry.CubicBezier) => void;
}

class BezierCurveUI {
  #curveUI: BezierUI;
  #bezier: Geometry.CubicBezier;
  #curve: Element;
  #mouseDownPosition?: Geometry.Point;
  #controlPosition?: Geometry.Point;
  #selectedPoint?: number;
  #onBezierChange: (bezier: Geometry.CubicBezier) => void;

  constructor({bezier, container, onBezierChange}: Params) {
    this.#bezier = bezier;
    this.#curveUI = new BezierUI({
      width: 150,
      height: 250,
      marginTop: 50,
      controlPointRadius: 7,
      shouldDrawLine: true,
    });

    this.#curve = UI.UIUtils.createSVGChild(container, 'svg', 'bezier-curve');
    this.#onBezierChange = onBezierChange;

    UI.UIUtils.installDragHandle(
        this.#curve, this.dragStart.bind(this), this.dragMove.bind(this), this.dragEnd.bind(this), 'default');
  }

  private dragStart(event: MouseEvent): boolean {
    this.#mouseDownPosition = new Geometry.Point(event.x, event.y);
    const ui = this.#curveUI;
    this.#controlPosition = new Geometry.Point(
        Platform.NumberUtilities.clamp((event.offsetX - ui.radius) / ui.curveWidth(), 0, 1),
        (ui.curveHeight() + ui.marginTop + ui.radius - event.offsetY) / ui.curveHeight());

    const firstControlPointIsCloser = this.#controlPosition.distanceTo(this.#bezier.controlPoints[0]) <
        this.#controlPosition.distanceTo(this.#bezier.controlPoints[1]);
    this.#selectedPoint = firstControlPointIsCloser ? 0 : 1;

    this.#bezier.controlPoints[this.#selectedPoint] = this.#controlPosition;
    this.#onBezierChange(this.#bezier);

    event.consume(true);
    return true;
  }

  private updateControlPosition(mouseX: number, mouseY: number): void {
    if (this.#mouseDownPosition === undefined || this.#controlPosition === undefined ||
        this.#selectedPoint === undefined) {
      return;
    }
    const deltaX = (mouseX - this.#mouseDownPosition.x) / this.#curveUI.curveWidth();
    const deltaY = (mouseY - this.#mouseDownPosition.y) / this.#curveUI.curveHeight();
    const newPosition = new Geometry.Point(
        Platform.NumberUtilities.clamp(this.#controlPosition.x + deltaX, 0, 1), this.#controlPosition.y - deltaY);
    this.#bezier.controlPoints[this.#selectedPoint] = newPosition;
  }

  private dragMove(event: MouseEvent): void {
    this.updateControlPosition(event.x, event.y);
    this.#onBezierChange(this.#bezier);
  }

  private dragEnd(event: MouseEvent): void {
    this.updateControlPosition(event.x, event.y);
    this.#onBezierChange(this.#bezier);
  }

  setBezier(bezier: Geometry.CubicBezier): void {
    this.#bezier = bezier;
    this.draw();
  }

  draw(): void {
    this.#curveUI.drawCurve(this.#bezier, this.#curve);
  }
}

interface LinearEasingPresentationParams {
  width: number;
  height: number;
  marginTop: number;
  pointRadius: number;
}
interface Position {
  x: number;
  y: number;
}
class LinearEasingPresentation {
  params: LinearEasingPresentationParams;
  renderedPositions?: Position[];

  constructor(params: LinearEasingPresentationParams) {
    this.params = params;
  }

  #curveWidth(): number {
    return this.params.width - this.params.pointRadius * 2;
  }

  #curveHeight(): number {
    return this.params.height - this.params.pointRadius * 2 - this.params.marginTop * 2;
  }

  #drawControlPoint(parentElement: Element, controlX: number, controlY: number, index: number): void {
    const circle = UI.UIUtils.createSVGChild(parentElement, 'circle', 'bezier-control-circle');
    circle.setAttribute(
        'jslog', `${VisualLogging.controlPoint('bezier.linear-control-circle').track({drag: true, dblclick: true})}`);
    circle.setAttribute('data-point-index', String(index));
    circle.setAttribute('cx', String(controlX));
    circle.setAttribute('cy', String(controlY));
    circle.setAttribute('r', String(this.params.pointRadius));
  }

  timingPointToPosition(point: Point): Position {
    return {
      x: (point.input / 100) * this.#curveWidth() + this.params.pointRadius,
      y: (1 - point.output) * this.#curveHeight() + this.params.pointRadius,
    };
  }

  positionToTimingPoint(position: Position): Point {
    return {
      input: ((position.x - this.params.pointRadius) / this.#curveWidth()) * 100,
      output: 1 - (position.y - this.params.pointRadius) / this.#curveHeight(),
    };
  }

  draw(linearEasingModel: CSSLinearEasingModel, svg: Element): void {
    svg.setAttribute('width', String(this.#curveWidth()));
    svg.setAttribute('height', String(this.#curveHeight()));
    svg.removeChildren();
    const group = UI.UIUtils.createSVGChild(svg, 'g');

    const positions = linearEasingModel.points().map(point => this.timingPointToPosition(point));
    this.renderedPositions = positions;
    let startingPoint = positions[0];
    for (let i = 1; i < positions.length; i++) {
      const position = positions[i];
      const line = UI.UIUtils.createSVGChild(group, 'path', 'bezier-path linear-path');
      line.setAttribute('d', `M ${startingPoint.x} ${startingPoint.y} L ${position.x} ${position.y}`);
      line.setAttribute('data-line-index', String(i));
      startingPoint = position;
    }

    for (let i = 0; i < positions.length; i++) {
      const point = positions[i];
      this.#drawControlPoint(group, point.x, point.y, i);
    }
  }
}

class LinearEasingUI {
  #model: CSSLinearEasingModel;
  #onChange: (model: CSSLinearEasingModel) => void;
  #presentation: LinearEasingPresentation;
  #selectedPointIndex?: number;
  #doubleClickTimer?: number;
  #pointIndexForDoubleClick?: number;
  #mouseDownPosition?: {x: number, y: number};

  #svg: Element;

  constructor({
    model,
    container,
    onChange,
  }: {
    model: CSSLinearEasingModel,
    container: HTMLElement,
    onChange: (model: CSSLinearEasingModel) => void,
  }) {
    this.#model = model;
    this.#onChange = onChange;
    this.#presentation = new LinearEasingPresentation({
      width: 150,
      height: 250,
      pointRadius: 7,
      marginTop: 50,
    });
    this.#svg = UI.UIUtils.createSVGChild(container, 'svg', 'bezier-curve linear');

    UI.UIUtils.installDragHandle(
        this.#svg, this.#dragStart.bind(this), this.#dragMove.bind(this), this.#dragEnd.bind(this), 'default');
  }

  #handleLineClick(event: MouseEvent, lineIndex: number): void {
    const newPoint = this.#presentation.positionToTimingPoint({x: event.offsetX, y: event.offsetY});
    this.#model.addPoint(newPoint, lineIndex);
    this.#selectedPointIndex = undefined;
    this.#mouseDownPosition = undefined;
  }

  #handleControlPointClick(event: MouseEvent, pointIndex: number): void {
    this.#selectedPointIndex = pointIndex;
    this.#mouseDownPosition = {x: event.x, y: event.y};

    // This is a workaround to understand whether the user double clicked
    // a point or not. The reason is, we also want to handle drag interactions
    // for the point and the way we install drag handlers (starting with mousedown event)
    // doesn't allow us to register a `dblclick` handler. So, we're checking
    // whether user double clicked (or mouse downed) a point with a timer.
    // `#pointIndexForDoubleClick` holds the point clicked in a double click
    // delay time frame. We reset that point after
    // the DOUBLE_CLICK_DELAY time has passed.
    clearTimeout(this.#doubleClickTimer);
    if (this.#pointIndexForDoubleClick === this.#selectedPointIndex) {
      this.#model.removePoint(this.#selectedPointIndex);
      this.#pointIndexForDoubleClick = undefined;
      this.#selectedPointIndex = undefined;
      this.#mouseDownPosition = undefined;
      return;
    }

    this.#pointIndexForDoubleClick = this.#selectedPointIndex;
    this.#doubleClickTimer = window.setTimeout(() => {
      this.#pointIndexForDoubleClick = undefined;
    }, DOUBLE_CLICK_DELAY);
  }

  #dragStart(event: MouseEvent): boolean {
    if (!(event.target instanceof SVGElement)) {
      return false;
    }

    if (event.target.dataset.lineIndex !== undefined) {
      this.#handleLineClick(event, Number(event.target.dataset.lineIndex));
      event.consume(true);
      return true;
    }

    if (event.target.dataset.pointIndex !== undefined) {
      this.#handleControlPointClick(event, Number(event.target.dataset.pointIndex));
      event.consume(true);
      return true;
    }

    return false;
  }

  #updatePointPosition(mouseX: number, mouseY: number): void {
    if (this.#selectedPointIndex === undefined || this.#mouseDownPosition === undefined) {
      return;
    }

    const controlPosition = this.#presentation.renderedPositions?.[this.#selectedPointIndex];
    if (!controlPosition) {
      return;
    }

    const deltaX = mouseX - this.#mouseDownPosition.x;
    const deltaY = mouseY - this.#mouseDownPosition.y;

    this.#mouseDownPosition = {
      x: mouseX,
      y: mouseY,
    };

    const newPoint = {
      x: controlPosition.x + deltaX,
      y: controlPosition.y + deltaY,
    };

    this.#model.setPoint(this.#selectedPointIndex, this.#presentation.positionToTimingPoint(newPoint));
  }

  #dragMove(event: MouseEvent): void {
    this.#updatePointPosition(event.x, event.y);
    this.#onChange(this.#model);
  }

  #dragEnd(event: MouseEvent): void {
    this.#updatePointPosition(event.x, event.y);
    this.#onChange(this.#model);
  }

  setCSSLinearEasingModel(model: CSSLinearEasingModel): void {
    this.#model = model;
    this.draw();
  }

  draw(): void {
    this.#presentation.draw(this.#model, this.#svg);
  }
}

export class PresetUI {
  #linearEasingPresentation: LinearEasingPresentation;
  #bezierPresentation: BezierUI;

  constructor() {
    this.#linearEasingPresentation = new LinearEasingPresentation({
      width: 40,
      height: 40,
      marginTop: 0,
      pointRadius: 2,
    });

    this.#bezierPresentation = new BezierUI({
      width: 40,
      height: 40,
      marginTop: 0,
      controlPointRadius: 2,
      shouldDrawLine: false,
    });
  }

  draw(model: AnimationTimingModel, svg: Element): void {
    if (model instanceof CSSLinearEasingModel) {
      this.#linearEasingPresentation.draw(model, svg);
    } else if (model instanceof Geometry.CubicBezier) {
      this.#bezierPresentation.drawCurve(model, svg);
    }
  }
}

interface AnimationTimingUIParams {
  model: AnimationTimingModel;
  onChange: (model: AnimationTimingModel) => void;
}
export class AnimationTimingUI {
  #container: HTMLElement;
  #bezierContainer: HTMLElement;
  #linearEasingContainer: HTMLElement;
  #model: AnimationTimingModel;
  #onChange: (model: AnimationTimingModel) => void;
  #bezierCurveUI?: BezierCurveUI;
  #linearEasingUI?: LinearEasingUI;

  constructor({model, onChange}: AnimationTimingUIParams) {
    this.#container = document.createElement('div');
    this.#container.className = 'animation-timing-ui';
    this.#container.style.width = '150px';
    this.#container.style.height = '250px';

    this.#bezierContainer = document.createElement('div');
    this.#bezierContainer.classList.add('bezier-ui-container');
    this.#linearEasingContainer = document.createElement('div');
    this.#linearEasingContainer.classList.add('linear-easing-ui-container');

    this.#container.appendChild(this.#bezierContainer);
    this.#container.appendChild(this.#linearEasingContainer);

    this.#model = model;
    this.#onChange = onChange;

    if (this.#model instanceof Geometry.CubicBezier) {
      this.#bezierCurveUI =
          new BezierCurveUI({bezier: this.#model, container: this.#bezierContainer, onBezierChange: this.#onChange});
    } else if (this.#model instanceof CSSLinearEasingModel) {
      this.#linearEasingUI = new LinearEasingUI({
        model: this.#model,
        container: this.#linearEasingContainer,
        onChange: this.#onChange,
      });
    }
  }

  element(): Element {
    return this.#container;
  }

  setModel(model: AnimationTimingModel): void {
    this.#model = model;
    if (this.#model instanceof Geometry.CubicBezier) {
      if (this.#bezierCurveUI) {
        this.#bezierCurveUI.setBezier(this.#model);
      } else {
        this.#bezierCurveUI =
            new BezierCurveUI({bezier: this.#model, container: this.#bezierContainer, onBezierChange: this.#onChange});
      }
    } else if (this.#model instanceof CSSLinearEasingModel) {
      if (this.#linearEasingUI) {
        this.#linearEasingUI.setCSSLinearEasingModel(this.#model);
      } else {
        this.#linearEasingUI =
            new LinearEasingUI({model: this.#model, container: this.#linearEasingContainer, onChange: this.#onChange});
      }
    }

    this.draw();
  }

  draw(): void {
    this.#linearEasingContainer.classList.toggle('hidden', !(this.#model instanceof CSSLinearEasingModel));
    this.#bezierContainer.classList.toggle('hidden', !(this.#model instanceof Geometry.CubicBezier));

    if (this.#bezierCurveUI) {
      this.#bezierCurveUI.draw();
    }

    if (this.#linearEasingUI) {
      this.#linearEasingUI.draw();
    }
  }
}
