import MouseInteractionManager from "@client/canvas/interaction/mouse-handler.mjs";
import {RenderFlagsMixin} from "@client/canvas/interaction/render-flags.mjs";

/**
 * @import {BaseShapeData} from "@common/data/_module.mjs";
 * @import Document from "@common/abstract/document.mjs";
 * @import {PlaceableObject} from "@client/canvas/placeables/_module.mjs";
 * @import {PlaceablesLayer} from "@client/canvas/layers/_module.mjs";
 * @import {Point, DeepReadonly} from "@common/_types.mjs";
 */

/**
 * Controls for a shape.
 * @template {Document} DocumentClass
 * @template {PlaceableObject} ObjectClass
 * @template {PlaceablesLayer} LayerClass
 * @template {BaseShapeData} [ShapeClass=BaseShapeData]
 */
export class ShapeControls extends RenderFlagsMixin(PIXI.Container) {
  /**
   * @param {ShapeClass} shape    The shape.
   */
  constructor(shape) {
    super();
    this.#shape = shape;
    this.#border = this.addChild(new PIXI.Graphics());
    this.#border.eventMode = "none";
    this.#handles = this.addChild(new PIXI.Container());
    this.#handles.eventMode = "passive";
  }

  /* -------------------------------------------- */

  /** @override */
  static RENDER_FLAG_PRIORITY = "INTERFACE";

  /** @override */
  static RENDER_FLAGS = {
    redraw: {propagate: ["refresh"]},
    refresh: {}
  };

  /* -------------------------------------------- */

  /**
   * The shape.
   * @type {ShapeClass}
   */
  get shape() {
    return this.#shape;
  }

  #shape;

  /* -------------------------------------------- */

  /**
   * The Document of this shape.
   * @type {DocumentClass}
   */
  get document() {
    return this.shape.parent;
  }

  /* -------------------------------------------- */

  /**
   * The PlaceableObject of this shape.
   * @type {ObjectClass}
   */
  get object() {
    return this.document.object;
  }

  /* -------------------------------------------- */

  /**
   * The PlaceableLayer of this shape.
   * @type {LayerClass}
   */
  get layer() {
    return this.object.layer;
  }

  /* -------------------------------------------- */

  /**
   * The border of the shape.
   * @type {PIXI.Graphics}
   */
  get border() {
    return this.#border;
  }

  #border;

  /* -------------------------------------------- */

  /**
   * The handles of the shape.
   * @type {PIXI.Container<ShapeControlsHandle>}
   */
  get handles() {
    return this.#handles;
  }

  #handles;

  /* -------------------------------------------- */

  /**
   * The handle offset delta.
   * @type {number}
   */
  #handleOffsetDelta;

  /* -------------------------------------------- */

  /**
   * An internal reference to a Promise in-progress to draw these controls.
   * @type {Promise<this>}
   */
  #drawing = Promise.resolve(this);

  /* -------------------------------------------- */

  /**
   * Have these controls been drawn and is there no drawing in progress?
   * @type {boolean}
   */
  #drawn = false;

  /* -------------------------------------------- */

  /**
   * The tint applied to these controls.
   * @type {number}
   * @defaultValue 0xFFFFFF
   */
  get tint() {
    return this.#tintColor.value;
  }

  set tint(tint) {
    const currentTint = this.#tintColor.value;
    this.#tintColor.setValue(tint);
    if ( currentTint === this.#tintColor.value ) return;
    this.#border.tint = this.#tintColor.value;
    for ( const handle of this.#handles.children ) handle.tint = this.#tintColor.value;
  }

  /* ---------------------------------------- */

  /**
   * The tint applied to the controls.
   * @type {PIXI.Color}
   */
  #tintColor = new PIXI.Color(0xFFFFFF);

  /* -------------------------------------------- */

  /**
   * Are the controls editable?
   * @type {boolean}
   * @defaultValue true
   */
  editable = true;

  /* -------------------------------------------- */

  /**
   * Is the border dashed?
   * @type {boolean}
   * @defaultValue false
   */
  get dashed() {
    return this.#dashed;
  }

  set dashed(value) {
    if ( this.#dashed === value ) return;
    this.#dashed = value;
    this.refresh();
  }

  #dashed = false;

  /* -------------------------------------------- */
  /*  Incremental Refresh                         */
  /* -------------------------------------------- */

  /** @override */
  applyRenderFlags() {
    if ( !this.renderFlags.size || this._destroyed ) return;
    const flags = this.renderFlags.clear();

    // Full re-draw
    if ( flags.redraw ) {
      this.draw();
      return;
    }

    // Don't refresh until these controls are drawn
    if ( !this.#drawn ) return;

    if ( flags.refresh ) this._refresh();
  }

  /* -------------------------------------------- */

  /**
   * Refresh the visualization of these controls.
   * @protected
   */
  _refresh() {

    // Refresh border
    foundry.canvas.borders.drawBorder(this.#border, this._drawShape.bind(this), {dashed: this.#dashed});

    // Refresh the position and rotation of each handle
    const offsets = {};
    for ( const handle of this.#handles.children ) {
      const {position, rotation, visible} = this.#shape.controlHandles[handle.name];
      const positionKey = `${position.x},${position.y}`;
      const offset = offsets[positionKey] = (offsets[positionKey] ?? -1) + 1;
      handle.position = position;
      handle.pivot.set(-offset * this.#handleOffsetDelta, 0);
      handle.rotation = rotation;
      handle.visible = visible;
    }
    MouseInteractionManager.emulateMoveEvent();
  }

  /* -------------------------------------------- */

  /**
   * Refresh the visualization of these controls.
   */
  refresh() {
    this.renderFlags.set({refresh: true});
  }

  /* -------------------------------------------- */
  /*  Rendering                                   */
  /* -------------------------------------------- */

  /**
   * Draw the shape.
   * @param {PIXI.Graphics} graphics
   * @protected
   */
  _drawShape(graphics) {
    this.#shape.drawShape(graphics);
  }

  /* -------------------------------------------- */

  /**
   * Draw the visualization of these controls.
   * @returns {Promise<this>}
   */
  async draw() {
    return this.#drawing = this.#drawing.finally(async () => {
      this.#drawn = false;
      this._clear();
      await this._draw();
      this.refresh();
      this.#drawn = true;
    });
  }

  /* -------------------------------------------- */

  /**
   * Draw these controls.
   * @returns {Promise<void>}
   * @protected
   */
  async _draw() {
    const thickness = CONFIG.Canvas.objectBorderThickness * canvas.dimensions.uiScale;
    const handleSize = thickness * 4;
    const outlineThickness = thickness / 4;

    // Create and draw handles
    this.#handleOffsetDelta = handleSize + (3 * outlineThickness);
    const handles = [];
    const handleStyle = {size: handleSize, outlineThickness};
    for ( const action of Object.keys(this.#shape.controlHandles) ) {
      const handle = new ShapeControlsHandle(this, action);
      handle.tint = this.tint;
      handles.push(handle);
    }
    await Promise.all(handles.map(handle => handle.draw(handleStyle)));
    for ( const handle of handles ) this.#handles.addChild(handle);
  }

  /* -------------------------------------------- */

  /**
   * Clear these controls.
   * @protected
   */
  _clear() {
    this.#border.clear();
    this.#handles.removeChildren().forEach(c => c.destroy({children: true}));
    MouseInteractionManager.emulateMoveEvent();
  }

  /* -------------------------------------------- */

  /** @inheritDoc */
  destroy(options) {
    this.renderFlags.clear();
    MouseInteractionManager.emulateMoveEvent();
    super.destroy(options);
  }

  /* -------------------------------------------- */
  /*  Interactivity                               */
  /* -------------------------------------------- */

  /**
   * Can the handle be dragged?
   * @param {PIXI.FederatedEvent} event      The pointer event
   * @param {{notify: boolean}} [options]    Options, used internally
   * @returns {boolean}
   * @protected
   */
  _canDragStart(event, options) {
    return this.editable;
  }

  /* -------------------------------------------- */

  /**
   * Handle the drag start event of a handle.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   * @protected
   */
  _onDragStart(event) {
    event.interactionData.shape = this.shape.clone();
    event.interactionData.preview = ShapeControls._createDragPreview(this.object);
    event.interactionData.unlinked = false;
    event.interactionData.cancelOnPause = true;
  }

  /* -------------------------------------------- */

  /**
   * Create and draw the drag preview for a placeable object.
   * @param {ObjectClass} object    The original placeable object
   * @returns {ObjectClass}         The preview of the placeable object.
   * @internal
   */
  static _createDragPreview(object) {
    const preview = object.clone();
    preview._previewType = "controls";
    preview._onDragStart();
    object.layer.preview.addChild(preview);
    preview.draw();
    return preview;
  }

  /* -------------------------------------------- */

  /**
   * Handle the drag move event of a handle.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   * @protected
   */
  _onDragMove(event) {
    canvas._onDragCanvasPan(event);
    const {handle, destination, shape, unlinked: wasUnlinked} = event.interactionData;
    const isUnlinked = event.altKey;
    if ( wasUnlinked !== isUnlinked ) {
      event.interactionData.unlinked = isUnlinked;
      if ( wasUnlinked ) shape.updateSource(this.shape.toObject());
    }
    shape.moveControlHandle(handle.name, destination, {snap: !event.shiftKey, unlinked: isUnlinked});
    this._updateDragPreview(event);
  }

  /* -------------------------------------------- */

  /**
   * Update the drag preview. Called when the shape has changed.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   * @protected
   */
  _updateDragPreview(event) {
    const {shape, preview} = event.interactionData;
    if ( this.document.schema.has("shapes") ) {
      const shapes = this.document.toObject().shapes;
      shapes[this.shape._index] = shape.toObject();
      preview.document.updateSource({shapes});
      preview.renderFlags.set({refreshShapes: true});
    } else if ( this.document.schema.has("shape") ) {
      preview.document.updateSource({shape: shape.toObject()});
      preview.renderFlags.set({refreshShape: true});
    } else {
      throw new Error(`${this.constructor.name} must implement the _updateDragPreview method.`);
    }
  }

  /* -------------------------------------------- */

  /** @inheritDoc */
  _onDragDrop(event) {
    event.interactionData.clearPreviewContainer = false;
    let result = this._prepareDragDropUpdate(event);
    if ( !Array.isArray(result[0]) ) result = [result];
    const [data, options] = result;
    this.#commitDragDropUpdate(data, options);
  }

  /* -------------------------------------------- */

  /**
   * Prepare the database update that should occur as the result of a drop operation.
   * @param {PIXI.FederatedEvent} event                    The pointer event.
   * @returns {object|[data: object, options?: object]}    The update data and options (optional)
   * @protected
   */
  _prepareDragDropUpdate(event) {
    const {preview} = event.interactionData;
    if ( this.document.schema.has("shapes") ) {
      return {shapes: preview.document.toObject().shapes};
    } else if ( this.document.schema.has("shape") ) {
      return {shape: preview.document.toObject().shape};
    } else {
      throw new Error(`${this.constructor.name} must implement the _prepareDragDropUpdate method.`);
    }
  }

  /* -------------------------------------------- */

  /**
   * Perform the database update using the result of a drop operation.
   * @param {object} data         The update data.
   * @param {object} [options]    Additional update options.
   * @returns {Promise<void>}
   */
  async #commitDragDropUpdate(data, options) {
    await this.document.update(data, options);
    this.layer.clearPreviewContainer();
  }

  /* -------------------------------------------- */

  /**
   * Handle the drag cancel event of a handle.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   * @protected
   */
  _onDragCancel(event) {
    if ( event.interactionData.clearPreviewContainer !== false ) {
      this.layer.clearPreviewContainer();
    }
  }

  /* -------------------------------------------- */

  /**
   * Handle the double left-click event of a handle.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   * @protected
   */
  _onClick2(event) {
    if ( this.document.schema.has("shapes") || this.document.schema.has("shape") ) {
      new foundry.applications.apps.ShapeConfig({shape: this.shape}).render({force: true});
    } else {
      this.document.sheet.render({force: true});
    }
  }
}

/* -------------------------------------------- */

/**
 * A handle of a shape controls element.
 */
export class ShapeControlsHandle extends PIXI.smooth.SmoothGraphics {
  /**
   * @param {ShapeControls} controls    The controls this handle belongs to.
   * @param {string} name               The name of this handle.
   */
  constructor(controls, name) {
    super();
    this.#controls = controls;
    this.name = name;
    this.cursor = "grab";
    this.eventMode = "static";
    this.on("pointerover", this.#onHoverIn.bind(this));
    this.on("pointerout", this.#onHoverOut.bind(this));
  }

  /* -------------------------------------------- */

  /**
   * The controls that this handle belongs to.
   * @type {ShapeControls}
   */
  get controls() {
    return this.#controls;
  }

  #controls;

  /* -------------------------------------------- */

  /**
   * Is hovered?
   * @type {boolean}
   */
  get hovered() {
    return this.#hovered;
  }

  #hovered = false;

  /* -------------------------------------------- */
  /*  Rendering                                   */
  /* -------------------------------------------- */

  /**
   * Draw the handle.
   * @param {{size: number; outlineThickness: number}} style    The style.
   * @returns {Promise<void>}
   */
  async draw({size, offset, outlineThickness}) {
    this.clear();
    this.beginFill(0xFFFFFF);
    this.lineStyle(outlineThickness, 0x000000);
    const radius = (size + outlineThickness) / 2;
    switch ( this.name ) {
      case "translate": this.drawCircle(0, 0, radius); break;
      case "scale":
      case "scaleX":
      case "scaleY":
      case "inset":
      case "offset":
      case "sweep": this.drawRect(-radius, -radius, radius * 2, radius * 2); break;
      case "rotate": this.drawPolygon(radius, 0, -radius, radius, -radius, -radius); break;
    }
    this.endFill();
  }

  /* -------------------------------------------- */
  /*  Interactivity                               */
  /* -------------------------------------------- */

  /**
   * Handle the handle being hovered in.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   */
  #onHoverIn(event) {
    this.#hovered = true;
    this.#controls.object._hoveredHandle = this;
  }

  /* -------------------------------------------- */

  /**
   * Handle the handle being hovered out.
   * @param {PIXI.FederatedEvent} event    The pointer event.
   */
  #onHoverOut(event) {
    this.#hovered = false;
    if ( this.#controls.object._hoveredHandle === this ) {
      this.#controls.object._hoveredHandle = null;
    }
  }
}
