/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2017 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Class that controls updates to connections during drags.
 * @author fenichel@google.com (Rachel Fenichel)
 */
'use strict';

goog.provide('Blockly.DraggedConnectionManager');

goog.require('Blockly.BlockAnimations');
goog.require('Blockly.RenderedConnection');

goog.require('goog.math.Coordinate');


/**
 * Class that controls updates to connections during drags.  It is primarily
 * responsible for finding the closest eligible connection and highlighting or
 * unhiglighting it as needed during a drag.
 * @param {!Blockly.BlockSvg} block The top block in the stack being dragged.
 * @constructor
 */
Blockly.DraggedConnectionManager = function(block) {
  Blockly.selected = block;

  /**
   * The top block in the stack being dragged.
   * Does not change during a drag.
   * @type {!Blockly.Block}
   * @private
   */
  this.topBlock_ = block;

  /**
   * The workspace on which these connections are being dragged.
   * Does not change during a drag.
   * @type {!Blockly.WorkspaceSvg}
   * @private
   */
  this.workspace_ = block.workspace;

  /**
   * The connections on the dragging blocks that are available to connect to
   * other blocks.  This includes all open connections on the top block, as well
   * as the last connection on the block stack.
   * Does not change during a drag.
   * @type {!Array.<!Blockly.RenderedConnection>}
   * @private
   */
  this.availableConnections_ = this.initAvailableConnections_();

  /**
   * The connection that this block would connect to if released immediately.
   * Updated on every mouse move.
   * @type {Blockly.RenderedConnection}
   * @private
   */
  this.closestConnection_ = null;

  /**
   * The connection that would connect to this.closestConnection_ if this block
   * were released immediately.
   * Updated on every mouse move.
   * @type {Blockly.RenderedConnection}
   * @private
   */
  this.localConnection_ = null;

  /**
   * The distance between this.closestConnection_ and this.localConnection_,
   * in workspace units.
   * Updated on every mouse move.
   * @type {number}
   * @private
   */
  this.radiusConnection_ = 0;

  /**
   * Whether the block would be deleted if it were dropped immediately.
   * Updated on every mouse move.
   * @type {boolean}
   * @private
   */
  this.wouldDeleteBlock_ = false;
};

/**
 * Sever all links from this object.
 * @package
 */
Blockly.DraggedConnectionManager.prototype.dispose = function() {
  this.topBlock_ = null;
  this.workspace_ = null;
  this.availableConnections_.length = 0;
  this.closestConnection_ = null;
  this.localConnection_ = null;
};

/**
 * Return whether the block would be deleted if dropped immediately, based on
 * information from the most recent move event.
 * @return {boolean} true if the block would be deleted if dropped immediately.
 * @package
 */
Blockly.DraggedConnectionManager.prototype.wouldDeleteBlock = function() {
  return this.wouldDeleteBlock_;
};

/**
 * Return whether the block would be connected if dropped immediately, based on
 * information from the most recent move event.
 * @return {boolean} true if the block would be connected if dropped immediately.
 * @package
 */
Blockly.DraggedConnectionManager.prototype.wouldConnectBlock = function() {
  return !!this.closestConnection_;
};

/**
 * Connect to the closest connection and render the results.
 * This should be called at the end of a drag.
 * @package
 */
Blockly.DraggedConnectionManager.prototype.applyConnections = function() {
  if (this.closestConnection_) {
    // Connect two blocks together.
    this.localConnection_.connect(this.closestConnection_);
    if (this.topBlock_.rendered) {
      // Trigger a connection animation.
      // Determine which connection is inferior (lower in the source stack).
      var inferiorConnection = this.localConnection_.isSuperior() ?
          this.closestConnection_ : this.localConnection_;
      Blockly.BlockAnimations.connectionUiEffect(
          inferiorConnection.getSourceBlock());
      // Bring the just-edited stack to the front.
      var rootBlock = this.topBlock_.getRootBlock();
      rootBlock.bringToFront();
    }
    this.removeHighlighting_();
  }
};

/**
 * Update highlighted connections based on the most recent move location.
 * @param {!goog.math.Coordinate} dxy Position relative to drag start,
 *     in workspace units.
 * @param {?number} deleteArea One of {@link Blockly.DELETE_AREA_TRASH},
 *     {@link Blockly.DELETE_AREA_TOOLBOX}, or {@link Blockly.DELETE_AREA_NONE}.
 * @param {?boolean} isOutside True if the drag is going outside the blocks workspace
 * @package
 */
Blockly.DraggedConnectionManager.prototype.update = function(dxy, deleteArea, isOutside) {
  var oldClosestConnection;
  var closestConnectionChanged;
  // If dragged outside, don't connect, since the connections aren't visible.
  if (!isOutside) {
    oldClosestConnection = this.closestConnection_;
    closestConnectionChanged = this.updateClosest_(dxy);
    if (closestConnectionChanged && oldClosestConnection) {
      oldClosestConnection.unhighlight();
    }
  } else if (this.closestConnection_) {
    this.closestConnection_.unhighlight();
    this.closestConnection_ = null;
  }

  // Prefer connecting over dropping into the trash can, but prefer dragging to
  // the toolbox over connecting to other blocks.
  var wouldConnect = !!this.closestConnection_ &&
      deleteArea != Blockly.DELETE_AREA_TOOLBOX;
  var wouldDelete = !!deleteArea && !this.topBlock_.getParent() &&
      this.topBlock_.isDeletable();
  this.wouldDeleteBlock_ = wouldDelete && !wouldConnect;

  if (!this.wouldDeleteBlock_ && closestConnectionChanged &&
      this.closestConnection_) {
    this.addHighlighting_();
  }
};

/**
 * Remove highlighting from the currently highlighted connection, if it exists.
 * @private
 */
Blockly.DraggedConnectionManager.prototype.removeHighlighting_ = function() {
  if (this.closestConnection_) {
    this.closestConnection_.unhighlight();
  }
};

/**
 * Add highlighting to the closest connection, if it exists.
 * @private
 */
Blockly.DraggedConnectionManager.prototype.addHighlighting_ = function() {
  if (this.closestConnection_) {
    this.closestConnection_.highlight();
  }
};

/**
 * Populate the list of available connections on this block stack.  This should
 * only be called once, at the beginning of a drag.
 * @return {!Array.<!Blockly.RenderedConnection>} a list of available
 *     connections.
 * @private
 */
Blockly.DraggedConnectionManager.prototype.initAvailableConnections_ = function() {
  var available = this.topBlock_.getConnections_(false);
  // Also check the last connection on this stack
  var lastOnStack = this.topBlock_.lastConnectionInStack();
  if (lastOnStack && lastOnStack != this.topBlock_.nextConnection) {
    available.push(lastOnStack);
  }
  return available;
};

/**
 * Find the new closest connection, and update internal state in response.
 * @param {!goog.math.Coordinate} dxy Position relative to the drag start,
 *     in workspace units.
 * @return {boolean} Whether the closest connection has changed.
 * @private
 */
Blockly.DraggedConnectionManager.prototype.updateClosest_ = function(dxy) {
  var oldClosestConnection = this.closestConnection_;

  this.closestConnection_ = null;
  this.localConnection_ = null;
  this.radiusConnection_ = Blockly.SNAP_RADIUS;
  for (var i = 0; i < this.availableConnections_.length; i++) {
    var myConnection = this.availableConnections_[i];
    var neighbour = myConnection.closest(this.radiusConnection_, dxy);
    if (neighbour.connection) {
      this.closestConnection_ = neighbour.connection;
      this.localConnection_ = myConnection;
      this.radiusConnection_ = neighbour.radius;
    }
  }
  return oldClosestConnection != this.closestConnection_;
};