/*
*  Copyright (C) 1998-2023 by Northwoods Software Corporation. All Rights Reserved.
*/

import * as go from '../release/go.js';

/**
* A custom RotatingTool that also supports the user moving the point about which the object is rotated.
*
* This tool uses two separate Adornments -- the regular one holding the rotation handle and an
* additional one named "MovingSpot" that holds the handle for interactively moving the
* {@link RotatingTool#rotationPoint} by changing the {@link Part#rotationSpot}.
* @category Tool Extension
*/
export class SpotRotatingTool extends go.RotatingTool {
  private _spotAdornmentTemplate: go.Adornment;
  private _originalRotationSpot: go.Spot = go.Spot.Default;

  public constructor() {
    super();
    const $ = go.GraphObject.make;
    this._spotAdornmentTemplate =
      $(go.Adornment, "Spot",
        { locationSpot: go.Spot.Center, cursor: "move" },
        $(go.Shape, "Circle",
          { fill: "lightblue", stroke: "dodgerblue", width: 10, height: 10 }),
        $(go.Shape, "Circle",
          { fill: "dodgerblue", strokeWidth: 0, width: 4, height: 4 })
      );
  }

  /**
   * In addition to updating the standard "Rotating" Adornment, this updates a "MovingSpot"
   * Adornment that the user may drag in order to move the {@link RotatingTool#rotationPoint}.
   * @param {Part} part
   */
  public override updateAdornments(part: go.Part): void {
    super.updateAdornments(part);
    if (part === null) return;
    if (part.isSelected && !this.diagram.isReadOnly) {
      const rotateObj = part.rotateObject;
      if (rotateObj !== null && part.canRotate() && part.actualBounds.isReal() &&
          part.isVisible() && rotateObj.actualBounds.isReal() && rotateObj.isVisibleObject()) {
        let ad = part.findAdornment("RotateSpot");
        if (ad === null || ad.adornedObject !== rotateObj) {
          ad = this._spotAdornmentTemplate.copy();
          ad.adornedObject = part.rotateObject;
        }
        if (ad !== null) {
          ad.location = this.computeRotationPoint(ad.adornedObject);
          part.addAdornment("RotateSpot", ad);
          return;
        }
      }
    }
    part.removeAdornment("RotateSpot");
  };

  /**
   * Change the positioning of the "Rotating" Adornment to adapt to the rotation point
   * potentially being well outside of the object being rotated.
   *
   * This assumes that {@link RotatingTool#handleAngle} is zero.
   * @param {GraphObject} obj the object being rotated
   * @returns Point in document coordinates
   */
  computeAdornmentLocation(obj: go.GraphObject): go.Point {
    let p = this.rotationPoint;
    if (!p.isReal()) p = this.computeRotationPoint(obj);
    const q = obj.getLocalPoint(p);
    //??? ignores this.handleAngle
    q.x = Math.max(obj.naturalBounds.right, q.x) + this.handleDistance;
    return obj.getDocumentPoint(q);
  }

  /**
   * In addition to the standard behavior of {@link RotatingTool#canStart},
   * also start when the user starts dragging the "MovingSpot" adornment/handle.
   * @returns boolean
   */
  public override canStart(): boolean {
    if (!this.isEnabled) return false;
    const diagram = this.diagram;
    if (diagram.isReadOnly) return false;
    if (!diagram.allowRotate) return false;
    if (!diagram.lastInput.left) return false;

    let h = this.findToolHandleAt(diagram.firstInput.documentPoint, this.name);
    if (h !== null) return true;

    h = this.findToolHandleAt(diagram.firstInput.documentPoint, "RotateSpot");
    return (h !== null);
  }

  /**
   * @hidden @internal
   */
  public override doActivate(): void {
    // might be dragging the spot handle instead of the rotate handle
    this.handle = this.findToolHandleAt(this.diagram.firstInput.documentPoint, "RotateSpot");
    if (this.handle !== null) {
      const ad = this.handle.part as go.Adornment;
      if (ad.adornedObject !== null) {
        const part = ad.adornedPart;
        if (part !== null) this._originalRotationSpot = part.rotationSpot;
      }
    }
    // doActivate uses this.handle if it is set beforehand, rather than searching for a rotate handle
    super.doActivate();
  }

  /**
   * @hidden @internal
   */
  public override doCancel(): void {
    if (this.adornedObject !== null) {
      const part = this.adornedObject.part;
      if (part !== null) {
        part.rotationSpot = this._originalRotationSpot;
        this.rotationPoint.set(this.computeRotationPoint(this.adornedObject));
        this.updateAdornments(part);
      }
    }
    super.doCancel();
  }

  /**
   * @hidden @internal
   */
  public override doMouseMove(): void {
    if (this.isActive) {
      if (this.handle !== null && this.handle.part && this.handle.part.category === "RotateSpot") {
        // modify part.rotationSpot and this.rotationPoint
        this.shiftRotationPoint();
      } else {
        super.doMouseMove();
      }
    }
  }

  /**
   * @hidden @internal
   */
  public override doMouseUp(): void {
    if (this.isActive) {
      if (this.handle !== null && this.handle.part && this.handle.part.category === "RotateSpot") {
        // modify part.rotationSpot and this.rotationPoint
        this.shiftRotationPoint();
        this.transactionResult = "Shifted rotationSpot";
        this.stopTool();
      } else {
        super.doMouseUp();
      }
    }
  }

  /**
   * This is called by mouse moves and mouse up events when the handle being dragged is "MovingSpot".
   * This needs to update the {@link Part#rotationSpot} and {@link RotatingTool#rotationPoint} properties.
   *
   * For each of the X and Y directions, when the handle is within the bounds of the rotated object,
   * the new rotation Spot will be purely fractional; when it is outside the Spot will be limited to
   * a fraction of zero or one (whichever is closer) and an absolute offset that places the rotation point
   * where the handle is.
   * @expose
   */
  shiftRotationPoint(): void {
    const dp = this.diagram.lastInput.documentPoint;
    const obj = this.adornedObject;
    if (obj === null) return;
    const w = obj.naturalBounds.width || 1;  // disallow zero
    const h = obj.naturalBounds.height || 1;
    const part = obj.part;
    if (part === null) return;
    const op = obj.getLocalPoint(dp);
    const fx = (op.x < 0) ? 0 : (op.x > w ? 1 : op.x/w);
    const fy = (op.y < 0) ? 0 : (op.y > h ? 1 : op.y/h);
    const ox = (op.x < 0) ? op.x : (op.x > w ? op.x-w : 0);
    const oy = (op.y < 0) ? op.y : (op.y > h ? op.y-h : 0);
    part.rotationSpot = new go.Spot(fx, fy, ox, oy);
    this.rotationPoint.set(this.computeRotationPoint(obj));
    this.updateAdornments(part);
  }
}
