/**
 * @file Visual mapping.
 */
define(function (require) {

    var zrUtil = require('zrender/core/util');
    var zrColor = require('zrender/tool/color');
    var linearMap = require('../util/number').linearMap;
    var each = zrUtil.each;
    var isObject = zrUtil.isObject;

    var CATEGORY_DEFAULT_VISUAL_INDEX = -1;

    /**
     * @param {Object} option
     * @param {string} [option.type] See visualHandlers.
     * @param {string} [option.mappingMethod] 'linear' or 'piecewise' or 'category' or 'fixed'
     * @param {Array.<number>=} [option.dataExtent] [minExtent, maxExtent],
     *                                              required when mappingMethod is 'linear'
     * @param {Array.<Object>=} [option.pieceList] [
     *                                             {value: someValue},
     *                                             {interval: [min1, max1], visual: {...}},
     *                                             {interval: [min2, max2]}
     *                                             ],
     *                                            required when mappingMethod is 'piecewise'.
     *                                            Visual for only each piece can be specified.
     * @param {Array.<string|Object>=} [option.categories] ['cate1', 'cate2']
     *                                            required when mappingMethod is 'category'.
     *                                            If no option.categories, categories is set
     *                                            as [0, 1, 2, ...].
     * @param {boolean} [option.loop=false] Whether loop mapping when mappingMethod is 'category'.
     * @param {(Array|Object|*)} [option.visual]  Visual data.
     *                                            when mappingMethod is 'category',
     *                                            visual data can be array or object
     *                                            (like: {cate1: '#222', none: '#fff'})
     *                                            or primary types (which represents
     *                                            defualt category visual), otherwise visual
     *                                            can be array or primary (which will be
     *                                            normalized to array).
     *
     */
    var VisualMapping = function (option) {
        var mappingMethod = option.mappingMethod;
        var visualType = option.type;

        /**
         * @readOnly
         * @type {Object}
         */
        var thisOption = this.option = zrUtil.clone(option);

        /**
         * @readOnly
         * @type {string}
         */
        this.type = visualType;

        /**
         * @readOnly
         * @type {string}
         */
        this.mappingMethod = mappingMethod;

        /**
         * @private
         * @type {Function}
         */
        this._normalizeData = normalizers[mappingMethod];

        var visualHandler = visualHandlers[visualType];

        /**
         * @public
         * @type {Function}
         */
        this.applyVisual = visualHandler.applyVisual;

        /**
         * @public
         * @type {Function}
         */
        this.getColorMapper = visualHandler.getColorMapper;

        /**
         * @private
         * @type {Function}
         */
        this._doMap = visualHandler._doMap[mappingMethod];

        if (mappingMethod === 'piecewise') {
            normalizeVisualRange(thisOption);
            preprocessForPiecewise(thisOption);
        }
        else if (mappingMethod === 'category') {
            thisOption.categories
                ? preprocessForSpecifiedCategory(thisOption)
                // categories is ordinal when thisOption.categories not specified,
                // which need no more preprocess except normalize visual.
                : normalizeVisualRange(thisOption, true);
        }
        else { // mappingMethod === 'linear' or 'fixed'
            zrUtil.assert(mappingMethod !== 'linear' || thisOption.dataExtent);
            normalizeVisualRange(thisOption);
        }
    };

    VisualMapping.prototype = {

        constructor: VisualMapping,

        mapValueToVisual: function (value) {
            var normalized = this._normalizeData(value);
            return this._doMap(normalized, value);
        },

        getNormalizer: function () {
            return zrUtil.bind(this._normalizeData, this);
        }
    };

    var visualHandlers = VisualMapping.visualHandlers = {

        color: {

            applyVisual: makeApplyVisual('color'),

            /**
             * Create a mapper function
             * @return {Function}
             */
            getColorMapper: function () {
                var thisOption = this.option;
                var parsedVisual = zrUtil.map(thisOption.visual, zrColor.parse);

                return zrUtil.bind(
                    thisOption.mappingMethod === 'category'
                        ? function (value, isNormalized) {
                            !isNormalized && (value = this._normalizeData(value));
                            return doMapCategory(this, value);
                        }
                        : function (value, isNormalized, out) {
                            // If output rgb array
                            // which will be much faster and useful in pixel manipulation
                            var returnRGBArray = !!out;
                            !isNormalized && (value = this._normalizeData(value));
                            out = zrColor.fastMapToColor(value, parsedVisual, out);
                            return returnRGBArray ? out : zrUtil.stringify(out, 'rgba');
                        },
                    this
                );
            },

            _doMap: {
                linear: function (normalized) {
                    return zrColor.mapToColor(normalized, this.option.visual);
                },
                category: doMapCategory,
                piecewise: function (normalized, value) {
                    var result = getSpecifiedVisual.call(this, value);
                    if (result == null) {
                        result = zrColor.mapToColor(normalized, this.option.visual);
                    }
                    return result;
                },
                fixed: doMapFixed
            }
        },

        colorHue: makePartialColorVisualHandler(function (color, value) {
            return zrColor.modifyHSL(color, value);
        }),

        colorSaturation: makePartialColorVisualHandler(function (color, value) {
            return zrColor.modifyHSL(color, null, value);
        }),

        colorLightness: makePartialColorVisualHandler(function (color, value) {
            return zrColor.modifyHSL(color, null, null, value);
        }),

        colorAlpha: makePartialColorVisualHandler(function (color, value) {
            return zrColor.modifyAlpha(color, value);
        }),

        opacity: {
            applyVisual: makeApplyVisual('opacity'),
            _doMap: makeDoMap([0, 1])
        },

        symbol: {
            applyVisual: function (value, getter, setter) {
                var symbolCfg = this.mapValueToVisual(value);
                if (zrUtil.isString(symbolCfg)) {
                    setter('symbol', symbolCfg);
                }
                else if (isObject(symbolCfg)) {
                    for (var name in symbolCfg) {
                        if (symbolCfg.hasOwnProperty(name)) {
                            setter(name, symbolCfg[name]);
                        }
                    }
                }
            },
            _doMap: {
                linear: doMapToArray,
                category: doMapCategory,
                piecewise: function (normalized, value) {
                    var result = getSpecifiedVisual.call(this, value);
                    if (result == null) {
                        result = doMapToArray.call(this, normalized);
                    }
                    return result;
                },
                fixed: doMapFixed
            }
        },

        symbolSize: {
            applyVisual: makeApplyVisual('symbolSize'),
            _doMap: makeDoMap([0, 1])
        }
    };


    function preprocessForPiecewise(thisOption) {
        var pieceList = thisOption.pieceList;
        thisOption.hasSpecialVisual = false;

        zrUtil.each(pieceList, function (piece, index) {
            piece.originIndex = index;
            // piece.visual is "result visual value" but not
            // a visual range, so it does not need to be normalized.
            if (piece.visual != null) {
                thisOption.hasSpecialVisual = true;
            }
        });
    }

    function preprocessForSpecifiedCategory(thisOption) {
        // Hash categories.
        var categories = thisOption.categories;
        var visual = thisOption.visual;

        var categoryMap = thisOption.categoryMap = {};
        each(categories, function (cate, index) {
            categoryMap[cate] = index;
        });

        // Process visual map input.
        if (!zrUtil.isArray(visual)) {
            var visualArr = [];

            if (zrUtil.isObject(visual)) {
                each(visual, function (v, cate) {
                    var index = categoryMap[cate];
                    visualArr[index != null ? index : CATEGORY_DEFAULT_VISUAL_INDEX] = v;
                });
            }
            else { // Is primary type, represents default visual.
                visualArr[CATEGORY_DEFAULT_VISUAL_INDEX] = visual;
            }

            visual = thisOption.visual = visualArr;
        }

        // Remove categories that has no visual,
        // then we can mapping them to CATEGORY_DEFAULT_VISUAL_INDEX.
        for (var i = categories.length - 1; i >= 0; i--) {
            if (visual[i] == null) {
                delete categoryMap[categories[i]];
                categories.pop();
            }
        }
    }

    function normalizeVisualRange(thisOption, isCategory) {
        var visual = thisOption.visual;
        var visualArr = [];

        if (zrUtil.isObject(visual)) {
            each(visual, function (v) {
                visualArr.push(v);
            });
        }
        else if (visual != null) {
            visualArr.push(visual);
        }

        var doNotNeedPair = {color: 1, symbol: 1};

        if (!isCategory
            && visualArr.length === 1
            && !(thisOption.type in doNotNeedPair)
        ) {
            // Do not care visualArr.length === 0, which is illegal.
            visualArr[1] = visualArr[0];
        }

        thisOption.visual = visualArr;
    }

    function makePartialColorVisualHandler(applyValue) {
        return {
            applyVisual: function (value, getter, setter) {
                value = this.mapValueToVisual(value);
                // Must not be array value
                setter('color', applyValue(getter('color'), value));
            },
            _doMap: makeDoMap([0, 1])
        };
    }

    function doMapToArray(arr, normalized) {
        var visual = this.option.visual;
        return visual[
            Math.round(linearMap(normalized, [0, 1], [0, visual.length - 1], true))
        ] || {};
    }

    function makeApplyVisual(visualType) {
        return function (value, getter, setter) {
            setter(visualType, this.mapValueToVisual(value));
        };
    }

    function doMapCategory(normalized) {
        var visual = this.option.visual;
        return visual[
            (this.option.loop && normalized !== CATEGORY_DEFAULT_VISUAL_INDEX)
                ? normalized % visual.length
                : normalized
        ];
    }

    function doMapFixed() {
        return this.option.visual[0];
    }

    function makeDoMap(sourceExtent) {
        return {
            linear: function (normalized) {
                return linearMap(normalized, sourceExtent, this.option.visual, true);
            },
            category: doMapCategory,
            piecewise: function (normalized, value) {
                var result = getSpecifiedVisual.call(this, value);
                if (result == null) {
                    result = linearMap(normalized, sourceExtent, this.option.visual, true);
                }
                return result;
            },
            fixed: doMapFixed
        };
    }

    function getSpecifiedVisual(value) {
        var thisOption = this.option;
        var pieceList = thisOption.pieceList;
        if (thisOption.hasSpecialVisual) {
            var pieceIndex = VisualMapping.findPieceIndex(value, pieceList);
            var piece = pieceList[pieceIndex];
            if (piece && piece.visual) {
                return piece.visual[this.type];
            }
        }
    }


    /**
     * Normalizers by mapping methods.
     */
    var normalizers = {

        linear: function (value) {
            return linearMap(value, this.option.dataExtent, [0, 1], true);
        },

        piecewise: function (value) {
            var pieceList = this.option.pieceList;
            var pieceIndex = VisualMapping.findPieceIndex(value, pieceList, true);
            if (pieceIndex != null) {
                return linearMap(pieceIndex, [0, pieceList.length - 1], [0, 1], true);
            }
        },

        category: function (value) {
            var index = this.option.categories
                ? this.option.categoryMap[value]
                : value; // ordinal
            return index == null ? CATEGORY_DEFAULT_VISUAL_INDEX : index;
        },

        fixed: zrUtil.noop
    };



    /**
     * @public
     */
    VisualMapping.addVisualHandler = function (name, handler) {
        visualHandlers[name] = handler;
    };

    /**
     * @public
     */
    VisualMapping.isValidType = function (visualType) {
        return visualHandlers.hasOwnProperty(visualType);
    };

    /**
     * Convinent method.
     * Visual can be Object or Array or primary type.
     *
     * @public
     */
    VisualMapping.eachVisual = function (visual, callback, context) {
        if (zrUtil.isObject(visual)) {
            zrUtil.each(visual, callback, context);
        }
        else {
            callback.call(context, visual);
        }
    };

    VisualMapping.mapVisual = function (visual, callback, context) {
        var isPrimary;
        var newVisual = zrUtil.isArray(visual)
            ? []
            : zrUtil.isObject(visual)
            ? {}
            : (isPrimary = true, null);

        VisualMapping.eachVisual(visual, function (v, key) {
            var newVal = callback.call(context, v, key);
            isPrimary ? (newVisual = newVal) : (newVisual[key] = newVal);
        });
        return newVisual;
    };

    /**
     * @public
     * @param {Object} obj
     * @return {Oject} new object containers visual values.
     *                 If no visuals, return null.
     */
    VisualMapping.retrieveVisuals = function (obj) {
        var ret = {};
        var hasVisual;

        obj && each(visualHandlers, function (h, visualType) {
            if (obj.hasOwnProperty(visualType)) {
                ret[visualType] = obj[visualType];
                hasVisual = true;
            }
        });

        return hasVisual ? ret : null;
    };

    /**
     * Give order to visual types, considering colorSaturation, colorAlpha depends on color.
     *
     * @public
     * @param {(Object|Array)} visualTypes If Object, like: {color: ..., colorSaturation: ...}
     *                                     IF Array, like: ['color', 'symbol', 'colorSaturation']
     * @return {Array.<string>} Sorted visual types.
     */
    VisualMapping.prepareVisualTypes = function (visualTypes) {
        if (isObject(visualTypes)) {
            var types = [];
            each(visualTypes, function (item, type) {
                types.push(type);
            });
            visualTypes = types;
        }
        else if (zrUtil.isArray(visualTypes)) {
            visualTypes = visualTypes.slice();
        }
        else {
            return [];
        }

        visualTypes.sort(function (type1, type2) {
            // color should be front of colorSaturation, colorAlpha, ...
            // symbol and symbolSize do not matter.
            return (type2 === 'color' && type1 !== 'color' && type1.indexOf('color') === 0)
                ? 1 : -1;
        });

        return visualTypes;
    };

    /**
     * 'color', 'colorSaturation', 'colorAlpha', ... are depends on 'color'.
     * Other visuals are only depends on themself.
     *
     * @public
     * @param {string} visualType1
     * @param {string} visualType2
     * @return {boolean}
     */
    VisualMapping.dependsOn = function (visualType1, visualType2) {
        return visualType2 === 'color'
            ? !!(visualType1 && visualType1.indexOf(visualType2) === 0)
            : visualType1 === visualType2;
    };

    /**
     * @param {number} value
     * @param {Array.<Object>} pieceList [{value: ..., interval: [min, max]}, ...]
     *                         Always from small to big.
     * @param {boolean} [findClosestWhenOutside=false]
     * @return {number} index
     */
    VisualMapping.findPieceIndex = function (value, pieceList, findClosestWhenOutside) {
        var possibleI;
        var abs = Infinity;

        // value has the higher priority.
        for (var i = 0, len = pieceList.length; i < len; i++) {
            var pieceValue = pieceList[i].value;
            if (pieceValue != null) {
                if (pieceValue === value) {
                    return i;
                }
                findClosestWhenOutside && updatePossible(pieceValue, i);
            }
        }

        for (var i = 0, len = pieceList.length; i < len; i++) {
            var piece = pieceList[i];
            var interval = piece.interval;
            var close = piece.close;

            if (interval) {
                if (interval[0] === -Infinity) {
                    if (littleThan(close[1], value, interval[1])) {
                        return i;
                    }
                }
                else if (interval[1] === Infinity) {
                    if (littleThan(close[0], interval[0], value)) {
                        return i;
                    }
                }
                else if (
                    littleThan(close[0], interval[0], value)
                    && littleThan(close[1], value, interval[1])
                ) {
                    return i;
                }
                findClosestWhenOutside && updatePossible(interval[0], i);
                findClosestWhenOutside && updatePossible(interval[1], i);
            }
        }

        if (findClosestWhenOutside) {
            return value === Infinity
                ? pieceList.length - 1
                : value === -Infinity
                ? 0
                : possibleI;
        }

        function updatePossible(val, index) {
            var newAbs = Math.abs(val - value);
            if (newAbs < abs) {
                abs = newAbs;
                possibleI = index;
            }
        }

    };

    function littleThan(close, a, b) {
        return close ? a <= b : a < b;
    }

    return VisualMapping;

});