'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.
 *
 * @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 {Annotation.MockPoint.Options} pointOptions point's options
     * @param {Annotation.PointLike} point a point like instance
     * @return {Annotation.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);
    },

    /**
     * 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;
