﻿/*
*  Copyright (C) 1998-2023 by Northwoods Software Corporation. All Rights Reserved.
*/

import * as go from '../release/go.js';

/**
 * The RotateMultipleTool class lets the user rotate multiple objects at a time.
 * When more than one part is selected, rotates all parts, revolving them about their collective center.
 * If the control key is held down during rotation, rotates all parts individually.
 *
 * Caution: this only works for Groups that do *not* have a Placeholder.
 *
 * If you want to experiment with this extension, try the <a href="../../extensionsJSM/RotateMultiple.html">Rotate Multiple</a> sample.
 * @category Tool Extension
 */
export class RotateMultipleTool extends go.RotatingTool {
  /**
   * Holds references to all selected non-Link Parts and their offset & angles
   */
  private _initialInfo: go.Map<go.Part, PartInfo> | null = null;
  /**
   * Initial angle when rotating as a whole
   */
  private _initialAngle: number = 0;
  /**
   * Rotation point of selection
   */
  private _centerPoint: go.Point = new go.Point();

  /**
   * Constructs a RotateMultipleTool and sets the name for the tool.
   */
  constructor() {
    super();
    this.name = 'RotateMultiple';
  }

  /**
   * Calls {@link RotatingTool#doActivate}, and then remembers the center point of the collection,
   * and the initial distances and angles of selected parts to the center.
   */
  public override doActivate(): void {
    super.doActivate();
    const diagram = this.diagram;
    // center point of the collection
    this._centerPoint = diagram.computePartsBounds(diagram.selection).center;

    // remember the angle relative to the center point when rotating the whole collection
    this._initialAngle = this._centerPoint.directionPoint(diagram.lastInput.documentPoint);

    // remember initial angle and distance for each Part
    const infos = new go.Map<go.Part, PartInfo>();
    const tool = this;
    diagram.selection.each(function(part) {
      tool.walkTree(part, infos);
    });
    this._initialInfo = infos;

    // forget the rotationPoint since we use _centerPoint instead
    this.rotationPoint = new go.Point(NaN, NaN);
  }

  /**
   * @hidden @internal
   */
  private walkTree(part: go.Part, infos: go.Map<go.Part, PartInfo>): void {
    if (part === null || part instanceof go.Link) return;
    // distance from _centerPoint to locationSpot of part
    const dist = Math.sqrt(this._centerPoint.distanceSquaredPoint(part.location));
    // calculate initial relative angle
    const dir = this._centerPoint.directionPoint(part.location);
    // saves part-angle combination in array
    infos.add(part, new PartInfo(dir, dist, part.rotateObject.angle));
    // recurse into Groups
    if (part instanceof go.Group) {
      const it = part.memberParts.iterator;
      while (it.next()) this.walkTree(it.value, infos);
    }
  }

  /**
   * Clean up any references to Parts.
   */
  public override doDeactivate(): void {
    this._initialInfo = null;
    super.doDeactivate();
  }

  /**
   * Rotate all selected objects about their collective center.
   * When the control key is held down while rotating, all selected objects are rotated individually.
   */
  public override rotate(newangle: number): void {
    const diagram = this.diagram;
    if (this._initialInfo === null) return;
    const node = this.adornedObject !== null ? this.adornedObject.part : null;
    if (node === null) return;
    const e = diagram.lastInput;
    // when rotating individual parts, remember the original angle difference
    const angleDiff = newangle - node.rotateObject.angle;
    const tool = this;
    this._initialInfo.each(function(kvp) {
      const part = kvp.key;
      if (part instanceof go.Link) return; // only Nodes and simple Parts
      const partInfo = kvp.value;
      // rotate every selected non-Link Part
      // find information about the part set in RotateMultipleTool._initialInformation
      if (e.control || e.meta) {
        if (node === part) {
          part.rotateObject.angle = newangle;
        } else {
          part.rotateObject.angle += angleDiff;
        }
      } else {
        const radAngle = newangle * (Math.PI / 180); // converts the angle traveled from degrees to radians
        // calculate the part's x-y location relative to the central rotation point
        const offsetX = partInfo.distance * Math.cos(radAngle + partInfo.placementAngle);
        const offsetY = partInfo.distance * Math.sin(radAngle + partInfo.placementAngle);
        // move part
        part.location = new go.Point(tool._centerPoint.x + offsetX, tool._centerPoint.y + offsetY);
        // rotate part
        part.rotateObject.angle = partInfo.rotationAngle + newangle;
      }
    });
  }

  /**
   * Calculate the desired angle with different rotation points,
   * depending on whether we are rotating the whole selection as one, or Parts individually.
   * @param {Point} newPoint in document coordinates
   */
  public override computeRotate(newPoint: go.Point): number {
    const diagram = this.diagram;
    if (this.adornedObject === null) return 0.0;
    let angle = 0.0;
    const e = diagram.lastInput;
    if (e.control || e.meta) {  // relative to the center of the Node whose handle we are rotating
      const part = this.adornedObject.part;
      if (part !== null) {
        const rotationPoint = part.getDocumentPoint(part.locationSpot);
        angle = rotationPoint.directionPoint(newPoint);
      }
    } else {  // relative to the center of the whole selection
      angle = this._centerPoint.directionPoint(newPoint) - this._initialAngle;
    }
    if (angle >= 360) angle -= 360;
    else if (angle < 0) angle += 360;
    const interval = Math.min(Math.abs(this.snapAngleMultiple), 180);
    const epsilon = Math.min(Math.abs(this.snapAngleEpsilon), interval / 2);
    // if it's close to a multiple of INTERVAL degrees, make it exactly so
    if (!diagram.lastInput.shift && interval > 0 && epsilon > 0) {
      if (angle % interval < epsilon) {
        angle = Math.floor(angle / interval) * interval;
      } else if (angle % interval > interval - epsilon) {
        angle = (Math.floor(angle / interval) + 1) * interval;
      }
      if (angle >= 360) angle -= 360;
      else if (angle < 0) angle += 360;
    }
    return angle;
  }

}

/**
 * Internal class to remember a Part's offset and angle.
 */
class PartInfo {
  public placementAngle: number;
  public distance: number;
  public rotationAngle: number;
  constructor(placementAngle: number, distance: number, rotationAngle: number) {
    this.placementAngle = placementAngle * (Math.PI / 180);  // in radians
    this.distance = distance;
    this.rotationAngle = rotationAngle;  // in degrees
  }
}
