text: 'use strict';
  import H from './../../parts/Globals.js';
  import './../../parts/Utilities.js';
  import './../../parts/Tooltip.js';
  import ControlPoint from './../ControlPoint.js';
  import MockPoint from './../MockPoint.js';
  
  /**
   * It provides methods for handling points, control points
   * and points transformations.
   *
   * @private
   * @mixin
   * @memberOf Annotation
   */
  var controllableMixin = {
      /**
       * Init the controllable
       *
       * @param {Annotation} annotation - an annotation instance
       * @param {Object} options - options specific for controllable
       * @param {number} index - index of the controllable element
       **/
      init: function (annotation, options, index) {
          this.annotation = annotation;
          this.chart = annotation.chart;
          this.options = options;
          this.points = [];
          this.controlPoints = [];
          this.index = index;
  
          this.linkPoints();
          this.addControlPoints();
      },
  
      /**
       * Redirect attr usage on the controllable graphic element.
       **/
      attr: function () {
          this.graphic.attr.apply(this.graphic, arguments);
      },
  
  
      /**
       * Get the controllable's points options.
       *
       * @return {Array<PointLikeOptions>} - an array of points' options.
       *
       */
      getPointsOptions: function () {
          var options = this.options;
  
          return options.points || (options.point && H.splat(options.point));
      },
  
      /**
       * Utility function for mapping item's options
       * to element's attribute
       *
       * @param {Object} options
       * @return {Object} mapped options
       **/
      attrsFromOptions: function (options) {
          var map = this.constructor.attrsMap,
              attrs = {},
              key,
              mappedKey,
              styledMode = this.chart.styledMode;
  
          for (key in options) {
              mappedKey = map[key];
  
              if (
                  mappedKey &&
                  (
                      !styledMode ||
                      ['fill', 'stroke', 'stroke-width']
                          .indexOf(mappedKey) === -1
                  )
              ) {
                  attrs[mappedKey] = options[key];
              }
          }
  
          return attrs;
      },
  
      /**
       * @typedef {Object} Annotation.controllableMixin.Position
       * @property {number} x
       * @property {number} y
       */
  
      /**
       * An object which denotes an anchor position
       *
       * @typedef Annotation.controllableMixin.AnchorPosition
       *          Annotation.controllableMixin.Position
       * @property {number} height
       * @property {number} width
       */
  
      /**
       * An object which denots a controllable's anchor positions
       * - relative and absolute.
       *
       * @typedef {Object} Annotation.controllableMixin.Anchor
       * @property {Annotation.controllableMixin.AnchorPosition} relativePosition
       * @property {Annotation.controllableMixin.AnchorPosition} absolutePosition
       */
  
      /**
       * Returns object which denotes anchor position - relative and absolute.
       *
       * @param {Annotation.PointLike} point a point like object
       * @return {Annotation.controllableMixin.Anchor} a controllable anchor
       */
      anchor: function (point) {
          var plotBox = point.series.getPlotBox(),
  
              box = point.mock ?
                  point.toAnchor() :
                  H.Tooltip.prototype.getAnchor.call({
                      chart: point.series.chart
                  }, point),
  
              anchor = {
                  x: box[0] + (this.options.x || 0),
                  y: box[1] + (this.options.y || 0),
                  height: box[2] || 0,
                  width: box[3] || 0
              };
  
          return {
              relativePosition: anchor,
              absolutePosition: H.merge(anchor, {
                  x: anchor.x + plotBox.translateX,
                  y: anchor.y + plotBox.translateY
              })
          };
      },
  
      /**
       * Map point's options to a point-like object.
       *
       * @param {Highcharts.MockPointOptionsObject} pointOptions
       *        point's options
       * @param {Highcharts.PointLike} point
       *        a point like instance
       *
       * @return {Highcharts.PointLike|null}
       *         if the point is found/set returns this point, otherwise null
       */
      point: function (pointOptions, point) {
          if (pointOptions && pointOptions.series) {
              return pointOptions;
          }
  
          if (!point || point.series === null) {
              if (H.isObject(pointOptions)) {
                  point = new MockPoint(
                      this.chart,
                      this,
                      pointOptions
                  );
              } else if (H.isString(pointOptions)) {
                  point = this.chart.get(pointOptions) || null;
              } else if (typeof pointOptions === 'function') {
                  var pointConfig = pointOptions.call(point, this);
  
                  point = pointConfig.series ?
                      pointConfig :
                      new MockPoint(
                          this.chart,
                          this,
                          pointOptions
                      );
              }
          }
  
          return point;
      },
  
      /**
       * Find point-like objects based on points options.
       *
       * @return {Array<Annotation.PointLike>} an array of point-like objects
       */
      linkPoints: function () {
          var pointsOptions = this.getPointsOptions(),
              points = this.points,
              len = (pointsOptions && pointsOptions.length) || 0,
              i,
              point;
  
          for (i = 0; i < len; i++) {
              point = this.point(pointsOptions[i], points[i]);
  
              if (!point) {
                  points.length = 0;
  
                  return;
              }
  
              if (point.mock) {
                  point.refresh();
              }
  
              points[i] = point;
          }
  
          return points;
      },
  
  
      /**
       * Add control points to a controllable.
       */
      addControlPoints: function () {
          var controlPointsOptions = this.options.controlPoints;
  
          (controlPointsOptions || []).forEach(
              function (controlPointOptions, i) {
                  var options = H.merge(
                      this.options.controlPointOptions,
                      controlPointOptions
                  );
  
                  if (!options.index) {
                      options.index = i;
                  }
  
                  controlPointsOptions[i] = options;
  
                  this.controlPoints.push(
                      new ControlPoint(this.chart, this, options)
                  );
              },
              this
          );
      },
  
      /**
       * Check if a controllable should be rendered/redrawn.
       *
       * @return {boolean} whether a controllable should be drawn.
       */
      shouldBeDrawn: function () {
          return Boolean(this.points.length);
      },
  
      /**
       * Render a controllable.
       **/
      render: function () {
          this.controlPoints.forEach(function (controlPoint) {
              controlPoint.render();
          });
      },
  
      /**
       * Redraw a controllable.
       *
       * @param {boolean} animation
       **/
      redraw: function (animation) {
          this.controlPoints.forEach(function (controlPoint) {
              controlPoint.redraw(animation);
          });
      },
  
      /**
       * Transform a controllable with a specific transformation.
       *
       * @param {string} transformation a transformation name
       * @param {number} cx origin x transformation
       * @param {number} cy origin y transformation
       * @param {number} p1 param for the transformation
       * @param {number} p2 param for the transformation
       **/
      transform: function (transformation, cx, cy, p1, p2) {
          if (this.chart.inverted) {
              var temp = cx;
  
              cx = cy;
              cy = temp;
          }
  
          this.points.forEach(function (point, i) {
              this.transformPoint(transformation, cx, cy, p1, p2, i);
          }, this);
      },
  
      /**
       * Transform a point with a specific transformation
       * If a transformed point is a real point it is replaced with
       * the mock point.
       *
       * @param {string} transformation a transformation name
       * @param {number} cx origin x transformation
       * @param {number} cy origin y transformation
       * @param {number} p1 param for the transformation
       * @param {number} p2 param for the transformation
       * @param {number} i index of the point
       *
       **/
      transformPoint: function (transformation, cx, cy, p1, p2, i) {
          var point = this.points[i];
  
          if (!point.mock) {
              point = this.points[i] = MockPoint.fromPoint(point);
          }
  
          point[transformation](cx, cy, p1, p2);
      },
  
      /**
       * Translate a controllable.
       *
       * @param {number} dx translation for x coordinate
       * @param {number} dy translation for y coordinate
       **/
      translate: function (dx, dy) {
          this.transform('translate', null, null, dx, dy);
      },
  
      /**
       * Translate a specific point within a controllable.
       *
       * @param {number} dx translation for x coordinate
       * @param {number} dy translation for y coordinate
       * @param {number} i index of the point
       **/
      translatePoint: function (dx, dy, i) {
          this.transformPoint('translate', null, null, dx, dy, i);
      },
  
      /**
       * Translate shape within controllable item.
       * Replaces `controllable.translate` method.
       *
       * @param {number} dx translation for x coordinate
       * @param {number} dy translation for y coordinate
       */
      translateShape: function (dx, dy) {
          var chart = this.annotation.chart,
              // Annotation.options
              shapeOptions = this.annotation.userOptions,
              // Chart.options.annotations
              annotationIndex = chart.annotations.indexOf(this.annotation),
              chartOptions = chart.options.annotations[annotationIndex];
  
          this.translatePoint(dx, dy, 0);
  
          // Options stored in:
          // - chart (for exporting)
          // - current config (for redraws)
          chartOptions[this.collection][this.index].point = this.options.point;
          shapeOptions[this.collection][this.index].point = this.options.point;
      },
  
      /**
       * Rotate a controllable.
       *
       * @param {number} cx origin x rotation
       * @param {number} cy origin y rotation
       * @param {number} radians
       **/
      rotate: function (cx, cy, radians) {
          this.transform('rotate', cx, cy, radians);
      },
  
      /**
       * Scale a controllable.
       *
       * @param {number} cx origin x rotation
       * @param {number} cy origin y rotation
       * @param {number} sx scale factor x
       * @param {number} sy scale factor y
       */
      scale: function (cx, cy, sx, sy) {
          this.transform('scale', cx, cy, sx, sy);
      },
  
      /**
       * Set control points' visibility.
       *
       * @param {boolean} [visible]
       */
      setControlPointsVisibility: function (visible) {
          this.controlPoints.forEach(function (controlPoint) {
              controlPoint.setVisibility(visible);
          });
      },
  
      /**
       * Destroy a controllable.
       */
      destroy: function () {
          if (this.graphic) {
              this.graphic = this.graphic.destroy();
          }
  
          if (this.tracker) {
              this.tracker = this.tracker.destroy();
          }
  
          this.controlPoints.forEach(function (controlPoint) {
              controlPoint.destroy();
          });
  
          this.chart = null;
          this.points = null;
          this.controlPoints = null;
          this.options = null;
  
          if (this.annotation) {
              this.annotation = null;
          }
      },
  
      /**
       * Update a controllable.
       *
       * @param {Object} newOptions
       */
      update: function (newOptions) {
          var annotation = this.annotation,
              options = H.merge(true, this.options, newOptions),
              parentGroup = this.graphic.parentGroup;
  
          this.destroy();
          this.constructor(annotation, options);
          this.render(parentGroup);
          this.redraw();
      }
  };
  
  export default controllableMixin;
  