/*
 *  Copyright (C) 1998-2021 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.
 */
import * as go from './go';
/**
 * This CommandHandler class allows the user to position selected Parts in a diagram relative to the
 * first part selected, in addition to overriding the doKeyDown method of the CommandHandler for
 * handling the arrow keys in additional manners.
 *
 * Typical usage: ```js $(go.Diagram, "myDiagramDiv",
 *     {
 *     commandHandler: $(DrawCommandHandler),
 *     . . .
 *     } ) ``` or: ```js
 *     myDiagram.commandHandler = new DrawCommandHandler(); ```
 *
 * If you want to experiment with this extension, try the <a
 * href="../../extensionsTS/DrawCommandHandler.html">Drawing Commands</a> sample.
 */
export default class DrawCommandHandler extends go.CommandHandler {
  constructor() {
    super(...arguments);
    this._arrowKeyBehavior = 'move';
    this._pasteOffset = new go.Point(10, 10);
    this._lastPasteOffset = new go.Point(0, 0);
  }
  /**
   * Gets or sets the arrow key behavior. Possible values are "move", "select", and "scroll".
   *
   * The default value is "move".
   */
  get arrowKeyBehavior() {
    return this._arrowKeyBehavior;
  }
  set arrowKeyBehavior(val) {
    if (val !== 'move' && val !== 'select' && val !== 'scroll' && val !== 'none') {
      throw new Error(
        'DrawCommandHandler.arrowKeyBehavior must be either "move", "select", "scroll", or "none", not: ' +
          val,
      );
    }
    this._arrowKeyBehavior = val;
  }
  /**
   * Gets or sets the offset at which each repeated {@link #pasteSelection} puts the new copied
   * parts from the clipboard.
   */
  get pasteOffset() {
    return this._pasteOffset;
  }
  set pasteOffset(val) {
    if (!(val instanceof go.Point))
      throw new Error('DrawCommandHandler.pasteOffset must be a Point, not: ' + val);
    this._pasteOffset.set(val);
  }
  /**
   * This controls whether or not the user can invoke the {@link #alignLeft}, {@link #alignRight},
   * {@link #alignTop}, {@link #alignBottom}, {@link #alignCenterX}, {@link #alignCenterY} commands.
   *
   * @returns {boolean} This returns true:
   *     if the diagram is not {@link Diagram#isReadOnly},
   *     if the model is not {@link Model#isReadOnly}, and
   *     if there are at least two selected {@link Part}s.
   */
  canAlignSelection() {
    const diagram = this.diagram;
    if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
    if (diagram.selection.count < 2) return false;
    return true;
  }
  /** Aligns selected parts along the left-most edge of the left-most part. */
  alignLeft() {
    const diagram = this.diagram;
    diagram.startTransaction('aligning left');
    let minPosition = Infinity;
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      minPosition = Math.min(current.position.x, minPosition);
    });
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      current.move(new go.Point(minPosition, current.position.y));
    });
    diagram.commitTransaction('aligning left');
  }
  /** Aligns selected parts at the right-most edge of the right-most part. */
  alignRight() {
    const diagram = this.diagram;
    diagram.startTransaction('aligning right');
    let maxPosition = -Infinity;
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      const rightSideLoc = current.actualBounds.x + current.actualBounds.width;
      maxPosition = Math.max(rightSideLoc, maxPosition);
    });
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      current.move(new go.Point(maxPosition - current.actualBounds.width, current.position.y));
    });
    diagram.commitTransaction('aligning right');
  }
  /** Aligns selected parts at the top-most edge of the top-most part. */
  alignTop() {
    const diagram = this.diagram;
    diagram.startTransaction('alignTop');
    let minPosition = Infinity;
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      minPosition = Math.min(current.position.y, minPosition);
    });
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      current.move(new go.Point(current.position.x, minPosition));
    });
    diagram.commitTransaction('alignTop');
  }
  /** Aligns selected parts at the bottom-most edge of the bottom-most part. */
  alignBottom() {
    const diagram = this.diagram;
    diagram.startTransaction('aligning bottom');
    let maxPosition = -Infinity;
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      const bottomSideLoc = current.actualBounds.y + current.actualBounds.height;
      maxPosition = Math.max(bottomSideLoc, maxPosition);
    });
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      current.move(new go.Point(current.actualBounds.x, maxPosition - current.actualBounds.height));
    });
    diagram.commitTransaction('aligning bottom');
  }
  /** Aligns selected parts at the x-value of the center point of the first selected part. */
  alignCenterX() {
    const diagram = this.diagram;
    const firstSelection = diagram.selection.first();
    if (!firstSelection) return;
    diagram.startTransaction('aligning Center X');
    const centerX = firstSelection.actualBounds.x + firstSelection.actualBounds.width / 2;
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      current.move(new go.Point(centerX - current.actualBounds.width / 2, current.actualBounds.y));
    });
    diagram.commitTransaction('aligning Center X');
  }
  /** Aligns selected parts at the y-value of the center point of the first selected part. */
  alignCenterY() {
    const diagram = this.diagram;
    const firstSelection = diagram.selection.first();
    if (!firstSelection) return;
    diagram.startTransaction('aligning Center Y');
    const centerY = firstSelection.actualBounds.y + firstSelection.actualBounds.height / 2;
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      current.move(new go.Point(current.actualBounds.x, centerY - current.actualBounds.height / 2));
    });
    diagram.commitTransaction('aligning Center Y');
  }
  /**
   * Aligns selected parts top-to-bottom in order of the order selected. Distance between parts can
   * be specified. Default distance is 0.
   */
  alignColumn(distance) {
    const diagram = this.diagram;
    diagram.startTransaction('align Column');
    if (distance === undefined) distance = 0; // for aligning edge to edge
    distance = parseFloat(distance.toString());
    const selectedParts = new Array();
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      selectedParts.push(current);
    });
    for (let i = 0; i < selectedParts.length - 1; i++) {
      const current = selectedParts[i];
      // adds distance specified between parts
      const curBottomSideLoc = current.actualBounds.y + current.actualBounds.height + distance;
      const next = selectedParts[i + 1];
      next.move(new go.Point(current.actualBounds.x, curBottomSideLoc));
    }
    diagram.commitTransaction('align Column');
  }
  /**
   * Aligns selected parts left-to-right in order of the order selected. Distance between parts can
   * be specified. Default distance is 0.
   */
  alignRow(distance) {
    if (distance === undefined) distance = 0; // for aligning edge to edge
    distance = parseFloat(distance.toString());
    const diagram = this.diagram;
    diagram.startTransaction('align Row');
    const selectedParts = new Array();
    diagram.selection.each((current) => {
      if (current instanceof go.Link) return; // skips over go.Link
      selectedParts.push(current);
    });
    for (let i = 0; i < selectedParts.length - 1; i++) {
      const current = selectedParts[i];
      // adds distance specified between parts
      const curRightSideLoc = current.actualBounds.x + current.actualBounds.width + distance;
      const next = selectedParts[i + 1];
      next.move(new go.Point(curRightSideLoc, current.actualBounds.y));
    }
    diagram.commitTransaction('align Row');
  }
  /**
   * This controls whether or not the user can invoke the {@link #rotate} command.
   *
   * @returns {boolean} This returns true:
   *     if the diagram is not {@link Diagram#isReadOnly},
   *     if the model is not {@link Model#isReadOnly}, and
   *     if there is at least one selected {@link Part}.
   */
  canRotate() {
    const diagram = this.diagram;
    if (diagram.isReadOnly || diagram.isModelReadOnly) return false;
    if (diagram.selection.count < 1) return false;
    return true;
  }
  /**
   * Change the angle of the parts connected with the given part. This is in the command handler so
   * it can be easily accessed for the purpose of creating commands that change the rotation of a part.
   *
   * @param {number} angle The positive (clockwise) or negative (counter-clockwise) change in the
   *     rotation angle of each Part, in degrees.
   */
  rotate(angle) {
    if (angle === undefined) angle = 90;
    const diagram = this.diagram;
    diagram.startTransaction('rotate ' + angle.toString());
    diagram.selection.each((current) => {
      if (current instanceof go.Link || current instanceof go.Group) return; // skips over Links and Groups
      current.angle += angle;
    });
    diagram.commitTransaction('rotate ' + angle.toString());
  }
  /**
   * Change the z-ordering of selected parts to pull them forward, in front of all other parts in
   * their respective layers. All unselected parts in each layer with a selected Part with a
   * non-numeric {@link Part#zOrder} will get a zOrder of zero.
   *
   * @this {DrawCommandHandler}
   */
  pullToFront() {
    const diagram = this.diagram;
    diagram.startTransaction('pullToFront');
    // find the affected Layers
    const layers = new go.Map();
    diagram.selection.each(function (part) {
      if (part.layer !== null) layers.set(part.layer, 0);
    });
    // find the maximum zOrder in each Layer
    layers.iteratorKeys.each(function (layer) {
      let max = 0;
      layer.parts.each(function (part) {
        if (part.isSelected) return;
        const z = part.zOrder;
        if (isNaN(z)) {
          part.zOrder = 0;
        } else {
          max = Math.max(max, z);
        }
      });
      layers.set(layer, max);
    });
    // assign each selected Part.zOrder to the computed value for each Layer
    diagram.selection.each(function (part) {
      const z = layers.get(part.layer) || 0;
      DrawCommandHandler._assignZOrder(part, z + 1);
    });
    diagram.commitTransaction('pullToFront');
  }
  /**
   * Change the z-ordering of selected parts to push them backward, behind of all other parts in
   * their respective layers. All unselected parts in each layer with a selected Part with a
   * non-numeric {@link Part#zOrder} will get a zOrder of zero.
   *
   * @this {DrawCommandHandler}
   */
  pushToBack() {
    const diagram = this.diagram;
    diagram.startTransaction('pushToBack');
    // find the affected Layers
    const layers = new go.Map();
    diagram.selection.each(function (part) {
      if (part.layer !== null) layers.set(part.layer, 0);
    });
    // find the minimum zOrder in each Layer
    layers.iteratorKeys.each(function (layer) {
      let min = 0;
      layer.parts.each(function (part) {
        if (part.isSelected) return;
        const z = part.zOrder;
        if (isNaN(z)) {
          part.zOrder = 0;
        } else {
          min = Math.min(min, z);
        }
      });
      layers.set(layer, min);
    });
    // assign each selected Part.zOrder to the computed value for each Layer
    diagram.selection.each(function (part) {
      const z = layers.get(part.layer) || 0;
      DrawCommandHandler._assignZOrder(
        part,
        // make sure a group's nested nodes are also behind everything else
        z - 1 - DrawCommandHandler._findGroupDepth(part),
      );
    });
    diagram.commitTransaction('pushToBack');
  }
  static _assignZOrder(part, z, root) {
    if (root === undefined) root = part;
    if (part.layer === root.layer) part.zOrder = z;
    if (part instanceof go.Group) {
      part.memberParts.each(function (m) {
        DrawCommandHandler._assignZOrder(m, z + 1, root);
      });
    }
  }
  static _findGroupDepth(part) {
    if (part instanceof go.Group) {
      let d = 0;
      part.memberParts.each(function (m) {
        d = Math.max(d, DrawCommandHandler._findGroupDepth(m));
      });
      return d + 1;
    } else {
      return 0;
    }
  }
  /**
   * This implements custom behaviors for arrow key keyboard events. Set {@link #arrowKeyBehavior}
   * to "select", "move" (the default), "scroll" (the standard behavior), or "none" to affect the
   * behavior when the user types an arrow key.
   */
  doKeyDown() {
    const diagram = this.diagram;
    const e = diagram.lastInput;
    // determines the function of the arrow keys
    if (e.key === 'Up' || e.key === 'Down' || e.key === 'Left' || e.key === 'Right') {
      const behavior = this.arrowKeyBehavior;
      if (behavior === 'none') {
        // no-op
        return;
      } else if (behavior === 'select') {
        this._arrowKeySelect();
        return;
      } else if (behavior === 'move') {
        this._arrowKeyMove();
        return;
      }
      // otherwise drop through to get the default scrolling behavior
    }
    // otherwise still does all standard commands
    super.doKeyDown();
  }
  /** Collects in an Array all of the non-Link Parts currently in the Diagram. */
  _getAllParts() {
    const allParts = new Array();
    this.diagram.nodes.each((node) => {
      allParts.push(node);
    });
    this.diagram.parts.each((part) => {
      allParts.push(part);
    });
    // note that this ignores Links
    return allParts;
  }
  /** To be called when arrow keys should move the Diagram.selection. */
  _arrowKeyMove() {
    const diagram = this.diagram;
    const e = diagram.lastInput;
    // moves all selected parts in the specified direction
    let vdistance = 0;
    let hdistance = 0;
    // if control is being held down, move pixel by pixel. Else, moves by grid cell size
    if (e.control || e.meta) {
      vdistance = 1;
      hdistance = 1;
    } else if (diagram.grid !== null) {
      const cellsize = diagram.grid.gridCellSize;
      hdistance = cellsize.width;
      vdistance = cellsize.height;
    }
    diagram.startTransaction('arrowKeyMove');
    diagram.selection.each((part) => {
      if (e.key === 'Up') {
        part.move(new go.Point(part.actualBounds.x, part.actualBounds.y - vdistance));
      } else if (e.key === 'Down') {
        part.move(new go.Point(part.actualBounds.x, part.actualBounds.y + vdistance));
      } else if (e.key === 'Left') {
        part.move(new go.Point(part.actualBounds.x - hdistance, part.actualBounds.y));
      } else if (e.key === 'Right') {
        part.move(new go.Point(part.actualBounds.x + hdistance, part.actualBounds.y));
      }
    });
    diagram.commitTransaction('arrowKeyMove');
  }
  /** To be called when arrow keys should change selection. */
  _arrowKeySelect() {
    const diagram = this.diagram;
    const e = diagram.lastInput;
    // with a part selected, arrow keys change the selection
    // arrow keys + shift selects the additional part in the specified direction
    // arrow keys + control toggles the selection of the additional part
    let nextPart = null;
    if (e.key === 'Up') {
      nextPart = this._findNearestPartTowards(270);
    } else if (e.key === 'Down') {
      nextPart = this._findNearestPartTowards(90);
    } else if (e.key === 'Left') {
      nextPart = this._findNearestPartTowards(180);
    } else if (e.key === 'Right') {
      nextPart = this._findNearestPartTowards(0);
    }
    if (nextPart !== null) {
      if (e.shift) {
        nextPart.isSelected = true;
      } else if (e.control || e.meta) {
        nextPart.isSelected = !nextPart.isSelected;
      } else {
        diagram.select(nextPart);
      }
    }
  }
  /**
   * Finds the nearest Part in the specified direction, based on their center points. if it doesn't
   * find anything, it just returns the current Part.
   *
   * @param {number} dir The direction, in degrees
   * @returns {Part} The closest Part found in the given direction
   */
  _findNearestPartTowards(dir) {
    const originalPart = this.diagram.selection.first();
    if (originalPart === null) return null;
    const originalPoint = originalPart.actualBounds.center;
    const allParts = this._getAllParts();
    let closestDistance = Infinity;
    let closest = originalPart; // if no parts meet the criteria, the same part remains selected
    for (let i = 0; i < allParts.length; i++) {
      const nextPart = allParts[i];
      if (nextPart === originalPart) continue; // skips over currently selected part
      const nextPoint = nextPart.actualBounds.center;
      const angle = originalPoint.directionPoint(nextPoint);
      const anglediff = this._angleCloseness(angle, dir);
      if (anglediff <= 45) {
        // if this part's center is within the desired direction's sector,
        let distance = originalPoint.distanceSquaredPoint(nextPoint);
        distance *= 1 + Math.sin((anglediff * Math.PI) / 180); // the more different from the intended angle, the further it is
        if (distance < closestDistance) {
          // and if it's closer than any other part,
          closestDistance = distance; // remember it as a better choice
          closest = nextPart;
        }
      }
    }
    return closest;
  }
  _angleCloseness(a, dir) {
    return Math.min(Math.abs(dir - a), Math.min(Math.abs(dir + 360 - a), Math.abs(dir - 360 - a)));
  }
  /**
   * Reset the last offset for pasting.
   *
   * @param {Iterable<Part>} coll A collection of {@link Part}s.
   */
  copyToClipboard(coll) {
    super.copyToClipboard(coll);
    this._lastPasteOffset.set(this.pasteOffset);
  }
  /**
   * Paste from the clipboard with an offset incremented on each paste, and reset when copied.
   *
   * @returns {Set<Part>} A collection of newly pasted {@link Part}s
   */
  pasteFromClipboard() {
    const coll = super.pasteFromClipboard();
    this.diagram.moveParts(coll, this._lastPasteOffset, false);
    this._lastPasteOffset.add(this.pasteOffset);
    return coll;
  }
}
