/*
* 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 { isArray, each } from 'zrender/src/core/util';
import * as vector from 'zrender/src/core/vector';
import * as symbolUtil from '../../util/symbol';
import ECLinePath from './LinePath';
import * as graphic from '../../util/graphic';
import { toggleHoverEmphasis, enterEmphasis, leaveEmphasis, SPECIAL_STATES } from '../../util/states';
import {getLabelStatesModels, setLabelStyle} from '../../label/labelStyle';
import {round} from '../../util/number';
import SeriesData from '../../data/SeriesData';
import {
    ZRTextAlign,
    ZRTextVerticalAlign,
    LineLabelOption,
    ColorString,
    DefaultEmphasisFocus,
    BlurScope
} from '../../util/types';
import SeriesModel from '../../model/Series';
import type { LineDrawSeriesScope, LineDrawModelOption } from './LineDraw';
import { TextStyleProps } from 'zrender/src/graphic/Text';
import { LineDataVisual } from '../../visual/commonVisualTypes';
import Model from '../../model/Model';

const SYMBOL_CATEGORIES = ['fromSymbol', 'toSymbol'] as const;

type ECSymbol = ReturnType<typeof createSymbol>;

type LineECSymbol = ECSymbol & {
    __specifiedRotation: number
};

type LineList = SeriesData<SeriesModel, LineDataVisual>;

export interface LineLabel extends graphic.Text {
    lineLabelOriginalOpacity: number
}

interface InnerLineLabel extends LineLabel {
    __align: TextStyleProps['align']
    __verticalAlign: TextStyleProps['verticalAlign']
    __position: LineLabelOption['position']
    __labelDistance: number[]
}

function makeSymbolTypeKey(symbolCategory: 'fromSymbol' | 'toSymbol') {
    return '_' + symbolCategory + 'Type' as '_fromSymbolType' | '_toSymbolType';
}
function makeSymbolTypeValue(name: 'fromSymbol' | 'toSymbol', lineData: LineList, idx: number) {
    const symbolType = lineData.getItemVisual(idx, name);
    if (!symbolType || symbolType === 'none') {
        return symbolType;
    }
    const symbolSize = lineData.getItemVisual(idx, name + 'Size' as 'fromSymbolSize' | 'toSymbolSize');
    const symbolRotate = lineData.getItemVisual(idx, name + 'Rotate' as 'fromSymbolRotate' | 'toSymbolRotate');
    const symbolOffset = lineData.getItemVisual(idx, name + 'Offset' as 'fromSymbolOffset' | 'toSymbolOffset');
    const symbolKeepAspect = lineData.getItemVisual(idx,
        name + 'KeepAspect' as 'fromSymbolKeepAspect' | 'toSymbolKeepAspect');
    const symbolSizeArr = symbolUtil.normalizeSymbolSize(symbolSize);

    const symbolOffsetArr = symbolUtil.normalizeSymbolOffset(symbolOffset || 0, symbolSizeArr);

    return symbolType + symbolSizeArr + symbolOffsetArr + (symbolRotate || '') + (symbolKeepAspect || '');
}

/**
 * @inner
 */
function createSymbol(name: 'fromSymbol' | 'toSymbol', lineData: LineList, idx: number) {
    const symbolType = lineData.getItemVisual(idx, name);
    if (!symbolType || symbolType === 'none') {
        return;
    }

    const symbolSize = lineData.getItemVisual(idx, name + 'Size' as 'fromSymbolSize' | 'toSymbolSize');
    const symbolRotate = lineData.getItemVisual(idx, name + 'Rotate' as 'fromSymbolRotate' | 'toSymbolRotate');
    const symbolOffset = lineData.getItemVisual(idx, name + 'Offset' as 'fromSymbolOffset' | 'toSymbolOffset');
    const symbolKeepAspect = lineData.getItemVisual(idx,
        name + 'KeepAspect' as 'fromSymbolKeepAspect' | 'toSymbolKeepAspect');

    const symbolSizeArr = symbolUtil.normalizeSymbolSize(symbolSize);

    const symbolOffsetArr = symbolUtil.normalizeSymbolOffset(symbolOffset || 0, symbolSizeArr);

    const symbolPath = symbolUtil.createSymbol(
        symbolType,
        -symbolSizeArr[0] / 2 + (symbolOffsetArr as number[])[0],
        -symbolSizeArr[1] / 2 + (symbolOffsetArr as number[])[1],
        symbolSizeArr[0],
        symbolSizeArr[1],
        null,
        symbolKeepAspect
    );

    (symbolPath as LineECSymbol).__specifiedRotation = symbolRotate == null || isNaN(symbolRotate)
        ? void 0
        : +symbolRotate * Math.PI / 180 || 0;

    symbolPath.name = name;

    return symbolPath;
}

function createLine(points: number[][]) {
    const line = new ECLinePath({
        name: 'line',
        subPixelOptimize: true
    });
    setLinePoints(line.shape, points);
    return line;
}

function setLinePoints(targetShape: ECLinePath['shape'], points: number[][]) {
    type CurveShape = ECLinePath['shape'] & {
        cpx1: number
        cpy1: number
    };

    targetShape.x1 = points[0][0];
    targetShape.y1 = points[0][1];
    targetShape.x2 = points[1][0];
    targetShape.y2 = points[1][1];
    targetShape.percent = 1;

    const cp1 = points[2];
    if (cp1) {
        (targetShape as CurveShape).cpx1 = cp1[0];
        (targetShape as CurveShape).cpy1 = cp1[1];
    }
    else {
        (targetShape as CurveShape).cpx1 = NaN;
        (targetShape as CurveShape).cpy1 = NaN;
    }
}

class Line extends graphic.Group {

    private _fromSymbolType: string;
    private _toSymbolType: string;

    constructor(lineData: SeriesData, idx: number, seriesScope?: LineDrawSeriesScope) {
        super();
        this._createLine(lineData as LineList, idx, seriesScope);
    }

    _createLine(lineData: LineList, idx: number, seriesScope?: LineDrawSeriesScope) {
        const seriesModel = lineData.hostModel;
        const linePoints = lineData.getItemLayout(idx);
        const line = createLine(linePoints);
        line.shape.percent = 0;
        graphic.initProps(line, {
            shape: {
                percent: 1
            }
        }, seriesModel, idx);

        this.add(line);

        each(SYMBOL_CATEGORIES, function (symbolCategory) {
            const symbol = createSymbol(symbolCategory, lineData, idx);
            // symbols must added after line to make sure
            // it will be updated after line#update.
            // Or symbol position and rotation update in line#beforeUpdate will be one frame slow
            this.add(symbol);
            this[makeSymbolTypeKey(symbolCategory)] = makeSymbolTypeValue(symbolCategory, lineData, idx);
        }, this);

        this._updateCommonStl(lineData, idx, seriesScope);
    }

    // TODO More strict on the List type in parameters?
    updateData(lineData: SeriesData, idx: number, seriesScope: LineDrawSeriesScope) {
        const seriesModel = lineData.hostModel;

        const line = this.childOfName('line') as ECLinePath;
        const linePoints = lineData.getItemLayout(idx);
        const target = {
            shape: {} as ECLinePath['shape']
        };

        setLinePoints(target.shape, linePoints);
        graphic.updateProps(line, target, seriesModel, idx);

        each(SYMBOL_CATEGORIES, function (symbolCategory) {
            const symbolType = makeSymbolTypeValue(symbolCategory, lineData as LineList, idx);
            const key = makeSymbolTypeKey(symbolCategory);
            // Symbol changed
            if (this[key] !== symbolType) {
                this.remove(this.childOfName(symbolCategory));
                const symbol = createSymbol(symbolCategory, lineData as LineList, idx);
                this.add(symbol);
            }
            this[key] = symbolType;
        }, this);

        this._updateCommonStl(lineData, idx, seriesScope);
    };

    getLinePath() {
        return this.childAt(0) as graphic.Line;
    }

    _updateCommonStl(lineData: SeriesData, idx: number, seriesScope?: LineDrawSeriesScope) {
        const seriesModel = lineData.hostModel as SeriesModel;

        const line = this.childOfName('line') as ECLinePath;

        let emphasisLineStyle = seriesScope && seriesScope.emphasisLineStyle;
        let blurLineStyle = seriesScope && seriesScope.blurLineStyle;
        let selectLineStyle = seriesScope && seriesScope.selectLineStyle;

        let labelStatesModels = seriesScope && seriesScope.labelStatesModels;

        let emphasisDisabled = seriesScope && seriesScope.emphasisDisabled;
        let focus = (seriesScope && seriesScope.focus) as DefaultEmphasisFocus;
        let blurScope = (seriesScope && seriesScope.blurScope) as BlurScope;

        // Optimization for large dataset
        if (!seriesScope || lineData.hasItemOption) {
            const itemModel = lineData.getItemModel<LineDrawModelOption>(idx);
            const emphasisModel = itemModel.getModel('emphasis');

            emphasisLineStyle = emphasisModel.getModel('lineStyle').getLineStyle();
            blurLineStyle = itemModel.getModel(['blur', 'lineStyle']).getLineStyle();
            selectLineStyle = itemModel.getModel(['select', 'lineStyle']).getLineStyle();
            emphasisDisabled = emphasisModel.get('disabled');
            focus = emphasisModel.get('focus');
            blurScope = emphasisModel.get('blurScope');

            labelStatesModels = getLabelStatesModels(itemModel);
        }

        const lineStyle = lineData.getItemVisual(idx, 'style');
        const visualColor = lineStyle.stroke;

        line.useStyle(lineStyle);
        line.style.fill = null;
        line.style.strokeNoScale = true;

        line.ensureState('emphasis').style = emphasisLineStyle;
        line.ensureState('blur').style = blurLineStyle;
        line.ensureState('select').style = selectLineStyle;

        // Update symbol
        each(SYMBOL_CATEGORIES, function (symbolCategory) {
            const symbol = this.childOfName(symbolCategory) as ECSymbol;
            if (symbol) {
                // Share opacity and color with line.
                symbol.setColor(visualColor);
                symbol.style.opacity = lineStyle.opacity;

                for (let i = 0; i < SPECIAL_STATES.length; i++) {
                    const stateName = SPECIAL_STATES[i];
                    const lineState = line.getState(stateName);
                    if (lineState) {
                        const lineStateStyle = lineState.style || {};
                        const state = symbol.ensureState(stateName);
                        const stateStyle = state.style || (state.style = {});
                        if (lineStateStyle.stroke != null) {
                            stateStyle[symbol.__isEmptyBrush ? 'stroke' : 'fill'] = lineStateStyle.stroke;
                        }
                        if (lineStateStyle.opacity != null) {
                            stateStyle.opacity = lineStateStyle.opacity;
                        }
                    }
                }

                symbol.markRedraw();
            }
        }, this);

        const rawVal = seriesModel.getRawValue(idx) as number;
        setLabelStyle(this, labelStatesModels, {
            labelDataIndex: idx,
            labelFetcher: {
                getFormattedLabel(dataIndex, stateName) {
                    return seriesModel.getFormattedLabel(dataIndex, stateName, lineData.dataType);
                }
            },
            inheritColor: visualColor as ColorString || '#000',
            defaultOpacity: lineStyle.opacity,
            defaultText: (rawVal == null
                ? lineData.getName(idx)
                : isFinite(rawVal)
                ? round(rawVal)
                : rawVal) + ''
        });
        const label = this.getTextContent() as InnerLineLabel;

        // Always set `textStyle` even if `normalStyle.text` is null, because default
        // values have to be set on `normalStyle`.
        if (label) {
            const labelNormalModel = labelStatesModels.normal as unknown as Model<LineLabelOption>;
            label.__align = label.style.align;
            label.__verticalAlign = label.style.verticalAlign;
            // 'start', 'middle', 'end'
            label.__position = labelNormalModel.get('position') || 'middle';

            let distance = labelNormalModel.get('distance');
            if (!isArray(distance)) {
                distance = [distance, distance];
            }
            label.__labelDistance = distance;
        }

        this.setTextConfig({
            position: null,
            local: true,
            inside: false   // Can't be inside for stroke element.
        });

        toggleHoverEmphasis(this, focus, blurScope, emphasisDisabled);
    }

    highlight() {
        enterEmphasis(this);
    }

    downplay() {
        leaveEmphasis(this);
    }

    updateLayout(lineData: SeriesData, idx: number) {
        this.setLinePoints(lineData.getItemLayout(idx));
    }

    setLinePoints(points: number[][]) {
        const linePath = this.childOfName('line') as ECLinePath;
        setLinePoints(linePath.shape, points);
        linePath.dirty();
    }

    beforeUpdate() {
        const lineGroup = this;
        const symbolFrom = lineGroup.childOfName('fromSymbol') as ECSymbol;
        const symbolTo = lineGroup.childOfName('toSymbol') as ECSymbol;
        const label = lineGroup.getTextContent() as InnerLineLabel;
        // Quick reject
        if (!symbolFrom && !symbolTo && (!label || label.ignore)) {
            return;
        }

        let invScale = 1;
        let parentNode = this.parent;
        while (parentNode) {
            if (parentNode.scaleX) {
                invScale /= parentNode.scaleX;
            }
            parentNode = parentNode.parent;
        }

        const line = lineGroup.childOfName('line') as ECLinePath;
        // If line not changed
        // FIXME Parent scale changed
        if (!this.__dirty && !line.__dirty) {
            return;
        }

        const percent = line.shape.percent;
        const fromPos = line.pointAt(0);
        const toPos = line.pointAt(percent);

        const d = vector.sub([], toPos, fromPos);
        vector.normalize(d, d);

        function setSymbolRotation(symbol: ECSymbol, percent: 0 | 1) {
            // Fix #12388
            // when symbol is set to be 'arrow' in markLine,
            // symbolRotate value will be ignored, and compulsively use tangent angle.
            // rotate by default if symbol rotation is not specified
            const specifiedRotation = (symbol as LineECSymbol).__specifiedRotation;
            if (specifiedRotation == null) {
                const tangent = line.tangentAt(percent);
                symbol.attr('rotation', (percent === 1 ? -1 : 1) * Math.PI / 2 - Math.atan2(
                    tangent[1], tangent[0]
                ));
            }
            else {
                symbol.attr('rotation', specifiedRotation);
            }
        }

        if (symbolFrom) {
            symbolFrom.setPosition(fromPos);
            setSymbolRotation(symbolFrom, 0);
            symbolFrom.scaleX = symbolFrom.scaleY = invScale * percent;
            symbolFrom.markRedraw();
        }
        if (symbolTo) {
            symbolTo.setPosition(toPos);
            setSymbolRotation(symbolTo, 1);
            symbolTo.scaleX = symbolTo.scaleY = invScale * percent;
            symbolTo.markRedraw();
        }

        if (label && !label.ignore) {
            label.x = label.y = 0;
            label.originX = label.originY = 0;

            let textAlign: ZRTextAlign;
            let textVerticalAlign: ZRTextVerticalAlign;

            const distance = label.__labelDistance;
            const distanceX = distance[0] * invScale;
            const distanceY = distance[1] * invScale;
            const halfPercent = percent / 2;
            const tangent = line.tangentAt(halfPercent);
            const n = [tangent[1], -tangent[0]];
            const cp = line.pointAt(halfPercent);
            if (n[1] > 0) {
                n[0] = -n[0];
                n[1] = -n[1];
            }
            const dir = tangent[0] < 0 ? -1 : 1;

            if (label.__position !== 'start' && label.__position !== 'end') {
                let rotation = -Math.atan2(tangent[1], tangent[0]);
                if (toPos[0] < fromPos[0]) {
                    rotation = Math.PI + rotation;
                }
                label.rotation = rotation;
            }

            let dy;
            switch (label.__position) {
                case 'insideStartTop':
                case 'insideMiddleTop':
                case 'insideEndTop':
                case 'middle':
                    dy = -distanceY;
                    textVerticalAlign = 'bottom';
                    break;

                case 'insideStartBottom':
                case 'insideMiddleBottom':
                case 'insideEndBottom':
                    dy = distanceY;
                    textVerticalAlign = 'top';
                    break;

                default:
                    dy = 0;
                    textVerticalAlign = 'middle';
            }

            switch (label.__position) {
                case 'end':
                    label.x = d[0] * distanceX + toPos[0];
                    label.y = d[1] * distanceY + toPos[1];
                    textAlign = d[0] > 0.8 ? 'left' : (d[0] < -0.8 ? 'right' : 'center');
                    textVerticalAlign = d[1] > 0.8 ? 'top' : (d[1] < -0.8 ? 'bottom' : 'middle');
                    break;

                case 'start':
                    label.x = -d[0] * distanceX + fromPos[0];
                    label.y = -d[1] * distanceY + fromPos[1];
                    textAlign = d[0] > 0.8 ? 'right' : (d[0] < -0.8 ? 'left' : 'center');
                    textVerticalAlign = d[1] > 0.8 ? 'bottom' : (d[1] < -0.8 ? 'top' : 'middle');
                    break;

                case 'insideStartTop':
                case 'insideStart':
                case 'insideStartBottom':
                    label.x = distanceX * dir + fromPos[0];
                    label.y = fromPos[1] + dy;
                    textAlign = tangent[0] < 0 ? 'right' : 'left';
                    label.originX = -distanceX * dir;
                    label.originY = -dy;
                    break;

                case 'insideMiddleTop':
                case 'insideMiddle':
                case 'insideMiddleBottom':
                case 'middle':
                    label.x = cp[0];
                    label.y = cp[1] + dy;
                    textAlign = 'center';
                    label.originY = -dy;
                    break;

                case 'insideEndTop':
                case 'insideEnd':
                case 'insideEndBottom':
                    label.x = -distanceX * dir + toPos[0];
                    label.y = toPos[1] + dy;
                    textAlign = tangent[0] >= 0 ? 'right' : 'left';
                    label.originX = distanceX * dir;
                    label.originY = -dy;
                    break;
            }

            label.scaleX = label.scaleY = invScale;
            label.setStyle({
                // Use the user specified text align and baseline first
                verticalAlign: label.__verticalAlign || textVerticalAlign,
                align: label.__align || textAlign
            });
        }
    }
}

export default Line;
