/*
* 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 SeriesModel from '../../model/Series';
import Tree from '../../data/Tree';
import {
    SeriesOption,
    SymbolOptionMixin,
    BoxLayoutOptionMixin,
    RoamOptionMixin,
    LineStyleOption,
    ItemStyleOption,
    SeriesLabelOption,
    OptionDataValue,
    StatesOptionMixin,
    OptionDataItemObject,
    CallbackDataParams,
    DefaultEmphasisFocus,
    ComponentOnCalendarOptionMixin,
    ComponentOnMatrixOptionMixin,
    RoamHostModel
} from '../../util/types';
import SeriesData from '../../data/SeriesData';
import View from '../../coord/View';
import { LayoutRect } from '../../util/layout';
import Model from '../../model/Model';
import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';
import { wrapTreePathInfo } from '../helper/treeHelper';
import tokens from '../../visual/tokens';

interface CurveLineStyleOption extends LineStyleOption{
    curveness?: number
}

export interface TreeSeriesStateOption<TCbParams = never> {
    itemStyle?: ItemStyleOption<TCbParams>
    /**
     * Line style of the edge between node and it's parent.
     */
    lineStyle?: CurveLineStyleOption
    label?: SeriesLabelOption
}

interface TreeStatesMixin {
    emphasis?: {
        focus?: DefaultEmphasisFocus | 'ancestor' | 'descendant' | 'relative'
        scale?: boolean
    }
}

export interface TreeSeriesNodeItemOption extends SymbolOptionMixin<CallbackDataParams>,
    TreeSeriesStateOption<CallbackDataParams>,
    StatesOptionMixin<TreeSeriesStateOption<CallbackDataParams>, TreeStatesMixin>,
    OptionDataItemObject<OptionDataValue> {

    children?: TreeSeriesNodeItemOption[]

    collapsed?: boolean

    link?: string
    target?: string
}

/**
 * Configuration of leaves nodes.
 */
export interface TreeSeriesLeavesOption
    extends TreeSeriesStateOption, StatesOptionMixin<TreeSeriesStateOption, TreeStatesMixin> {
}

export interface TreeSeriesOption extends
    SeriesOption<TreeSeriesStateOption, TreeStatesMixin>, TreeSeriesStateOption,
    ComponentOnCalendarOptionMixin, ComponentOnMatrixOptionMixin,
    SymbolOptionMixin<CallbackDataParams>, BoxLayoutOptionMixin, RoamOptionMixin {
    type?: 'tree'

    layout?: 'orthogonal' | 'radial'

    edgeShape?: 'polyline' | 'curve'

    /**
     * Available when edgeShape is polyline
     */
    edgeForkPosition?: string | number

    /**
     * The orient of orthoginal layout, can be setted to 'LR', 'TB', 'RL', 'BT'.
     * and the backward compatibility configuration 'horizontal = LR', 'vertical = TB'.
     */
    orient?: 'LR' | 'TB' | 'RL' | 'BT' | 'horizontal' | 'vertical'

    expandAndCollapse?: boolean

    /**
     * The initial expanded depth of tree
     */
    initialTreeDepth?: number

    leaves?: TreeSeriesLeavesOption

    data?: TreeSeriesNodeItemOption[]
}

export interface TreeAncestors {
    name: string
    dataIndex: number
    value: number
}

export interface TreeSeriesCallbackDataParams extends CallbackDataParams {
    collapsed: boolean;
    treeAncestors?: TreeAncestors[]
}

export const SERIES_TYPE_TREE = 'tree';

class TreeSeriesModel extends SeriesModel<TreeSeriesOption> implements RoamHostModel {
    static readonly type = 'series.' + SERIES_TYPE_TREE;
    readonly type = TreeSeriesModel.type;

    // can support the position parameters 'left', 'top','right','bottom', 'width',
    // 'height' in the setOption() with 'merge' mode normal.
    static readonly layoutMode = 'box';

    coordinateSystem: View;

    layoutInfo: LayoutRect;

    hasSymbolVisual = true;

    // Do it self.
    ignoreStyleOnData = true;

    /**
     * Init a tree data structure from data in option series
     */
    getInitialData(option: TreeSeriesOption): SeriesData {

        // create a virtual root
        const root: TreeSeriesNodeItemOption = {
            name: option.name,
            children: option.data
        };

        const leaves = option.leaves || {};
        const leavesModel = new Model(leaves, this, this.ecModel);

        const tree = Tree.createTree(root, this, beforeLink);

        function beforeLink(nodeData: SeriesData) {
            nodeData.wrapMethod('getItemModel', function (model, idx) {
                const node = tree.getNodeByDataIndex(idx);
                if (!(node && node.children.length && node.isExpand)) {
                    model.parentModel = leavesModel;
                }
                return model;
            });
        }

        let treeDepth = 0;

        tree.eachNode('preorder', function (node) {
            if (node.depth > treeDepth) {
                treeDepth = node.depth;
            }
        });

        const expandAndCollapse = option.expandAndCollapse;
        const expandTreeDepth = (expandAndCollapse && option.initialTreeDepth >= 0)
            ? option.initialTreeDepth : treeDepth;

        tree.root.eachNode('preorder', function (node) {
            const item = node.hostTree.data.getRawDataItem(node.dataIndex) as TreeSeriesNodeItemOption;
            // Add item.collapsed != null, because users can collapse node original in the series.data.
            node.isExpand = (item && item.collapsed != null)
                ? !item.collapsed
                : node.depth <= expandTreeDepth;
        });

        return tree.data;
    }

    /**
     * Make the configuration 'orient' backward compatibly, with 'horizontal = LR', 'vertical = TB'.
     * @returns {string} orient
     */
    getOrient() {
        let orient = this.get('orient');
        if (orient === 'horizontal') {
            orient = 'LR';
        }
        else if (orient === 'vertical') {
            orient = 'TB';
        }
        return orient;
    }

    formatTooltip(
        dataIndex: number,
        multipleSeries: boolean,
        dataType: string
    ) {
        const tree = this.getData().tree;
        const realRoot = tree.root.children[0];
        let node = tree.getNodeByDataIndex(dataIndex);
        const value = node.getValue();
        let name = node.name;
        while (node && (node !== realRoot)) {
            name = node.parentNode.name + '.' + name;
            node = node.parentNode;
        }

        return createTooltipMarkup('nameValue', {
            name: name,
            value: value,
            noValue: isNaN(value as number) || value == null
        });
    }

    // Add tree path to tooltip param
    getDataParams(dataIndex: number) {
        const params = super.getDataParams.apply(this, arguments as any) as TreeSeriesCallbackDataParams;

        const node = this.getData().tree.getNodeByDataIndex(dataIndex);
        params.treeAncestors = wrapTreePathInfo(node, this);
        params.collapsed = !node.isExpand;

        return params;
    }

    __ownRoamView() {
        return this.coordinateSystem;
    }

    static defaultOption: TreeSeriesOption = {
        // zlevel: 0,
        z: 2,

        // `coordinateSystem` can be declared as 'matrix', 'calendar',
        //  which provides box layout container.
        coordinateSystemUsage: 'box',

        // the position of the whole view
        left: '12%',
        top: '12%',
        right: '12%',
        bottom: '12%',

        // the layout of the tree, two value can be selected, 'orthogonal' or 'radial'
        layout: 'orthogonal',

        // value can be 'polyline'
        edgeShape: 'curve',

        edgeForkPosition: '50%',

        // true | false | 'move' | 'scale', see module:component/helper/RoamController.
        roam: false,
        roamTrigger: 'global',

        // Symbol size scale ratio in roam
        nodeScaleRatio: 0.4,

        // Default on center of graph
        center: null,

        zoom: 1,

        orient: 'LR',

        symbol: 'emptyCircle',

        symbolSize: 7,

        expandAndCollapse: true,

        initialTreeDepth: 2,

        lineStyle: {
            color: tokens.color.borderTint,
            width: 1.5,
            curveness: 0.5
        },

        itemStyle: {
            color: 'lightsteelblue',
            // borderColor: '#c23531',
            borderWidth: 1.5
        },

        label: {
            show: true
        },

        animationEasing: 'linear',

        animationDuration: 700,

        animationDurationUpdate: 500
    };
}

export default TreeSeriesModel;