// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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 Represents a path used with a Graphics implementation.
 * @author arv@google.com (Erik Arvidsson)
 */

goog.provide('goog.math.Path');
goog.provide('goog.math.Path.Segment');

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



/**
 * Creates a path object. A path is a sequence of segments and may be open or
 * closed. Path uses the EVEN-ODD fill rule for determining the interior of the
 * path. A path must start with a moveTo command.
 *
 * A "simple" path does not contain any arcs and may be transformed using
 * the {@code transform} method.
 *
 * @struct
 * @constructor
 * @final
 */
goog.math.Path = function() {
  /**
   * The segment types that constitute this path.
   * @private {!Array<goog.math.Path.Segment>}
   */
  this.segments_ = [];

  /**
   * The number of repeated segments of the current type.
   * @type {!Array<number>}
   * @private
   */
  this.count_ = [];

  /**
   * The arguments corresponding to each of the segments.
   * @type {!Array<number>}
   * @private
   */
  this.arguments_ = [];

  /**
   * The coordinates of the point which closes the path (the point of the
   * last moveTo command).
   * @type {Array<number>?}
   * @private
   */
  this.closePoint_ = null;

  /**
   * The coordinates most recently added to the end of the path.
   * @type {Array<number>?}
   * @private
   */
  this.currentPoint_ = null;

  /**
   * Flag for whether this is a simple path (contains no arc segments).
   * @type {boolean}
   * @private
   */
  this.simple_ = true;
};


/**
 * Path segment types.
 * @enum {number}
 */
goog.math.Path.Segment = {
  MOVETO: 0,
  LINETO: 1,
  CURVETO: 2,
  ARCTO: 3,
  CLOSE: 4
};


/**
 * The number of points for each segment type.
 * @type {!Array<number>}
 * @private
 */
goog.math.Path.segmentArgCounts_ = (function() {
  var counts = [];
  counts[goog.math.Path.Segment.MOVETO] = 2;
  counts[goog.math.Path.Segment.LINETO] = 2;
  counts[goog.math.Path.Segment.CURVETO] = 6;
  counts[goog.math.Path.Segment.ARCTO] = 6;
  counts[goog.math.Path.Segment.CLOSE] = 0;
  return counts;
})();


/**
 * Returns an array of the segment types in this path, in the order of their
 * appearance. Adjacent segments of the same type are collapsed into a single
 * entry in the array. The returned array is a copy; modifications are not
 * reflected in the Path object.
 * @return {!Array<number>}
 */
goog.math.Path.prototype.getSegmentTypes = function() {
  return this.segments_.concat();
};


/**
 * Returns an array of the number of times each segment type repeats in this
 * path, in order. The returned array is a copy; modifications are not reflected
 * in the Path object.
 * @return {!Array<number>}
 */
goog.math.Path.prototype.getSegmentCounts = function() {
  return this.count_.concat();
};


/**
 * Returns an array of all arguments for the segments of this path object, in
 * order. The returned array is a copy; modifications are not reflected in the
 * Path object.
 * @return {!Array<number>}
 */
goog.math.Path.prototype.getSegmentArgs = function() {
  return this.arguments_.concat();
};


/**
 * Returns the number of points for a segment type.
 *
 * @param {number} segment The segment type.
 * @return {number} The number of points.
 */
goog.math.Path.getSegmentCount = function(segment) {
  return goog.math.Path.segmentArgCounts_[segment];
};


/**
 * Appends another path to the end of this path.
 *
 * @param {!goog.math.Path} path The path to append.
 * @return {!goog.math.Path} This path.
 */
goog.math.Path.prototype.appendPath = function(path) {
  if (path.currentPoint_) {
    Array.prototype.push.apply(this.segments_, path.segments_);
    Array.prototype.push.apply(this.count_, path.count_);
    Array.prototype.push.apply(this.arguments_, path.arguments_);
    this.currentPoint_ = path.currentPoint_.concat();
    this.closePoint_ = path.closePoint_.concat();
    this.simple_ = this.simple_ && path.simple_;
  }
  return this;
};


/**
 * Clears the path.
 *
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.clear = function() {
  this.segments_.length = 0;
  this.count_.length = 0;
  this.arguments_.length = 0;
  this.closePoint_ = null;
  this.currentPoint_ = null;
  this.simple_ = true;
  return this;
};


/**
 * Adds a point to the path by moving to the specified point. Repeated moveTo
 * commands are collapsed into a single moveTo.
 *
 * @param {number} x X coordinate of destination point.
 * @param {number} y Y coordinate of destination point.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.moveTo = function(x, y) {
  if (goog.array.peek(this.segments_) == goog.math.Path.Segment.MOVETO) {
    this.arguments_.length -= 2;
  } else {
    this.segments_.push(goog.math.Path.Segment.MOVETO);
    this.count_.push(1);
  }
  this.arguments_.push(x, y);
  this.currentPoint_ = this.closePoint_ = [x, y];
  return this;
};


/**
 * Adds points to the path by drawing a straight line to each point.
 *
 * @param {...number} var_args The coordinates of each destination point as x, y
 *     value pairs.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.lineTo = function(var_args) {
  return this.lineTo_(arguments);
};


/**
 * Adds points to the path by drawing a straight line to each point.
 *
 * @param {!Array<number>} coordinates The coordinates of each
 *     destination point as x, y value pairs.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.lineToFromArray = function(coordinates) {
  return this.lineTo_(coordinates);
};


/**
 * Adds points to the path by drawing a straight line to each point.
 *
 * @param {!Array<number>|Arguments} coordinates The coordinates of each
 *     destination point as x, y value pairs.
 * @return {!goog.math.Path} The path itself.
 * @private
 */
goog.math.Path.prototype.lineTo_ = function(coordinates) {
  var lastSegment = goog.array.peek(this.segments_);
  if (lastSegment == null) {
    throw Error('Path cannot start with lineTo');
  }
  if (lastSegment != goog.math.Path.Segment.LINETO) {
    this.segments_.push(goog.math.Path.Segment.LINETO);
    this.count_.push(0);
  }
  for (var i = 0; i < coordinates.length; i += 2) {
    var x = coordinates[i];
    var y = coordinates[i + 1];
    this.arguments_.push(x, y);
  }
  this.count_[this.count_.length - 1] += i / 2;
  this.currentPoint_ = [x, y];
  return this;
};


/**
 * Adds points to the path by drawing cubic Bezier curves. Each curve is
 * specified using 3 points (6 coordinates) - two control points and the end
 * point of the curve.
 *
 * @param {...number} var_args The coordinates specifiying each curve in sets of
 *     6 points: {@code [x1, y1]} the first control point, {@code [x2, y2]} the
 *     second control point and {@code [x, y]} the end point.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.curveTo = function(var_args) {
  return this.curveTo_(arguments);
};


/**
 * Adds points to the path by drawing cubic Bezier curves. Each curve is
 * specified using 3 points (6 coordinates) - two control points and the end
 * point of the curve.
 *
 * @param {!Array<number>} coordinates The coordinates specifiying
 *     each curve in sets of 6 points: {@code [x1, y1]} the first control point,
 *     {@code [x2, y2]} the second control point and {@code [x, y]} the end
 *     point.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.curveToFromArray = function(coordinates) {
  return this.curveTo_(coordinates);
};


/**
 * Adds points to the path by drawing cubic Bezier curves. Each curve is
 * specified using 3 points (6 coordinates) - two control points and the end
 * point of the curve.
 *
 * @param {!Array<number>|Arguments} coordinates The coordinates specifiying
 *     each curve in sets of 6 points: {@code [x1, y1]} the first control point,
 *     {@code [x2, y2]} the second control point and {@code [x, y]} the end
 *     point.
 * @return {!goog.math.Path} The path itself.
 * @private
 */
goog.math.Path.prototype.curveTo_ = function(coordinates) {
  var lastSegment = goog.array.peek(this.segments_);
  if (lastSegment == null) {
    throw Error('Path cannot start with curve');
  }
  if (lastSegment != goog.math.Path.Segment.CURVETO) {
    this.segments_.push(goog.math.Path.Segment.CURVETO);
    this.count_.push(0);
  }
  for (var i = 0; i < coordinates.length; i += 6) {
    var x = coordinates[i + 4];
    var y = coordinates[i + 5];
    this.arguments_.push(coordinates[i], coordinates[i + 1],
        coordinates[i + 2], coordinates[i + 3], x, y);
  }
  this.count_[this.count_.length - 1] += i / 6;
  this.currentPoint_ = [x, y];
  return this;
};


/**
 * Adds a path command to close the path by connecting the
 * last point to the first point.
 *
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.close = function() {
  var lastSegment = goog.array.peek(this.segments_);
  if (lastSegment == null) {
    throw Error('Path cannot start with close');
  }
  if (lastSegment != goog.math.Path.Segment.CLOSE) {
    this.segments_.push(goog.math.Path.Segment.CLOSE);
    this.count_.push(1);
    this.currentPoint_ = this.closePoint_;
  }
  return this;
};


/**
 * Adds a path command to draw an arc centered at the point {@code (cx, cy)}
 * with radius {@code rx} along the x-axis and {@code ry} along the y-axis from
 * {@code startAngle} through {@code extent} degrees. Positive rotation is in
 * the direction from positive x-axis to positive y-axis.
 *
 * @param {number} cx X coordinate of center of ellipse.
 * @param {number} cy Y coordinate of center of ellipse.
 * @param {number} rx Radius of ellipse on x axis.
 * @param {number} ry Radius of ellipse on y axis.
 * @param {number} fromAngle Starting angle measured in degrees from the
 *     positive x-axis.
 * @param {number} extent The span of the arc in degrees.
 * @param {boolean} connect If true, the starting point of the arc is connected
 *     to the current point.
 * @return {!goog.math.Path} The path itself.
 * @deprecated Use {@code arcTo} or {@code arcToAsCurves} instead.
 */
goog.math.Path.prototype.arc = function(cx, cy, rx, ry,
    fromAngle, extent, connect) {
  var startX = cx + goog.math.angleDx(fromAngle, rx);
  var startY = cy + goog.math.angleDy(fromAngle, ry);
  if (connect) {
    if (!this.currentPoint_ || startX != this.currentPoint_[0] ||
        startY != this.currentPoint_[1]) {
      this.lineTo(startX, startY);
    }
  } else {
    this.moveTo(startX, startY);
  }
  return this.arcTo(rx, ry, fromAngle, extent);
};


/**
 * Adds a path command to draw an arc starting at the path's current point,
 * with radius {@code rx} along the x-axis and {@code ry} along the y-axis from
 * {@code startAngle} through {@code extent} degrees. Positive rotation is in
 * the direction from positive x-axis to positive y-axis.
 *
 * This method makes the path non-simple.
 *
 * @param {number} rx Radius of ellipse on x axis.
 * @param {number} ry Radius of ellipse on y axis.
 * @param {number} fromAngle Starting angle measured in degrees from the
 *     positive x-axis.
 * @param {number} extent The span of the arc in degrees.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.arcTo = function(rx, ry, fromAngle, extent) {
  var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);
  var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);
  var ex = cx + goog.math.angleDx(fromAngle + extent, rx);
  var ey = cy + goog.math.angleDy(fromAngle + extent, ry);
  this.segments_.push(goog.math.Path.Segment.ARCTO);
  this.count_.push(1);
  this.arguments_.push(rx, ry, fromAngle, extent, ex, ey);
  this.simple_ = false;
  this.currentPoint_ = [ex, ey];
  return this;
};


/**
 * Same as {@code arcTo}, but approximates the arc using bezier curves.
.* As a result, this method does not affect the simplified status of this path.
 * The algorithm is adapted from {@code java.awt.geom.ArcIterator}.
 *
 * @param {number} rx Radius of ellipse on x axis.
 * @param {number} ry Radius of ellipse on y axis.
 * @param {number} fromAngle Starting angle measured in degrees from the
 *     positive x-axis.
 * @param {number} extent The span of the arc in degrees.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.arcToAsCurves = function(
    rx, ry, fromAngle, extent) {
  var cx = this.currentPoint_[0] - goog.math.angleDx(fromAngle, rx);
  var cy = this.currentPoint_[1] - goog.math.angleDy(fromAngle, ry);
  var extentRad = goog.math.toRadians(extent);
  var arcSegs = Math.ceil(Math.abs(extentRad) / Math.PI * 2);
  var inc = extentRad / arcSegs;
  var angle = goog.math.toRadians(fromAngle);
  for (var j = 0; j < arcSegs; j++) {
    var relX = Math.cos(angle);
    var relY = Math.sin(angle);
    var z = 4 / 3 * Math.sin(inc / 2) / (1 + Math.cos(inc / 2));
    var c0 = cx + (relX - z * relY) * rx;
    var c1 = cy + (relY + z * relX) * ry;
    angle += inc;
    relX = Math.cos(angle);
    relY = Math.sin(angle);
    this.curveTo(c0, c1,
        cx + (relX + z * relY) * rx,
        cy + (relY - z * relX) * ry,
        cx + relX * rx,
        cy + relY * ry);
  }
  return this;
};


/**
 * Iterates over the path calling the supplied callback once for each path
 * segment. The arguments to the callback function are the segment type and
 * an array of its arguments.
 *
 * The {@code LINETO} and {@code CURVETO} arrays can contain multiple
 * segments of the same type. The number of segments is the length of the
 * array divided by the segment length (2 for lines, 6 for  curves).
 *
 * As a convenience the {@code ARCTO} segment also includes the end point as the
 * last two arguments: {@code rx, ry, fromAngle, extent, x, y}.
 *
 * @param {function(!goog.math.Path.Segment, !Array<number>)} callback
 *     The function to call with each path segment.
 */
goog.math.Path.prototype.forEachSegment = function(callback) {
  var points = this.arguments_;
  var index = 0;
  for (var i = 0, length = this.segments_.length; i < length; i++) {
    var seg = this.segments_[i];
    var n = goog.math.Path.segmentArgCounts_[seg] * this.count_[i];
    callback(seg, points.slice(index, index + n));
    index += n;
  }
};


/**
 * Returns the coordinates most recently added to the end of the path.
 *
 * @return {Array<number>?} An array containing the ending coordinates of the
 *     path of the form {@code [x, y]}.
 */
goog.math.Path.prototype.getCurrentPoint = function() {
  return this.currentPoint_ && this.currentPoint_.concat();
};


/**
 * @return {!goog.math.Path} A copy of this path.
 */
goog.math.Path.prototype.clone = function() {
  var path = new goog.math.Path();
  path.segments_ = this.segments_.concat();
  path.count_ = this.count_.concat();
  path.arguments_ = this.arguments_.concat();
  path.closePoint_ = this.closePoint_ && this.closePoint_.concat();
  path.currentPoint_ = this.currentPoint_ && this.currentPoint_.concat();
  path.simple_ = this.simple_;
  return path;
};


/**
 * Returns true if this path contains no arcs. Simplified paths can be
 * created using {@code createSimplifiedPath}.
 *
 * @return {boolean} True if the path contains no arcs.
 */
goog.math.Path.prototype.isSimple = function() {
  return this.simple_;
};


/**
 * A map from segment type to the path function to call to simplify a path.
 * @private {!Object<goog.math.Path.Segment, function(this: goog.math.Path)>}
 */
goog.math.Path.simplifySegmentMap_ = (function() {
  var map = {};
  map[goog.math.Path.Segment.MOVETO] = goog.math.Path.prototype.moveTo;
  map[goog.math.Path.Segment.LINETO] = goog.math.Path.prototype.lineTo;
  map[goog.math.Path.Segment.CLOSE] = goog.math.Path.prototype.close;
  map[goog.math.Path.Segment.CURVETO] =
      goog.math.Path.prototype.curveTo;
  map[goog.math.Path.Segment.ARCTO] =
      goog.math.Path.prototype.arcToAsCurves;
  return map;
})();


/**
 * Creates a copy of the given path, replacing {@code arcTo} with
 * {@code arcToAsCurves}. The resulting path is simplified and can
 * be transformed.
 *
 * @param {!goog.math.Path} src The path to simplify.
 * @return {!goog.math.Path} A new simplified path.
 */
goog.math.Path.createSimplifiedPath = function(src) {
  if (src.isSimple()) {
    return src.clone();
  }
  var path = new goog.math.Path();
  src.forEachSegment(function(segment, args) {
    goog.math.Path.simplifySegmentMap_[segment].apply(path, args);
  });
  return path;
};


// TODO(chrisn): Delete this method
/**
 * Creates a transformed copy of this path. The path is simplified
 * {@see #createSimplifiedPath} prior to transformation.
 *
 * @param {!goog.math.AffineTransform} tx The transformation to perform.
 * @return {!goog.math.Path} A new, transformed path.
 */
goog.math.Path.prototype.createTransformedPath = function(tx) {
  var path = goog.math.Path.createSimplifiedPath(this);
  path.transform(tx);
  return path;
};


/**
 * Transforms the path. Only simple paths are transformable. Attempting
 * to transform a non-simple path will throw an error.
 *
 * @param {!goog.math.AffineTransform} tx The transformation to perform.
 * @return {!goog.math.Path} The path itself.
 */
goog.math.Path.prototype.transform = function(tx) {
  if (!this.isSimple()) {
    throw Error('Non-simple path');
  }
  tx.transform(this.arguments_, 0, this.arguments_, 0,
      this.arguments_.length / 2);
  if (this.closePoint_) {
    tx.transform(this.closePoint_, 0, this.closePoint_, 0, 1);
  }
  if (this.currentPoint_ && this.closePoint_ != this.currentPoint_) {
    tx.transform(this.currentPoint_, 0, this.currentPoint_, 0, 1);
  }
  return this;
};


/**
 * @return {boolean} Whether the path is empty.
 */
goog.math.Path.prototype.isEmpty = function() {
  return this.segments_.length == 0;
};
