/**
 * The d3.svg.axis component is used to display reference lines for D3 scales.
 * The Ext.d3.axis.Axis component wraps both with an added ability to display an axis title
 * in the user specified position. This allows to configure axes declaratively
 * in any D3 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.
 * 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 `d3.svg.axis` config object or a `d3.svg.axis` instance itself.
         * In case of a config object, the property names should represent `d3.svg.axis` methods,
         * while the property value should repsent method's parameter(s). In case the 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.time.days` should be made a string
         * `'d3.time.days'` that does not have any dependencies and will be evaluated at a later time,
         * when `d3` is already loaded.
         * For example, this
         *
         *     d3.svg.axis().orient('bottom').ticks(d3.time.days).tickFormat(d3.time.format('%b %d'));
         *
         * is equivalent to this:
         *
         *     {
         *         orient: 'bottom',
         *         ticks: 'd3.time.days',
         *         tickFormat: "d3.time.format('%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/Function} scale
         * A D3 scale or its config object.
         * In case of a config object, the property names should represent a particular scale's methods,
         * while the property value should repsent method's parameter(s). In case the 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.scale.linear().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-3.x-api-reference/blob/master/Scales.md)
         * 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)) {
                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.svg.axis 
                if (oldAxis) { // reconfigure 
                    axis = Ext.d3.Helpers.configure(oldAxis, axis);
                } else { // create 
                    axis = Ext.d3.Helpers.make('svg.axis', axis);
                }
            }
            if (scale) {
                axis.scale(scale);
            }
        }
 
        return axis || oldAxis;
    },
 
    updateParent: function (parent) {
        var me = this,
            axis = me.getAxis();
 
        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(),
            orient = axis.orient(),
            isVertical = orient === 'left' || orient === '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 (orient) {
            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(),
            orient = axis.orient(),
            scale = me.getScale();
 
        if (!(scale.domain().length && scale.range().length)) {
            return;
        }
 
        if (transition) {
            transition.select('#' + me.getId()).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', orient);
        me.positionTitle();
    },
 
    destroy: function () {
        this.mixins.detached.destroy.call(this);
    }
});