/*
* 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 createGraphFromNodeEdge from '../helper/createGraphFromNodeEdge';
import Model from '../../model/Model';
import {
    SeriesOption,
    BoxLayoutOptionMixin,
    OptionDataValue,
    SeriesLabelOption,
    ItemStyleOption,
    LineStyleOption,
    LayoutOrient,
    ColorString,
    StatesOptionMixin,
    OptionDataItemObject,
    GraphEdgeItemObject,
    OptionDataValueNumeric,
    DefaultEmphasisFocus,
    CallbackDataParams
} from '../../util/types';
import GlobalModel from '../../model/Global';
import SeriesData from '../../data/SeriesData';
import { LayoutRect } from '../../util/layout';
import { createTooltipMarkup } from '../../component/tooltip/tooltipMarkup';


type FocusNodeAdjacency = boolean | 'inEdges' | 'outEdges' | 'allEdges';

export interface SankeyNodeStateOption<TCbParams = never> {
    label?: SeriesLabelOption
    itemStyle?: ItemStyleOption<TCbParams>
}

export interface SankeyEdgeStateOption {
    lineStyle?: SankeyEdgeStyleOption
}

interface SankeyBothStateOption<TCbParams> extends SankeyNodeStateOption<TCbParams>, SankeyEdgeStateOption {}

interface SankeyEdgeStyleOption extends LineStyleOption {
    curveness?: number
}

interface ExtraStateOption {
    emphasis?: {
        focus?: DefaultEmphasisFocus | 'adjacency' | 'trajectory'
    }
}

export interface SankeyNodeItemOption extends SankeyNodeStateOption,
    StatesOptionMixin<SankeyNodeStateOption, ExtraStateOption>,
    OptionDataItemObject<OptionDataValue> {
    id?: string

    localX?: number
    localY?: number

    depth?: number

    draggable?: boolean

    focusNodeAdjacency?: FocusNodeAdjacency
}

export interface SankeyEdgeItemOption extends
        SankeyEdgeStateOption,
        StatesOptionMixin<SankeyEdgeStateOption, ExtraStateOption>,
        GraphEdgeItemObject<OptionDataValueNumeric> {
    focusNodeAdjacency?: FocusNodeAdjacency
    edgeLabel?: SeriesLabelOption
}

export interface SankeyLevelOption extends SankeyNodeStateOption, SankeyEdgeStateOption {
    depth: number
}

export interface SankeySeriesOption
    extends SeriesOption<SankeyBothStateOption<CallbackDataParams>, ExtraStateOption>,
    SankeyBothStateOption<CallbackDataParams>,
    BoxLayoutOptionMixin {
    type?: 'sankey'

    /**
     * color will be linear mapped.
     */
    color?: ColorString[]

    coordinateSystem?: 'view'

    orient?: LayoutOrient
    /**
     * The width of the node
     */
    nodeWidth?: number
    /**
     * The vertical distance between two nodes
     */
    nodeGap?: number

    /**
     * Control if the node can move or not
     */
    draggable?: boolean
    /**
     * Will be allEdges if true.
     * @deprecated
     */
    focusNodeAdjacency?: FocusNodeAdjacency
    /**
     * The number of iterations to change the position of the node
     */
    layoutIterations?: number

    nodeAlign?: 'justify' | 'left' | 'right'    // TODO justify should be auto

    data?: SankeyNodeItemOption[]
    nodes?: SankeyNodeItemOption[]

    edges?: SankeyEdgeItemOption[]
    links?: SankeyEdgeItemOption[]

    levels?: SankeyLevelOption[]

    edgeLabel?: SeriesLabelOption & {
        position?: 'inside'
    }
}

class SankeySeriesModel extends SeriesModel<SankeySeriesOption> {
    static readonly type = 'series.sankey';
    readonly type = SankeySeriesModel.type;

    levelModels: Model<SankeyLevelOption>[];

    layoutInfo: LayoutRect;

    /**
     * Init a graph data structure from data in option series
     */
    getInitialData(option: SankeySeriesOption, ecModel: GlobalModel) {
        const links = option.edges || option.links || [];
        const nodes = option.data || option.nodes || [];
        const levels = option.levels || [];
        this.levelModels = [];
        const levelModels = this.levelModels;

        for (let i = 0; i < levels.length; i++) {
            if (levels[i].depth != null && levels[i].depth >= 0) {
                levelModels[levels[i].depth] = new Model(levels[i], this, ecModel);
            }
            else {
                if (__DEV__) {
                    throw new Error('levels[i].depth is mandatory and should be natural number');
                }
            }
        }

        const graph = createGraphFromNodeEdge(nodes, links, this, true, beforeLink);
        return graph.data;

        function beforeLink(nodeData: SeriesData, edgeData: SeriesData) {
            nodeData.wrapMethod('getItemModel', function (model: Model, idx: number) {
                const seriesModel = model.parentModel as SankeySeriesModel;
                const layout = seriesModel.getData().getItemLayout(idx);
                if (layout) {
                    const nodeDepth = layout.depth;
                    const levelModel = seriesModel.levelModels[nodeDepth];
                    if (levelModel) {
                        model.parentModel = levelModel;
                    }
                }
                return model;
            });

            edgeData.wrapMethod('getItemModel', function (model: Model, idx: number) {
                const seriesModel = model.parentModel as SankeySeriesModel;
                const edge = seriesModel.getGraph().getEdgeByIndex(idx);
                const layout = edge.node1.getLayout();
                if (layout) {
                    const depth = layout.depth;
                    const levelModel = seriesModel.levelModels[depth];
                    if (levelModel) {
                        model.parentModel = levelModel;
                    }
                }
                return model;
            });
        }
    }

    setNodePosition(dataIndex: number, localPosition: number[]) {
        const nodes = this.option.data || this.option.nodes;
        const dataItem = nodes[dataIndex];
        dataItem.localX = localPosition[0];
        dataItem.localY = localPosition[1];
    }

    /**
     * Return the graphic data structure
     *
     * @return graphic data structure
     */
    getGraph() {
        return this.getData().graph;
    }

    /**
     * Get edge data of graphic data structure
     *
     * @return data structure of list
     */
    getEdgeData() {
        return this.getGraph().edgeData;
    }

    formatTooltip(
        dataIndex: number,
        multipleSeries: boolean,
        dataType: 'node' | 'edge'
    ) {
        function noValue(val: unknown): boolean {
            return isNaN(val as number) || val == null;
        }
        // dataType === 'node' or empty do not show tooltip by default
        if (dataType === 'edge') {
            const params = this.getDataParams(dataIndex, dataType);
            const rawDataOpt = params.data as SankeyEdgeItemOption;
            const edgeValue = params.value;
            const edgeName = rawDataOpt.source + ' -- ' + rawDataOpt.target;
            return createTooltipMarkup('nameValue', {
                name: edgeName,
                value: edgeValue,
                noValue: noValue(edgeValue)
            });
        }
        // dataType === 'node'
        else {
            const node = this.getGraph().getNodeByIndex(dataIndex);
            const value = node.getLayout().value;
            const name = (this.getDataParams(dataIndex, dataType).data as SankeyNodeItemOption).name;
            return createTooltipMarkup('nameValue', {
                name: name != null ? name + '' : null,
                value: value,
                noValue: noValue(value)
            });
        }
    }

    optionUpdated() {}

    // Override Series.getDataParams()
    getDataParams(dataIndex: number, dataType: 'node' | 'edge') {
        const params = super.getDataParams(dataIndex, dataType);
        if (params.value == null && dataType === 'node') {
            const node = this.getGraph().getNodeByIndex(dataIndex);
            const nodeValue = node.getLayout().value;
            params.value = nodeValue;
        }
        return params;
    }

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

        coordinateSystem: 'view',

        left: '5%',
        top: '5%',
        right: '20%',
        bottom: '5%',

        orient: 'horizontal',

        nodeWidth: 20,

        nodeGap: 8,
        draggable: true,

        layoutIterations: 32,

        label: {
            show: true,
            position: 'right',
            fontSize: 12
        },

        edgeLabel: {
            show: false,
            fontSize: 12
        },

        levels: [],

        nodeAlign: 'justify',

        lineStyle: {
            color: '#314656',
            opacity: 0.2,
            curveness: 0.5
        },

        emphasis: {
            label: {
                show: true
            },
            lineStyle: {
                opacity: 0.5
            }
        },

        select: {
            itemStyle: {
                borderColor: '#212121'
            }
        },

        animationEasing: 'linear',

        animationDuration: 1000
    };
}

export default SankeySeriesModel;
