/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import * as zrUtil from 'zrender/src/core/util';
import SeriesModel from '../../model/Series';
import Tree, { TreeNode } from '../../data/Tree';
import Model from '../../model/Model';
import {wrapTreePathInfo} from '../helper/treeHelper';
import {
    SeriesOption,
    BoxLayoutOptionMixin,
    ItemStyleOption,
    LabelOption,
    RoamOptionMixin,
    CallbackDataParams,
    ColorString,
    StatesOptionMixin,
    OptionId,
    OptionName,
    DecalObject,
    SeriesLabelOption,
    DefaultEmphasisFocus,
    AriaOptionMixin,
    BlurScope
} from '../../util/types';
import GlobalModel from '../../model/Global';
import { LayoutRect } from '../../util/layout';
import SeriesData from '../../data/SeriesData';
import { normalizeToArray } from '../../util/model';
import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
import enableAriaDecalForTree from '../helper/enableAriaDecalForTree';

// Only support numeric value.
type TreemapSeriesDataValue = number | number[];

interface BreadcrumbItemStyleOption extends ItemStyleOption {
    // TODO: textStyle should be in breadcrumb.label
    textStyle?: LabelOption
}

interface TreemapSeriesLabelOption extends SeriesLabelOption {
    formatter?: string | ((params: CallbackDataParams) => string)
}

interface TreemapSeriesItemStyleOption<TCbParams = never> extends ItemStyleOption<TCbParams> {
    borderRadius?: number | number[]

    colorAlpha?: number
    colorSaturation?: number

    borderColorSaturation?: number

    gapWidth?: number
}

interface TreePathInfo {
    name: string
    dataIndex: number
    value: TreemapSeriesDataValue
}

interface TreemapSeriesCallbackDataParams extends CallbackDataParams {
    /**
     * @deprecated
     */
    treePathInfo?: TreePathInfo[]

    treeAncestors?: TreePathInfo[]
}

interface ExtraStateOption {
    emphasis?: {
        focus?: DefaultEmphasisFocus | 'descendant' | 'ancestor'
    }
}

export interface TreemapStateOption<TCbParams = never> {
    itemStyle?: TreemapSeriesItemStyleOption<TCbParams>
    label?: TreemapSeriesLabelOption
    upperLabel?: TreemapSeriesLabelOption
}

export interface TreemapSeriesVisualOption {
    /**
     * Which dimension will be applied with the visual properties.
     */
    visualDimension?: number | string

    /**
     * @deprecated Use colorBy instead
     */
    colorMappingBy?: 'value' | 'index' | 'id'

    visualMin?: number
    visualMax?: number

    colorAlpha?: number[] | 'none'
    colorSaturation?: number[] | 'none'
    // A color list for a level. Each node in the level will obtain a color from the color list.
    // Only support ColorString for interpolation.
    // color?: ColorString[]

    /**
     * A node will not be shown when its area size is smaller than this value (unit: px square).
     */
    visibleMin?: number
    /**
     * Children will not be shown when area size of a node is smaller than this value (unit: px square).
     */
    childrenVisibleMin?: number
}

export interface TreemapSeriesLevelOption extends TreemapSeriesVisualOption,
    TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> {

    color?: ColorString[] | 'none',
    decal?: DecalObject[] | 'none'
}

export interface TreemapSeriesNodeItemOption extends TreemapSeriesVisualOption,
    TreemapStateOption, StatesOptionMixin<TreemapStateOption, ExtraStateOption> {
    id?: OptionId
    name?: OptionName

    value?: TreemapSeriesDataValue

    children?: TreemapSeriesNodeItemOption[]

    color?: ColorString[] | 'none'

    decal?: DecalObject[] | 'none'
}

export interface TreemapSeriesOption
    extends SeriesOption<TreemapStateOption<TreemapSeriesCallbackDataParams>, ExtraStateOption>,
    TreemapStateOption<TreemapSeriesCallbackDataParams>,
    BoxLayoutOptionMixin,
    RoamOptionMixin,
    TreemapSeriesVisualOption {

    type?: 'treemap'

    /**
     * configuration in echarts2
     * @deprecated
     */
    size?: (number | string)[]

    /**
     * If sort in desc order.
     * Default to be desc. asc has strange effect
     */
    sort?: boolean | 'asc' | 'desc'

    /**
     * Size of clipped window when zooming. 'origin' or 'fullscreen'
     */
    clipWindow?: 'origin' | 'fullscreen'

    squareRatio?: number
    /**
     * Nodes on depth from root are regarded as leaves.
     * Count from zero (zero represents only view root).
     */
    leafDepth?: number

    drillDownIcon?: string

    /**
     * Be effective when using zoomToNode. Specify the proportion of the
     * target node area in the view area.
     */
    zoomToNodeRatio?: number
    /**
     * Leaf node click behaviour: 'zoomToNode', 'link', false.
     * If leafDepth is set and clicking a node which has children but
     * be on left depth, the behaviour would be changing root. Otherwise
     * use behaviour defined above.
     */
    nodeClick?: 'zoomToNode' | 'link' | false

    breadcrumb?: BoxLayoutOptionMixin & {
        show?: boolean
        height?: number

        emptyItemWidth?: number  // With of empty width
        itemStyle?: BreadcrumbItemStyleOption

        emphasis?: {
            disabled?: boolean
            focus?: DefaultEmphasisFocus
            blurScope?: BlurScope
            itemStyle?: BreadcrumbItemStyleOption
        }
    }

    levels?: TreemapSeriesLevelOption[]

    data?: TreemapSeriesNodeItemOption[]
}

class TreemapSeriesModel extends SeriesModel<TreemapSeriesOption> {

    static type = 'series.treemap';
    type = TreemapSeriesModel.type;

    static layoutMode = 'box' as const;

    preventUsingHoverLayer = true;

    layoutInfo: LayoutRect;

    designatedVisualItemStyle: TreemapSeriesItemStyleOption;

    private _viewRoot: TreeNode;
    private _idIndexMap: zrUtil.HashMap<number>;
    private _idIndexMapCount: number;

    zoom: number;
    zoomLimit: {
        max?: number;
        min?: number;
    };

    static defaultOption: TreemapSeriesOption = {
        // Disable progressive rendering
        progressive: 0,
        // size: ['80%', '80%'],            // deprecated, compatible with ec2.
        left: 'center',
        top: 'middle',
        width: '80%',
        height: '80%',
        sort: true,

        clipWindow: 'origin',
        squareRatio: 0.5 * (1 + Math.sqrt(5)), // golden ratio
        leafDepth: null,

        drillDownIcon: '▶',                 // Use html character temporarily because it is complicated
                                            // to align specialized icon. ▷▶❒❐▼✚

        zoomToNodeRatio: 0.32 * 0.32,

        scaleLimit: null,

        roam: true,
        nodeClick: 'zoomToNode',
        animation: true,
        animationDurationUpdate: 900,
        animationEasing: 'quinticInOut',
        breadcrumb: {
            show: true,
            height: 22,
            left: 'center',
            top: 'bottom',
            // right
            // bottom
            emptyItemWidth: 25,             // Width of empty node.
            itemStyle: {
                color: 'rgba(0,0,0,0.7)', // '#5793f3',
                textStyle: {
                    color: '#fff'
                }
            },
            emphasis: {
                itemStyle: {
                    color: 'rgba(0,0,0,0.9)' // '#5793f3',
                }
            }
        },
        label: {
            show: true,
            // Do not use textDistance, for ellipsis rect just the same as treemap node rect.
            distance: 0,
            padding: 5,
            position: 'inside', // Can be [5, '5%'] or position string like 'insideTopLeft', ...
            // formatter: null,
            color: '#fff',
            overflow: 'truncate'
            // align
            // verticalAlign
        },
        upperLabel: {                   // Label when node is parent.
            show: false,
            position: [0, '50%'],
            height: 20,
            // formatter: null,
            // color: '#fff',
            overflow: 'truncate',
            // align: null,
            verticalAlign: 'middle'
        },
        itemStyle: {
            color: null,            // Can be 'none' if not necessary.
            colorAlpha: null,       // Can be 'none' if not necessary.
            colorSaturation: null,  // Can be 'none' if not necessary.
            borderWidth: 0,
            gapWidth: 0,
            borderColor: '#fff',
            borderColorSaturation: null // If specified, borderColor will be ineffective, and the
                                        // border color is evaluated by color of current node and
                                        // borderColorSaturation.
        },
        emphasis: {
            upperLabel: {
                show: true,
                position: [0, '50%'],
                overflow: 'truncate',
                verticalAlign: 'middle'
            }
        },

        visualDimension: 0,                 // Can be 0, 1, 2, 3.
        visualMin: null,
        visualMax: null,

        color: [],                  // + treemapSeries.color should not be modified. Please only modified
                                    // level[n].color (if necessary).
                                    // + Specify color list of each level. level[0].color would be global
                                    // color list if not specified. (see method `setDefault`).
                                    // + But set as a empty array to forbid fetch color from global palette
                                    // when using nodeModel.get('color'), otherwise nodes on deep level
                                    // will always has color palette set and are not able to inherit color
                                    // from parent node.
                                    // + TreemapSeries.color can not be set as 'none', otherwise effect
                                    // legend color fetching (see seriesColor.js).
        colorAlpha: null,           // Array. Specify color alpha range of each level, like [0.2, 0.8]
        colorSaturation: null,      // Array. Specify color saturation of each level, like [0.2, 0.5]
        colorMappingBy: 'index',    // 'value' or 'index' or 'id'.
        visibleMin: 10,             // If area less than this threshold (unit: pixel^2), node will not
                                    // be rendered. Only works when sort is 'asc' or 'desc'.
        childrenVisibleMin: null,   // If area of a node less than this threshold (unit: pixel^2),
                                    // grandchildren will not show.
                                    // Why grandchildren? If not grandchildren but children,
                                    // some siblings show children and some not,
                                    // the appearance may be mess and not consistent,
        levels: []                  // Each item: {
                                    //     visibleMin, itemStyle, visualDimension, label
                                    // }
    };

    /**
     * @override
     */
    getInitialData(option: TreemapSeriesOption, ecModel: GlobalModel) {
        // Create a virtual root.
        const root: TreemapSeriesNodeItemOption = {
            name: option.name,
            children: option.data
        };

        completeTreeValue(root);

        let levels = option.levels || [];

        // Used in "visual priority" in `treemapVisual.js`.
        // This way is a little tricky, must satisfy the precondition:
        //   1. There is no `treeNode.getModel('itemStyle.xxx')` used.
        //   2. The `Model.prototype.getModel()` will not use any clone-like way.
        const designatedVisualItemStyle = this.designatedVisualItemStyle = {};
        const designatedVisualModel = new Model({itemStyle: designatedVisualItemStyle}, this, ecModel);

        levels = option.levels = setDefault(levels, ecModel);
        const levelModels = zrUtil.map(levels || [], function (levelDefine) {
            return new Model(levelDefine, designatedVisualModel, ecModel);
        }, this);

        // Make sure always a new tree is created when setOption,
        // in TreemapView, we check whether oldTree === newTree
        // to choose mappings approach among old shapes and new shapes.
        const tree = Tree.createTree(root, this, beforeLink);

        function beforeLink(nodeData: SeriesData) {
            nodeData.wrapMethod('getItemModel', function (model, idx) {
                const node = tree.getNodeByDataIndex(idx);
                const levelModel = node ? levelModels[node.depth] : null;
                // If no levelModel, we also need `designatedVisualModel`.
                model.parentModel = levelModel || designatedVisualModel;
                return model;
            });
        }

        return tree.data;
    }

    optionUpdated() {
        this.resetViewRoot();
    }

    /**
     * @override
     * @param {number} dataIndex
     * @param {boolean} [mutipleSeries=false]
     */
    formatTooltip(
        dataIndex: number,
        multipleSeries: boolean,
        dataType: string
    ) {
        const data = this.getData();
        const value = this.getRawValue(dataIndex) as TreemapSeriesDataValue;
        const name = data.getName(dataIndex);

        return createTooltipMarkup('nameValue', { name: name, value: value });
    }

    /**
     * Add tree path to tooltip param
     *
     * @override
     * @param {number} dataIndex
     * @return {Object}
     */
    getDataParams(dataIndex: number) {
        const params = super.getDataParams.apply(this, arguments as any) as TreemapSeriesCallbackDataParams;

        const node = this.getData().tree.getNodeByDataIndex(dataIndex);
        params.treeAncestors = wrapTreePathInfo(node, this);
        // compatitable the previous code.
        params.treePathInfo = params.treeAncestors;

        return params;
    }

    /**
     * @public
     * @param {Object} layoutInfo {
     *                                x: containerGroup x
     *                                y: containerGroup y
     *                                width: containerGroup width
     *                                height: containerGroup height
     *                            }
     */
    setLayoutInfo(layoutInfo: LayoutRect) {
        /**
         * @readOnly
         * @type {Object}
         */
        this.layoutInfo = this.layoutInfo || {} as LayoutRect;
        zrUtil.extend(this.layoutInfo, layoutInfo);
    }

    /**
     * @param  {string} id
     * @return {number} index
     */
    mapIdToIndex(id: string): number {
        // A feature is implemented:
        // index is monotone increasing with the sequence of
        // input id at the first time.
        // This feature can make sure that each data item and its
        // mapped color have the same index between data list and
        // color list at the beginning, which is useful for user
        // to adjust data-color mapping.

        /**
         * @private
         * @type {Object}
         */
        let idIndexMap = this._idIndexMap;

        if (!idIndexMap) {
            idIndexMap = this._idIndexMap = zrUtil.createHashMap();
            /**
             * @private
             * @type {number}
             */
            this._idIndexMapCount = 0;
        }

        let index = idIndexMap.get(id);
        if (index == null) {
            idIndexMap.set(id, index = this._idIndexMapCount++);
        }

        return index;
    }

    getViewRoot() {
        return this._viewRoot;
    }

    resetViewRoot(viewRoot?: TreeNode) {
        viewRoot
            ? (this._viewRoot = viewRoot)
            : (viewRoot = this._viewRoot);

        const root = this.getRawData().tree.root;

        if (!viewRoot
            || (viewRoot !== root && !root.contains(viewRoot))
        ) {
            this._viewRoot = root;
        }
    }

    enableAriaDecal() {
        enableAriaDecalForTree(this);
    }
}

/**
 * @param {Object} dataNode
 */
function completeTreeValue(dataNode: TreemapSeriesNodeItemOption) {
    // Postorder travel tree.
    // If value of none-leaf node is not set,
    // calculate it by suming up the value of all children.
    let sum = 0;

    zrUtil.each(dataNode.children, function (child) {

        completeTreeValue(child);

        let childValue = child.value;
        zrUtil.isArray(childValue) && (childValue = childValue[0]);

        sum += childValue;
    });

    let thisValue = dataNode.value;
    if (zrUtil.isArray(thisValue)) {
        thisValue = thisValue[0];
    }

    if (thisValue == null || isNaN(thisValue)) {
        thisValue = sum;
    }
    // Value should not less than 0.
    if (thisValue < 0) {
        thisValue = 0;
    }

    zrUtil.isArray(dataNode.value)
        ? (dataNode.value[0] = thisValue)
        : (dataNode.value = thisValue);
}

/**
 * set default to level configuration
 */
function setDefault(levels: TreemapSeriesLevelOption[], ecModel: GlobalModel) {
    const globalColorList = normalizeToArray(ecModel.get('color')) as ColorString[];
    const globalDecalList = normalizeToArray(
        (ecModel as Model<AriaOptionMixin>).get(['aria', 'decal', 'decals'])
    ) as DecalObject[];

    if (!globalColorList) {
        return;
    }

    levels = levels || [];
    let hasColorDefine;
    let hasDecalDefine;
    zrUtil.each(levels, function (levelDefine) {
        const model = new Model(levelDefine);
        const modelColor = model.get('color');
        const modelDecal = model.get('decal');

        if (model.get(['itemStyle', 'color'])
            || (modelColor && modelColor !== 'none')
        ) {
            hasColorDefine = true;
        }
        if (model.get(['itemStyle', 'decal'])
            || (modelDecal && modelDecal !== 'none')
        ) {
            hasDecalDefine = true;
        }
    });

    const level0 = levels[0] || (levels[0] = {});
    if (!hasColorDefine) {
        level0.color = globalColorList.slice();
    }
    if (!hasDecalDefine && globalDecalList) {
        level0.decal = globalDecalList.slice();
    }

    return levels;
}

export default TreemapSeriesModel;
