/**
 * The d3.svg.axis component is used to display reference lines for D3 scales.
 * A thin wrapper around the d3.axis* and d3.scale* with an added ability to display
 * an axis title in the user specified position. This allows to configure axes declaratively
 * in any Ext component that uses them, instead of using D3's method chaining, which
 * would look quite alien in Ext views, as well as pose some technical and interoperability
 * issues (e.g. dependency management issues when views declared with Ext.define reference
 * `d3` directly).
 *
 * Note that this is a thin wrapper around the native D3 axis and scale, so when
 * any changes are applied directly to those D3 entities, for example
 * `ExtD3Axis.getScale().domain([30, 40])`, this class won't be nofied about such changes.
 * So it's up to the developer to account for them, like rerendering the axis `ExtD3Axis.render()`
 * or making sure that "nice" segmentation is preserved by chaining a call to `nice` after the
 * domain is set, e.g. `ExtD3Axis.getScale().domain([30, 40]).nice()`.
 *
 * The axis is designed to work with the {@link Ext.d3.svg.Svg} component.
 */
Ext.define('Ext.d3.axis.Axis', {
    requires: [
        'Ext.d3.Helpers'
    ],

    mixins: {
        observable: 'Ext.mixin.Observable',
        detached: 'Ext.d3.mixin.Detached'
    },

    config: {
        /**
         * @cfg {Object} axis
         * A config object to create a `d3.axis*` instance from.
         * A property name should represent an actual `d3.axis*` method,
         * while its value should represent method's parameter(s). In case a method takes multiple
         * parameters, the property name should be prefixed with the dollar sign, and the property
         * value should be an array of parameters. Additionally, the values should not reference
         * the global `d3` variable, as the `d3` dependency is unlikely to be loaded at the time
         * of component definition. So a value such as `d3.timeDay` should be made a string
         * `'d3.timeDay'` that does not have any dependencies and will be evaluated at a later time,
         * when `d3` is already loaded.
         * For example, this
         *
         *     d3.axisBottom().tickFormat(d3.timeFormat('%b %d'));
         *
         * is equivalent to this:
         *
         *     {
         *         orient: 'bottom',
         *         tickFormat: "d3.timeFormat('%b %d')"
         *     }
         *
         * Please see the D3's [SVG Axes](https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md)
         * documentation for more details.
         */
        axis: {
            orient: 'top'
        },

        /**
         * @cfg {Object} scale
         * A config object to create a `d3.scale*` instance from.
         * A property name should represent an actual `d3.scale*` method,
         * while its value should repsent method's parameter(s). In case a method takes multiple
         * parameters, the property name should be prefixed with the dollar sign, and the property
         * value should be an array of parameters. Additionally, the values should not reference
         * the global `d3` variable, as the `d3` dependency is unlikely to be loaded at the time
         * of component definition. So a value such as `d3.range(0, 100, 20)` should be made
         * a string `'d3.range(0, 100, 20)'` that does not have any dependencies and will be
         * evaluated at a later time, when `d3` is already loaded.
         * For example, this
         *
         *     d3.scaleLinear().range(d3.range(0, 100, 20));
         *
         * is equivalent to this:
         *
         *     {
         *         type: 'linear',
         *         range: 'd3.range(0, 100, 20)'
         *     }
         *
         * Please see the D3's [Scales](https://github.com/d3/d3-scale)
         * documentation for more details.
         */
        scale: {
            type: 'linear'
        },

        /**
         * @cfg {Object} title
         * @cfg {String} title.text Axis title text.
         * @cfg {String} [title.position='outside']
         * Controls the vertical placement of the axis title. Available options are:
         *
         *   - `'outside'`: axis title is placed on the tick side
         *   - `'inside'`: axis title is placed on the side with no ticks
         *
         * @cfg {String} [title.alignment='middle']
         * Controls the horizontal placement of the axis title. Available options are:
         *
         *   - `'middle'`, `'center'`: axis title is placed in the middle of the axis line
         *   - `'start'`, `'left'`: axis title is placed at the start of the axis line
         *   - `'end'`, `'right'`: axis title is placed at the end of the axis line
         *
         * @cfg {String} [title.padding='0.5em']
         * The gap between the title and axis labels.
         */
        title: null,

        /**
         * @cfg {SVGElement/d3.selection} parent
         * The parent group of the d3.svg.axis as either an SVGElement or a D3 selection.
         */
        parent: null,

        /**
         * @cfg {Ext.d3.svg.Svg} component
         * The SVG component that owns this axis.
         */
        component: null
    },

    defaultCls: {
        self: Ext.baseCSSPrefix + 'd3-axis',
        title: Ext.baseCSSPrefix + 'd3-axis-title'
    },

    title: null,
    group: null,
    domain: null,

    constructor: function(config) {
        var me = this,
            id;

        config = config || {};

        if ('id' in config) {
            id = config.id;
        }
        else if ('id' in me.config) {
            id = me.config.id;
        }
        else {
            id = me.getId();
        }

        me.setId(id);

        me.mixins.detached.constructor.call(me, config);
        me.group = me.getDetached().append('g')
            .classed(me.defaultCls.self, true)
            .attr('id', me.getId());

        me.mixins.observable.constructor.call(me, config);
    },

    getGroup: function() {
        return this.group;
    },

    getBox: function() {
        return this.group.node().getBBox();
    },

    applyScale: function(scale, oldScale) {
        var axis = this.getAxis();

        if (scale) {
            if (!Ext.isFunction(scale)) { // if `scale` is not already a d3.scale*
                scale = Ext.d3.Helpers.makeScale(scale);
            }

            if (axis) {
                axis.scale(scale);
            }
        }

        return scale || oldScale;
    },

    applyAxis: function(axis, oldAxis) {
        var scale = this.getScale();

        if (axis) {
            if (!Ext.isFunction(axis)) { // if `axis` is not already a d3.axis*
                axis = Ext.d3.Helpers.makeAxis(axis);
            }

            if (scale) {
                axis.scale(scale);
            }
        }

        return axis || oldAxis;
    },

    updateParent: function(parent) {
        var me = this;

        if (parent) {
            // Move axis `group` from `detached` to `parent`.
            me.attach(parent, me.group);
            me.render();
        }
        else {
            me.detach(me.group);
        }
    },

    updateTitle: function(title) {
        var me = this;

        if (title) {
            if (me.title) {
                if (me.isDetached(me.title)) {
                    me.attach(me.group, me.title);
                }
            }
            else {
                me.title = me.group.append('text').classed(me.defaultCls.title, true);
            }

            me.title.text(title.text || '');
            me.title.attr(title.attr);
            me.positionTitle(title);
        }
        else {
            if (me.title) {
                me.detach(me.title);
            }
        }
    },

    getAxisLine: function() {
        var me = this,
            domain = me.domain;

        if (!domain) {
            domain = me.group.select('path.domain');
        }

        return domain.empty() ? null : (me.domain = domain);
    },

    getTicksBBox: function() {
        var me = this,
            group = me.group,
            groupNode, temp, tempNode,
            ticks, bbox;

        ticks = group.selectAll('.tick');

        if (ticks.size()) {
            temp = group.append('g');
            tempNode = temp.node();
            groupNode = group.node();

            ticks.each(function() {
                tempNode.appendChild(this);
            });
            bbox = tempNode.getBBox();

            ticks.each(function() {
                groupNode.appendChild(this);
            });
            temp.remove();
        }

        return bbox;
    },

    positionTitle: function(cfg) {
        var me = this,
            title = me.title,
            axis = me.getAxis(),
            line = me.getAxisLine(),
            type = axis._type,
            isVertical = type === 'left' || type === 'right',
            Helpers = Ext.d3.Helpers,
            beforeEdge = 'text-before-edge',
            afterEdge = 'text-after-edge',
            alignment, position, padding,
            textAnchor, isOutside,
            lineBBox, ticksBBox,
            x = 0,
            y = 0;

        // See https://sencha.jira.com/browse/EXTJS-21421.
        // The scene may be insivible at this point, e.g. because we hide it
        // in the 'setupScene' method of the HeatMap component (see its comments).
        // The component itself is inside a document fragment during initialization.
        if (!(line && title && Ext.d3.Helpers.isBBoxable(me.getParent()))) {
            return;
        }

        cfg = cfg || me.getTitle();

        lineBBox = line.node().getBBox();
        ticksBBox = me.getTicksBBox();

        alignment = cfg.alignment || 'middle';
        position = cfg.position || 'outside';
        isOutside = position === 'outside';
        padding = cfg.padding || '0.5em';

        switch (alignment) {
            case 'start':
            case 'left':
                textAnchor = 'start';

                if (isVertical) {
                    y = lineBBox.y + lineBBox.height;
                }
                else {
                    x = lineBBox.x;
                }

                break;

            case 'end':
            case 'right':
                textAnchor = 'end';

                if (isVertical) {
                    y = lineBBox.y;
                }
                else {
                    x = lineBBox.x + lineBBox.width;
                }

                break;

            case 'middle':
            case 'center':
                textAnchor = 'middle';

                if (isVertical) {
                    y = lineBBox.y + lineBBox.height / 2;
                }
                else {
                    x = lineBBox.x + lineBBox.width / 2;
                }

                break;
        }

        switch (type) {
            case 'top':
                if (isOutside) {
                    title.attr('y', ticksBBox ? ticksBBox.y : 0);
                    padding = Helpers.unitMath(padding, '*', -1);
                }

                Helpers.setDominantBaseline(title.node(), isOutside ? afterEdge : beforeEdge);
                title
                    .attr('text-anchor', textAnchor)
                    .attr('x', x);
                break;

            case 'bottom':
                if (isOutside) {
                    title.attr('y', ticksBBox ? ticksBBox.y + ticksBBox.height : 0);
                }
                else {
                    padding = Helpers.unitMath(padding, '*', -1);
                }

                Helpers.setDominantBaseline(title.node(), isOutside ? beforeEdge : afterEdge);
                title
                    .attr('text-anchor', textAnchor)
                    .attr('x', x);
                break;

            case 'left':
                if (isOutside) {
                    x = ticksBBox ? ticksBBox.x : 0;
                    padding = Helpers.unitMath(padding, '*', -1);
                }

                Helpers.setDominantBaseline(title.node(), isOutside ? afterEdge : beforeEdge);
                title
                    .attr('text-anchor', textAnchor)
                    .attr('transform', 'translate(' + x + ', ' + y + ')' + 'rotate(-90)');
                break;

            case 'right':
                if (isOutside) {
                    x = ticksBBox ? ticksBBox.x + ticksBBox.width : 0;
                }
                else {
                    padding = Helpers.unitMath(padding, '*', -1);
                }

                Helpers.setDominantBaseline(title.node(), isOutside ? beforeEdge : afterEdge);
                title
                    .attr('text-anchor', textAnchor)
                    .attr('transform', 'translate(' + x + ', ' + y + ')' + 'rotate(-90)');
                break;
        }

        title.attr('dy', padding);
    },

    render: function(transition) {
        var me = this,
            axis = me.getAxis(),
            type = axis._type,
            scale = me.getScale();

        if (!(scale.domain().length && scale.range().length)) {
            return;
        }

        if (transition) {
            me.group.transition(transition).call(axis);
        }
        else {
            me.group.call(axis);
        }

        // It's crucial to set the 'data-orient' attribute before the call
        // to the positionTitle in order for the getTicksBBox method to
        // work correctly.
        me.group.attr('data-orient', type);
        me.positionTitle();
    },

    destroy: function() {
        this.mixins.detached.destroy.call(this);
    }
});
