import {BaseShapeData} from "@common/data/data.mjs";
import {getDocumentClass} from "@client/utils/helpers.mjs";
/**
 * @import PlaceablesLayer from "../base/placeables-layer.mjs";
 * @import PlaceableObject from "@client/canvas/placeables/placeable-object.mjs";
 */

/**
 * A mixin for UX shared between PlaceablesLayer with objects that have shapes.
 * @param {typeof PlaceablesLayer} Base    The PlaceablesLayer (sub)class.
 */
export default function ShapeLayerMixin(Base) {
  return class ShapeLayer extends Base {

    /** @inheritDoc */
    static get layerOptions() {
      return foundry.utils.mergeObject(super.layerOptions, {
        /**
         * The shape types that are allowed to be empty for the creation of a drawn object.
         * @type {string[]}
         */
        allowedEmptyShapes: [],
        /**
         * Discard the closing point of a polygon shape?
         * @type {boolean}
         */
        discardClosingPoint: true
      });
    }

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

    // TODO: https://github.com/foundryvtt/foundryvtt/issues/13831
    /**
     * The mouse wheel context.
     * @type {{preview: PlaceableObject; shape: BaseShapeData}|null}
     * @internal
     */
    _mouseWheelContext = null;

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

    /**
     * Listen for SHIFT/CTRL keyup events during mouse wheel rotation.
     * @type {() => void}
     */
    #mouseWheelKeyUpListener = () => {
      if ( !game.keyboard.isModifierActive("CONTROL") && !game.keyboard.isModifierActive("SHIFT") ) {
        this.#commitMouseWheel();
      }
    };

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

    /**
     * Can an object have multiple shapes?
     * @type {boolean}
     */
    get #multipleShapes() {
      return getDocumentClass(this.constructor.documentName).schema.fields.shapes?.max > 1;
    }

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

    /**
     * The objects of this layer that are either controlled or have their sheet rendered.
     * @type {PlaceableObject[]}
     */
    #getTargetedObjects() {
      const objects = [];
      for ( const object of this.placeables ) {
        if ( object.controlled || object.sheet.rendered ) objects.push(object);
      }
      return objects;
    }

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

    /** @override */
    getSnappedPoint(point) {
      const M = CONST.GRID_SNAPPING_MODES;
      const size = canvas.dimensions.size;
      return canvas.grid.getSnappedPoint({x: point.x, y: point.y}, canvas.forceSnapVertices ? {mode: M.VERTEX} : {
        mode: M.CENTER | M.VERTEX | M.CORNER | M.SIDE_MIDPOINT,
        resolution: size >= 128 ? 8 : (size >= 64 ? 4 : 2)
      });
    }

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

    /** @inheritDoc */
    _deactivate() {
      this._cancelMouseWheel();
      super._deactivate();
    }

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

    /** @inheritDoc */
    async _tearDown(options) {
      this._cancelMouseWheel();
      return super._tearDown(options);
    }

    /* -------------------------------------------- */
    /*  Event Listeners and Handlers                */
    /* -------------------------------------------- */

    /** @inheritDoc */
    _onClickLeft(event) {
      const interaction = event.interactionData;

      // Continue polygon point placement
      if ( interaction.shape?.type === "polygon" ) {
        const {destination, freehand} = interaction;
        const point = !event.shiftKey && !freehand ? this.getSnappedPoint(destination) : destination;
        const points = interaction.shape.points;

        // Clicking on the first point closes the shape
        if ( point.x.almostEqual(points.at(0)) && point.y.almostEqual(points.at(1)) ) {
          interaction.cancelled = true;
          if ( !this.options.discardClosingPoint ) {
            interaction.shape.updateSource({points: points.toSpliced(-2, 0, point.x, point.y)});
          }
        }

        // Add the point unless it's equal to the last one
        else if ( !(point.x.almostEqual(points.at(-4)) && point.y.almostEqual(points.at(-3))) ) {
          interaction.shape.updateSource({points: points.toSpliced(-2, 0, point.x, point.y)});
          this._updateDragPreview(event);
        }
        return;
      }

      // If one of the drawing tools is selected, prevent left-click-to-release
      if ( this._isCreationToolActive() ) return;

      // Standard left-click handling
      super._onClickLeft(event);
    }

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

    /** @inheritDoc */
    _onClickLeft2(event) {
      const interaction = event.interactionData;

      // Conclude polygon drawing with a double-click
      if ( interaction.shape?.type === "polygon" ) {
        interaction.cancelled = true;
        return;
      }

      // Standard double-click handling
      super._onClickLeft2(event);
    }

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

    /** @inheritDoc */
    _canDragLeftStart(user, event) {
      if ( !super._canDragLeftStart(user, event) ) return false;
      if ( this.#multipleShapes ) {
        const targets = this.#getTargetedObjects();
        if ( targets.length > 1 ) {
          const documentClass = getDocumentClass(this.constructor.documentName);
          ui.notifications.warn("SHAPE.NOTIFICATIONS.DrawingMultipleObjectsControlled", {format: {
            type: _loc(documentClass.metadata.label)}});
          return false;
        }
        if ( targets.at(0)?.document.locked ) {
          const documentClass = getDocumentClass(this.constructor.documentName);
          ui.notifications.warn("CONTROLS.ObjectIsLocked", {format: {
            type: _loc(documentClass.metadata.label)}});
          return false;
        }
      }
      return true;
    }

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

    /** @inheritDoc */
    _onDragLeftStart(event) {
      const interaction = event.interactionData;
      interaction.cancelOnPause = true;

      // Create the preview
      const object = this.#multipleShapes ? this.#getTargetedObjects()[0] : undefined;
      if ( object ) {
        interaction.preview = object.clone();
        interaction.preview._previewType = "creation";
        interaction.preview._onDragStart();
        interaction.preview.draw();
        this.preview.addChild(interaction.preview);
      }
      else super._onDragLeftStart(event);

      // Create the shape that is drag-created
      const shapeData = this._createDragShapeData(event);
      interaction.shape = new BaseShapeData.TYPES[shapeData.type](shapeData, {parent: interaction.preview.document});
      interaction.shape._onDragStart(event);
      interaction.cancelled = (interaction.shape.type !== "polygon") || !!interaction.freehand;
      this._updateDragPreview(event);
    }

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

    /** @override */
    _onDragLeftMove(event) {
      const interaction = event.interactionData;
      if ( !interaction.shape ) return;
      interaction.shape._onDragMove(event);
      this._updateDragPreview(event);
    }

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

    /** @inheritDoc */
    _onDragLeftDrop(event) {
      const interaction = event.interactionData;
      if ( !interaction.shape ) return;
      if ( interaction.shape.type === "polygon" ) {

        // In-progress polygon drawing
        if ( !interaction.cancelled ) {
          event.preventDefault();
          return;
        }

        // Slice off the preview point
        interaction.shape.updateSource({points: interaction.shape.points.slice(0, -2)});
        this._updateDragPreview(event);
      }

      // Do not create empty shapes
      if ( !this.options.allowedEmptyShapes.includes(interaction.shape.type) && interaction.shape.isEmpty ) return;

      super._onDragLeftDrop(event);
    }

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

    /** @inheritDoc */
    async _commitDragLeftDrop(event) {
      const interaction = event.interactionData;
      const preview = interaction.preview;
      if ( !preview || preview._destroyed ) return;

      // Add the shape to controlled object
      const object = preview._original;
      if ( object ) {
        if ( !object.document.locked ) {
          try {
            interaction.clearPreviewContainer = false;
            await object.document.update({shapes: preview.document.toObject().shapes});
          } finally {
            this.clearPreviewContainer();
          }
        }
        return;
      }

      return super._commitDragLeftDrop(event);
    }

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

    /** @inheritDoc */
    _onDragLeftCancel(event) {

      // Remove point from in-progress polygon drawing
      const interaction = event.interactionData;
      if ( (interaction.shape?.type === "polygon") && !interaction.cancelled ) {
        const points = interaction.shape.points.toSpliced(-4, 2);
        if ( points.length >= 4 ) {
          event.preventDefault();
          interaction.shape.updateSource({points});
          this._updateDragPreview(event);
          return;
        }
      }

      // Standard drag-cancel handling
      super._onDragLeftCancel(event);
    }

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

    /**
     * Create the shape data from the drag start event.
     * @param {PIXI.FederatedEvent} event    The pointer event
     * @returns {object}                     The initial shape data
     * @protected
     */
    _createDragShapeData(event) {
      const shapeData = foundry.utils.deepClone(ui.controls.tool?.shapeData);
      if ( !shapeData ) throw new Error("A shape drawing tool must define shapeData.");
      shapeData.hole = ui.controls.tools.hole?.active ?? false;
      return shapeData;
    }

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

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

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

    /** @override */
    _onMouseWheel(event) {
      if ( !this._mouseWheelContext ) {

        // Prevent wheel rotation during dragging
        if ( this.preview.children.length ) return;

        // The hover must be controlled to be rotated
        if ( this.options.controllableObjects && !this.hover?.controlled ) return;

        // Rotate if the translate handle is hovered
        const hoveredHandle = this.hover?.hoveredHandle;
        if ( hoveredHandle?.name !== "translate" ) return;
        const hoveredShape = hoveredHandle.controls.shape;
        if ( hoveredShape.hasRotationalSymmetry ) return;

        if ( game.paused && !game.user.isGM ) {
          ui.notifications.warn("GAME.PausedWarning", {localize: true});
          return;
        }

        const document = hoveredShape.parent;
        if ( document.locked ) {
          ui.notifications.warn("CONTROLS.ObjectIsLocked",
            {format: {type: _loc(document.constructor.metadata.label)}});
          return false;
        }

        this._mouseWheelContext = this.#createMouseWheelContext(hoveredShape);
      }

      this._updateMouseWheelShape(event);
      this._updateMouseWheelPreview();
    }

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

    /**
     * Create the mouse wheel context.
     * @param {BaseShapeData} shape    The shape that is to be rotated.
     * @returns {object}
     */
    #createMouseWheelContext(shape) {
      const document = shape.parent;

      // Create and draw preview region
      const preview = document.object.clone();
      preview._previewType = "wheel";
      preview._onDragStart();
      this.preview.addChild(preview);
      preview.draw();

      // Listen for SHIFT/CTRL keyup events
      window.addEventListener("keyup", this.#mouseWheelKeyUpListener);

      return {preview, shape: shape.clone()};
    }

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

    /**
     * Commit the mouse wheel rotation update.
     * @returns {Promise<void>}
     */
    async #commitMouseWheel() {
      if ( !this._mouseWheelContext ) return;
      window.removeEventListener("keyup", this.#mouseWheelKeyUpListener);
      let result = this._prepareMouseWheelUpdate();
      if ( !Array.isArray(result[0]) ) result = [result];
      const [data, options] = result;
      const {preview, shape} = this._mouseWheelContext;
      const document = shape.parent;
      this._mouseWheelContext = null;
      try {
        if ( game.paused && !game.user.isGM ) {
          ui.notifications.warn("GAME.PausedWarning", {localize: true});
          return;
        }
        await document.update(data, options);
      } finally {
        if ( !preview.destroyed ) {
          preview._onDragEnd();
          preview.destroy({children: true});
        }
      }
    }

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

    /**
     * Cancel mouse wheel rotation.
     * @protected
     */
    _cancelMouseWheel() {
      if ( !this._mouseWheelContext ) return;
      window.removeEventListener("keyup", this.#mouseWheelKeyUpListener);
      const {preview} = this._mouseWheelContext;
      this._mouseWheelContext = null;
      if ( !preview.destroyed ) {
        preview._onDragEnd();
        preview.destroy({children: true});
      }
    }

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

    /**
     * Rotate the shape of the preview.
     * @param {WheelEvent} event   The mouse wheel event
     */
    _updateMouseWheelShape(event) {
      const {shape} = this._mouseWheelContext;
      const snap = shape.type === "grid" ? (canvas.grid.isSquare ? 90 : 60) : (event.shiftKey ? 15 : 5);
      const delta = snap * Math.sign(event.delta);
      shape.rotate((shape.rotation + delta).toNearest(snap) - shape.rotation);
    }

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

    /**
     * Update the mouse wheel rotation preview.
     * @protected
     */
    _updateMouseWheelPreview() {
      const {preview, shape} = this._mouseWheelContext;
      if ( preview.document.schema.has("shapes") ) {
        const shapes = preview.document.toObject().shapes;
        shapes[shape._index] = shape.toObject();
        preview.document.updateSource({shapes});
        preview.renderFlags.set({refreshShapes: true});
      } else if ( preview.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 _updateMouseWheelPreview method.`);
      }
    }

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

    /**
     * Prepare the database update that should occur as the result of a mouse wheel rotation.
     * @returns {object|[data: object, options?: object]}    The update data and options (optional)
     * @protected
     */
    _prepareMouseWheelUpdate() {
      const {preview} = this._mouseWheelContext;
      if ( preview.document.schema.has("shapes") ) {
        return {shapes: preview.document.toObject().shapes};
      } else if ( preview.document.schema.has("shape") ) {
        return {shape: preview.document.toObject().shape};
      } else if ( preview.document.schema.has("rotation") ) {
        return {rotation: preview.document._source.rotation};
      } else {
        throw new Error(`${this.constructor.name} must implement the _prepareMouseWheelUpdate method.`);
      }
    }
  };
}
