/* 
 * This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
 * Copyright (c) 2021 Matthew Haentschke.
 * 
 * This program is free software: you can redistribute it and/or modify  
 * it under the terms of the GNU General Public License as published by  
 * the Free Software Foundation, version 3.
 *
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License 
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import { logger } from './logger.js'
import { MODULE } from './module.js'

/** @typedef {import('@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs/measuredTemplateData.js').MeasuredTemplateDataProperties} MeasuredTemplateProperties */

/**
 * Contains all fields from `MeasuredTemplate#toObject`, plus the following.
 * 
 * @typedef {Object} CrosshairsData
 * @borrows MeasuredTemplateProperties
 * @prop {boolean} cancelled Workflow cancelled via right click (true)
 * @prop {Scene} scene Scene on this crosshairs was last active
 * @prop {number} radius Final radius of template, in pixels
 * @prop {number} size Final diameter of template, in grid units
 */

/**
 * @class
 */
export class Crosshairs extends MeasuredTemplate {

  //constructor(gridSize = 1, data = {}){
  constructor(config, callbacks = {}) {
    const templateData = {
      t: "circle",
      user: game.user.id,
      distance: config.size,
      x: config.x,
      y: config.y,
      fillColor: config.fillColor,
      width: 1,
      texture: config.texture,
      direction: config.direction,
    }

    const template = new CONFIG.MeasuredTemplate.documentClass(templateData, {parent: canvas.scene});
    super(template);

    /** @TODO all of these fields should be part of the source data schema for this class **/
    /**  image path to display in the center (under mouse cursor) */
    this.icon = config.icon ?? Crosshairs.ERROR_TEXTURE;

    /** text to display below crosshairs' circle */
    this.label = config.label;

    /** Offsets the default position of the label (in pixels) */
    this.labelOffset = config.labelOffset;

    /**
     * Arbitrary field used to identify this instance
     * of a Crosshairs in the canvas.templates.preview
     * list
     */
    this.tag = config.tag;

    /** Should the center icon be shown? */
    this.drawIcon = config.drawIcon;

    /** Should the outer circle be shown? */
    this.drawOutline = config.drawOutline;

    /** Opacity of the fill color */
    this.fillAlpha = config.fillAlpha;

    /** Should the texture (if any) be tiled
     * or scaled and offset? */
    this.tileTexture = config.tileTexture;

    /** locks the size of crosshairs (shift+scroll) */
    this.lockSize = config.lockSize;

    /** locks the position of crosshairs */
    this.lockPosition = config.lockPosition;

    /** Number of quantization steps along
     * a square's edge (N+1 snap points 
     * along each edge, conting endpoints)
     */
    this.interval = config.interval;

    /** Callback functions to execute
     * at particular times
     */
    this.callbacks = callbacks;

    /** Indicates if the user is actively 
     * placing the crosshairs.
     * Setting this to true in the show
     * callback will stop execution
     * and report the current mouse position
     * as the chosen location
     */
    this.inFlight = false;

    /** indicates if the placement of
     * crosshairs was canceled (with
     * a right click)
     */
    this.cancelled = true;

    /**
     * Indicators on where cancel was initiated
     * for determining if it was a drag or a cancel
     */
    this.rightX = 0;
    this.rightY = 0;

    /** @type {number} */
    this.radius = this.document.distance * this.scene.grid.size / 2;
  }

  /**
   * @returns {CrosshairsData} Current Crosshairs class data
   */
  toObject() {

    /** @type {CrosshairsData} */
    const data = foundry.utils.mergeObject(this.document.toObject(), {
      cancelled: this.cancelled,
      scene: this.scene,
      radius: this.radius,
      size: this.document.distance,
    });
    delete data.width;
    return data;
  }

  static ERROR_TEXTURE = 'icons/svg/hazard.svg'

  /**
   * Will retrieve the active crosshairs instance with the defined tag identifier.
   * @param {string} key Crosshairs identifier. Will be compared against the Crosshairs `tag` field for strict equality.
   * @returns {PIXI.DisplayObject|undefined}
   */
  static getTag(key) {
    return canvas.templates.preview.children.find( child => child.tag === key )
  }

  static getSnappedPosition({x,y}, interval){
    const offset = interval < 0 ? canvas.grid.size/2 : 0;
    const snapped = canvas.grid.getSnappedPosition(x - offset, y - offset, interval);
    return {x: snapped.x + offset, y: snapped.y + offset};
  }

  /* -----------EXAMPLE CODE FROM MEASUREDTEMPLATE.JS--------- */
  /* Portions of the core package (MeasuredTemplate) repackaged 
   * in accordance with the "Limited License Agreement for Module 
   * Development, found here: https://foundryvtt.com/article/license/ 
   * Changes noted where possible
   */

  /**
   * Set the displayed ruler tooltip text and position
   * @private
   */
  //BEGIN WARPGATE
  _setRulerText() {
    this.ruler.text = this.label;
    /** swap the X and Y to use the default dx/dy of a ray (pointed right)
    //to align the text to the bottom of the template */
    this.ruler.position.set(-this.ruler.width / 2 + this.labelOffset.x, this.template.height / 2 + 5 + this.labelOffset.y);
    //END WARPGATE
  }

  /** @override */
  async draw() {
    this.clear();

    // Load the texture
    const texture = this.document.texture;
    if ( texture ) {
      this._texture = await loadTexture(texture, {fallback: 'icons/svg/hazard.svg'});
    } else {
      this._texture = null;
    }

    // Template shape
    this.template = this.addChild(new PIXI.Graphics());

    // Rotation handle
    //BEGIN WARPGATE
    //this.handle = this.addChild(new PIXI.Graphics());
    //END WARPGATE

    // Draw the control icon
    //if(this.drawIcon) 
    this.controlIcon = this.addChild(this._drawControlIcon());

    // Draw the ruler measurement
    this.ruler = this.addChild(this._drawRulerText());

    // Update the shape and highlight grid squares
    this.refresh();
    //BEGIN WARPGATE
    this._setRulerText();
    //this.highlightGrid();
    //END WARPGATE

    // Enable interactivity, only if the Tile has a true ID
    if ( this.id ) this.activateListeners();
    return this;
  }

  /**
   * Draw the Text label used for the MeasuredTemplate
   * @return {PreciseText}
   * @protected
   */
  _drawRulerText() {
    const style = CONFIG.canvasTextStyle.clone();
    style.fontSize = Math.max(Math.round(canvas.dimensions.size * 0.36 * 12) / 12, 36);
    const text = new PreciseText(null, style);
    //BEGIN WARPGATE
    //text.anchor.set(0.5, 0);
    text.anchor.set(0, 0);
    //END WARPGATE
    return text;
  }

  /**
   * Draw the ControlIcon for the MeasuredTemplate
   * @return {ControlIcon}
   * @protected
   */
  _drawControlIcon() {
    const size = Math.max(Math.round((canvas.dimensions.size * 0.5) / 20) * 20, 40);

    //BEGIN WARPGATE
    let icon = new ControlIcon({texture: this.icon, size: size});
    icon.visible = this.drawIcon;
    //END WARPGATE

    icon.pivot.set(size*0.5, size*0.5);
    //icon.x -= (size * 0.5);
    //icon.y -= (size * 0.5);
    icon.angle = this.document.direction;
    return icon;
  }

  /** @override */
  refresh() {
    if (!this.template) return;
    let d = canvas.dimensions;
    const document = this.document;
    this.position.set(document.x, document.y);

    // Extract and prepare data
    let {direction, distance} = document;
    distance *= (d.size/2);
    //BEGIN WARPGATE
    //width *= (d.size / d.distance);
    //END WARPGATE
    direction = Math.toRadians(direction);

    // Create ray and bounding rectangle
    this.ray = Ray.fromAngle(document.x, document.y, direction, distance);

    // Get the Template shape
    switch (document.t) {
      case "circle":
        this.shape = this._getCircleShape(distance);
        break;
      default: logger.error("Non-circular Crosshairs is unsupported!");
    }

    // Draw the Template outline
    this.template.clear()
      .lineStyle(this._borderThickness, this.borderColor, this.drawOutline ? 0.75 : 0)

    // Fill Color or Texture

    if (this._texture) {
      /* assume 0,0 is top left of texture
       * and scale/offset this texture (due to origin
       * at center of template). tileTexture indicates
       * that this texture is tilable and does not 
       * need to be scaled/offset */
      const scale = this.tileTexture ? 1 : distance * 2 / this._texture.width;
      const offset = this.tileTexture ? 0 : distance;
      this.template.beginTextureFill({
        texture: this._texture,
        matrix: new PIXI.Matrix().scale(scale, scale).translate(-offset, -offset)
      });
    } else {
      this.template.beginFill(this.fillColor, this.fillAlpha);
    }

    // Draw the shape
    this.template.drawShape(this.shape);

    // Draw origin and destination points
    //BEGIN WARPGATE
    //this.template.lineStyle(this._borderThickness, 0x000000, this.drawOutline ? 0.75 : 0)
    //  .beginFill(0x000000, 0.5)
    //.drawCircle(0, 0, 6)
    //.drawCircle(this.ray.dx, this.ray.dy, 6);
    //END WARPGATE

    // Update visibility
    if (this.drawIcon) {
      this.controlIcon.visible = true;
      this.controlIcon.border.visible = this._hover
      this.controlIcon.angle = document.direction;
    }

    // Draw ruler text
    //BEGIN WARPGATE
    this._setRulerText()
    //END WARPGATE
    return this;
  }

  /* END MEASUREDTEMPLATE.JS USAGE */


  /* -----------EXAMPLE CODE FROM ABILITY-TEMPLATE.JS--------- */
  /* Foundry VTT 5th Edition
   * Copyright (C) 2019  Foundry Network
   *
   * This program is free software: you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * Original License: 
   * https://gitlab.com/foundrynet/dnd5e/-/blob/master/LICENSE.txt
   */

  /**
   * Creates a preview of the spell template
   */
  async drawPreview() {
    // Draw the template and switch to the template layer
    this.initialLayer = canvas.activeLayer;
    this.layer.activate();
    this.draw();
    this.layer.preview.addChild(this);
    this.layer.interactiveChildren = false;

    // Hide the sheet that originated the preview
    //BEGIN WARPGATE
    this.inFlight = true;

    // Activate interactivity
    this.activatePreviewListeners();

    // Callbacks
    this.callbacks?.show?.(this);

    /* wait _indefinitely_ for placement to be decided. */
    await MODULE.waitFor(() => !this.inFlight, -1)
    if (this.activeHandlers) {
      this.clearHandlers();
    }

    //END WARPGATE
    return this;
  }

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

  _mouseMoveHandler(event) {
    event.stopPropagation();

    /* if our position is locked, do not update it */
    if (this.lockPosition) return;

    // Apply a 20ms throttle
    let now = Date.now();
    if (now - this.moveTime <= 20) return;

    const center = event.data.getLocalPosition(this.layer);
    const {x,y} = Crosshairs.getSnappedPosition(center, this.interval);
    this.document.updateSource({x, y});
    this.refresh();
    this.moveTime = now;
    canvas._onDragCanvasPan(event.data.originalEvent);
  }

  _leftClickHandler(event) {
    const document = this.document;
    const thisSceneSize = this.scene.grid.size;

    const destination = Crosshairs.getSnappedPosition(this.document, this.interval);
    this.radius = document.distance * thisSceneSize / 2;
    this.cancelled = false;

    this.document.updateSource({ ...destination });
    
    this.clearHandlers(event);
  }

  // Rotate the template by 3 degree increments (mouse-wheel)
  // none = rotate 5 degrees
  // shift = scale size
  // ctrl = rotate 30 or 15 degrees (square/hex)
  // alt = zoom canvas
  _mouseWheelHandler(event) {

    if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
    if (!event.altKey) event.stopPropagation();

    const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
    const snap = event.ctrlKey ? delta : 5;
    //BEGIN WARPGATE
    const document = this.document;
    const thisSceneSize = this.scene.grid.size;
    if (event.shiftKey && !this.lockSize) {
      let distance = document.distance + 0.25 * (Math.sign(event.deltaY));
      distance = Math.max(distance, 0.25);
      this.document.updateSource({ distance });
      this.radius = document.distance * thisSceneSize / 2;
    } else if (!event.altKey) {
      const direction = document.direction + (snap * Math.sign(event.deltaY));
      this.document.updateSource({ direction });
    }
    //END WARPGATE
    this.refresh();
  }

  _rightDownHandler(event) {
    if (event.button !== 2) return;

    this.rightX = event.screenX;
    this.rightY = event.screenY;
  }

  _rightUpHandler(event) {
    if (event.button !== 2) return;

    const isWithinThreshold = (current, previous) => Math.abs(current - previous) < 10;
    if (isWithinThreshold(this.rightX, event.screenX)
      && isWithinThreshold(this.rightY, event.screenY)
    ) {
      this.cancelled = true;
      this.clearHandlers(event);
    }
  }

  _clearHandlers(event) {
    //WARPGATE BEGIN
    /* remove only ourselves, in case of multiple */
    this.layer.preview.removeChild(this);
    
    canvas.stage.off("mousemove", this.activeMoveHandler);
    canvas.stage.off("mousedown", this.activeLeftClickHandler);
    canvas.app.view.onmousedown = null;
    canvas.app.view.onmouseup = null;
    canvas.app.view.onwheel = null;
		//WARPGATE END
    
		/* re-enable interactivity on this layer */
    this.layer.interactiveChildren = true;

    /* moving off this layer also deletes ALL active previews?
     * unexpected, but manageable
     */
    if (this.layer.preview.children.length == 0) {
      this.initialLayer.activate();
    }

    //BEGIN WARPGATE
    // Show the sheet that originated the preview
    if (this.actorSheet) this.actorSheet.maximize();
    this.activeHandlers = false;
    this.inFlight = false;

    /* mark this pixi element as destroyed */
    this._destroyed = true;
    //END WARPGATE
  }

  /**
   * Activate listeners for the template preview
   */
  activatePreviewListeners() {
    this.moveTime = 0;
    //BEGIN WARPGATE
    this.activeHandlers = true;

    /* Activate listeners */
    this.activeMoveHandler = this._mouseMoveHandler.bind(this);
    this.activeLeftClickHandler = this._leftClickHandler.bind(this);
    this.rightDownHandler = this._rightDownHandler.bind(this);
    this.rightUpHandler = this._rightUpHandler.bind(this);
    this.activeWheelHandler = this._mouseWheelHandler.bind(this);

    this.clearHandlers = this._clearHandlers.bind(this);

    // Update placement (mouse-move)
    canvas.stage.on("mousemove", this.activeMoveHandler);

    // Confirm the workflow (left-click)
    canvas.stage.on("mousedown", this.activeLeftClickHandler);

    // Mouse Wheel rotate
    canvas.app.view.onwheel = this.activeWheelHandler;

    // Right click cancel
    canvas.app.view.onmousedown = this.rightDownHandler;
    canvas.app.view.onmouseup = this.rightUpHandler;

    // END WARPGATE
  }

  /** END ABILITY-TEMPLATE.JS USAGE */
}
