/*
 *  Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved.
 */

/*
 * This is an extension and not part of the main GoJS library.
 * The source code for this is at extensionsJSM/SpotRotatingTool.ts.
 * Note that the API for this class may change with any version, even point releases.
 * If you intend to use an extension in production, you should copy the code to your own source directory.
 * Extensions can be found in the GoJS kit under the extensions or extensionsJSM folders.
 * See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
 */

import * as go from 'gojs';

/**
 * A custom RotatingTool that also supports the user moving the point about which the object is rotated.
 *
 * Typical usage:
 * ```js
 *   new go.Diagram(. . .,
 *     {
 *       rotatingTool: new SpotRotatingTool(),
 *       . . .
 *     })
 * ```
 *
 * 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 go.RotatingTool.rotationPoint} by changing the {@link go.Part.rotationSpot}.
 *
 * If you want to experiment with this extension, try the <a href="../../samples/SpotRotating.html">Spot Rotating</a> sample.
 * @category Tool Extension
 */
export class SpotRotatingTool extends go.RotatingTool {
  private _spotAdornmentTemplate: go.Adornment;
  private _originalRotationSpot: go.Spot;

  constructor(init?: Partial<SpotRotatingTool>) {
    super();
    this.name = 'SpotRotating';
    this._originalRotationSpot = go.Spot.Default;
    this._spotAdornmentTemplate = new go.Adornment('Spot',
        { locationSpot: go.Spot.Center, cursor: 'move' }).add(
      new go.Shape('Circle', { fill: 'lightblue', stroke: 'dodgerblue', width: 10, height: 10 }),
      new go.Shape('Circle', { fill: 'dodgerblue', strokeWidth: 0, width: 4, height: 4 })
    );
    if (init) Object.assign(this, init);
  }

  /**
   * In addition to updating the standard "Rotating" Adornment, this updates a "MovingSpot"
   * Adornment that the user may drag in order to move the {@link go.RotatingTool.rotationPoint}.
   * @param part
   */
  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 go.RotatingTool.handleAngle} is zero.
   * @param 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 go.RotatingTool.canStart},
   * also start when the user starts dragging the "MovingSpot" adornment/handle.
   * @returns boolean
   */
  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
   */
  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
   */
  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
   */
  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
   */
  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 go.Part.rotationSpot} and {@link go.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.
   * @virtual
   */
  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);
  }
}
