"use strict";
/*
*  Copyright (C) 1998-2020 by Northwoods Software Corporation. All Rights Reserved.
*/

/*
* This is an extension and not part of the main GoJS library.
* 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 extensionsTS folders.
* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
*/

/**
* @constructor
* @extends RotatingTool
* @class
* A custom tool for rotating 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.
* <p>
* Caution: this only works for Groups that do *not* have a Placeholder.
*/
function RotateMultipleTool() {
  go.RotatingTool.call(this);
  this.name = "RotateMultiple";
  // holds references to all selected non-Link Parts and their offset & angles
  this._initialInfo = null;
  // initial angle when rotating as a whole
  this._initialAngle = 0;
  // rotation point of selection
  this._centerPoint = null;
 }
go.Diagram.inherit(RotateMultipleTool, go.RotatingTool);

/**
* Calls RotatingTool.doActivate, and then remembers the center point of the collection,
* and the initial distances and angles of selected parts to the center.
* @this {RotateMultipleTool}
*/
RotateMultipleTool.prototype.doActivate = function() {
  go.RotatingTool.prototype.doActivate.call(this);
  var 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
  var infos = new go.Map(/*go.Part, PartInfo*/);
  var tool = this;
  diagram.selection.each(function(part) {
    tool.walkTree(part, infos);
  });
  this._initialInfo = infos;
}

/**
* @ignore
* @param {Part} part
* @param {Map} infos
*/
RotateMultipleTool.prototype.walkTree = function(part, infos) {
  if (part === null || part instanceof go.Link) return;
  // distance from _centerPoint to locationSpot of part
  var dist = Math.sqrt(this._centerPoint.distanceSquaredPoint(part.location));
  // calculate initial relative angle
  var 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) {
    var it = part.memberParts.iterator;
    while (it.next()) this.walkTree(it.value, infos);
  }
};

/**
* @ignore
* Internal class that remembers a Part's offset & angle.
*/
function PartInfo(placementAngle, distance, rotationAngle) {
  this.placementAngle = placementAngle * (Math.PI / 180);  // in radians
  this.distance = distance;
  this.rotationAngle = rotationAngle;  // in degrees
}

/**
* Clean up any references to Parts.
* @this {RotateMultipleTool}
*/
RotateMultipleTool.prototype.doDeactivate = function() {
  this._initialInfo = null;
  go.RotatingTool.prototype.doDeactivate.call(this);
};

/**
* Overrides rotatingTool.rotate to rotate all selected objects about their collective center.
* When the control key is held down while rotating, all selected objects are rotated individually.
* @this {RotateMultipleTool}
* @param {number} newangle
*/
RotateMultipleTool.prototype.rotate = function(newangle) {
  var diagram = this.diagram;
  var e = diagram.lastInput;
  // when rotating individual parts, remember the original angle difference
  var angleDiff = newangle - this.adornedObject.part.rotateObject.angle;
  var tool = this;
  this._initialInfo.each(function(kvp) {
    var part = kvp.key;
    if (part instanceof go.Link) return; // only Nodes and simple Parts
    var partInfo = kvp.value;
    // rotate every selected non-Link Part
    // find information about the part set in RotateMultipleTool.initialInformation
    if (e.control || e.meta) {
      if (tool.adornedObject.part === part) {
        part.rotateObject.angle = newangle;
      } else {
        part.rotateObject.angle += angleDiff;
      }
    } else {
      var 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
      var offsetX = partInfo.distance * Math.cos(radAngle + partInfo.placementAngle);
      var 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;
    }
  });
}

/**
* This override needs to calculate the desired angle with different rotation points,
* depending on whether we are rotating the whole selection as one, or Parts individually.
* @this {RotateMultipleTool}
* @param {Point} newPoint in document coordinates
*/
RotateMultipleTool.prototype.computeRotate = function(newPoint) {
  var diagram = this.diagram;
  var angle;
  var e = diagram.lastInput;
  if (e.control || e.meta) {  // relative to the center of the Node whose handle we are rotating
    var part = this.adornedObject.part;
    var 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;
  var interval = Math.min(Math.abs(this.snapAngleMultiple), 180);
  var 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;
};
