/*
* 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 {
    separateMorph,
    combineMorph,
    morphPath,
    DividePath,
    isCombineMorphing,
    SeparateConfig
} from 'zrender/src/tool/morphPath';
import { Path } from '../util/graphic';
import SeriesModel from '../model/Series';
import Element, { ElementAnimateConfig } from 'zrender/src/Element';
import { defaults, extend, isArray} from 'zrender/src/core/util';
import { getAnimationConfig } from './basicTransition';
import { ECElement, UniversalTransitionOption } from '../util/types';
import { clonePath } from 'zrender/src/tool/path';
import Model from '../model/Model';


type DescendentElements = Element[];
type DescendentPaths = Path[];

function isMultiple(elements: DescendentElements | DescendentElements[]): elements is DescendentElements[] {
    return isArray(elements[0]);
}

interface MorphingBatch {
    one: Path;
    many: Path[];
}

function prepareMorphBatches(one: DescendentPaths, many: DescendentPaths[]) {
    const batches: MorphingBatch[] = [];
    const batchCount = one.length;
    for (let i = 0; i < batchCount; i++) {
        batches.push({
            one: one[i],
            many: []
        });
    }

    for (let i = 0; i < many.length; i++) {
        const len = many[i].length;
        let k;
        for (k = 0; k < len; k++) {
            batches[k % batchCount].many.push(many[i][k]);
        }
    }

    let off = 0;
    // If one has more paths than each one of many. average them.
    for (let i = batchCount - 1; i >= 0; i--) {
        if (!batches[i].many.length) {
            const moveFrom = batches[off].many;
            if (moveFrom.length <= 1) { // Not enough
                // Start from the first one.
                if (off) {
                    off = 0;
                }
                else {
                    return batches;
                }
            }
            const len = moveFrom.length;
            const mid = Math.ceil(len / 2);
            batches[i].many = moveFrom.slice(mid, len);
            batches[off].many = moveFrom.slice(0, mid);

            off++;
        }
    }

    return batches;
}

const pathDividers: Record<UniversalTransitionOption['divideShape'], DividePath> = {
    clone(params) {
        const ret: Path[] = [];
        // Fitting the alpha
        const approxOpacity = 1 - Math.pow(1 - params.path.style.opacity, 1 / params.count);
        for (let i = 0; i < params.count; i++) {
            const cloned = clonePath(params.path);
            cloned.setStyle('opacity', approxOpacity);
            ret.push(cloned);
        }
        return ret;
    },
    // Use the default divider
    split: null
};

export function applyMorphAnimation(
    from: DescendentPaths | DescendentPaths[],
    to: DescendentPaths | DescendentPaths[],
    divideShape: UniversalTransitionOption['divideShape'],
    seriesModel: SeriesModel,
    dataIndex: number,
    animateOtherProps: (
        fromIndividual: Path,
        toIndividual: Path,
        rawFrom: Path,
        rawTo: Path,
        animationCfg: ElementAnimateConfig
    ) => void
) {
    if (!from.length || !to.length) {
        return;
    }

    const updateAnimationCfg = getAnimationConfig('update', seriesModel, dataIndex);
    if (!(updateAnimationCfg && updateAnimationCfg.duration > 0)) {
        return;
    }
    const animationDelay = (seriesModel.getModel('universalTransition') as Model<UniversalTransitionOption>)
        .get('delay');


    const animationCfg = extend({
        // Need to setToFinal so the further calculation based on the style can be correct.
        // Like emphasis color.
        setToFinal: true
    } as SeparateConfig, updateAnimationCfg);


    let many: DescendentPaths[];
    let one: DescendentPaths;
    if (isMultiple(from)) {    // manyToOne
        many = from;
        one = to as DescendentPaths;
    }
    if (isMultiple(to)) { // oneToMany
        many = to;
        one = from as DescendentPaths;
    }

    function morphOneBatch(
        batch: MorphingBatch,
        fromIsMany: boolean,
        animateIndex: number,
        animateCount: number,
        forceManyOne?: boolean
    ) {
        const batchMany = batch.many;
        const batchOne = batch.one;
        if (batchMany.length === 1 && !forceManyOne) {
            // Is one to one
            const batchFrom: Path = fromIsMany ? batchMany[0] : batchOne;
            const batchTo: Path = fromIsMany ? batchOne : batchMany[0];

            if (isCombineMorphing(batchFrom as Path)) {
                // Keep doing combine animation.
                morphOneBatch({
                    many: [batchFrom as Path],
                    one: batchTo as Path
                }, true, animateIndex, animateCount, true);
            }
            else {
                const individualAnimationCfg = animationDelay ? defaults({
                    delay: animationDelay(animateIndex, animateCount)
                } as ElementAnimateConfig, animationCfg) : animationCfg;
                morphPath(batchFrom, batchTo, individualAnimationCfg);
                animateOtherProps(batchFrom, batchTo, batchFrom, batchTo, individualAnimationCfg);
            }
        }
        else {
            const separateAnimationCfg = defaults({
                dividePath: pathDividers[divideShape],
                individualDelay: animationDelay && function (idx, count, fromPath, toPath) {
                    return animationDelay(idx + animateIndex, animateCount);
                }
            } as SeparateConfig, animationCfg);

            const {
                fromIndividuals,
                toIndividuals
            } = fromIsMany
                ? combineMorph(batchMany, batchOne, separateAnimationCfg)
                : separateMorph(batchOne, batchMany, separateAnimationCfg);

            const count = fromIndividuals.length;
            for (let k = 0; k < count; k++) {
                const individualAnimationCfg = animationDelay ? defaults({
                    delay: animationDelay(k, count)
                } as ElementAnimateConfig, animationCfg) : animationCfg;
                animateOtherProps(
                    fromIndividuals[k],
                    toIndividuals[k],
                    fromIsMany ? batchMany[k] : batch.one,
                    fromIsMany ? batch.one : batchMany[k],
                    individualAnimationCfg
                );
            }
        }
    }

    const fromIsMany = many
        ? many === from
        // Is one to one. If the path number not match. also needs do merge and separate morphing.
        : from.length > to.length;

    const morphBatches = many
        ? prepareMorphBatches(one, many)
        : prepareMorphBatches(
            (fromIsMany ? to : from) as DescendentPaths,
            [(fromIsMany ? from : to) as DescendentPaths]
        );
    let animateCount = 0;
    for (let i = 0; i < morphBatches.length; i++) {
        animateCount += morphBatches[i].many.length;
    }
    let animateIndex = 0;
    for (let i = 0; i < morphBatches.length; i++) {
        morphOneBatch(morphBatches[i], fromIsMany, animateIndex, animateCount);
        animateIndex += morphBatches[i].many.length;
    }
}

export function getPathList(
    elements: Element
): DescendentPaths;
export function getPathList(
    elements: Element[]
): DescendentPaths[];
export function getPathList(
    elements: Element | Element[]
): DescendentPaths | DescendentPaths[] {
    if (!elements) {
        return [];
    }

    if (isArray(elements)) {
        const pathList = [];
        for (let i = 0; i < elements.length; i++) {
            pathList.push(getPathList(elements[i]));
        }
        return pathList as DescendentPaths[];
    }

    const pathList: DescendentPaths = [];

    elements.traverse(el => {
        if ((el instanceof Path) && !(el as ECElement).disableMorphing && !el.invisible && !el.ignore) {
            pathList.push(el);
        }
    });
    return pathList;
}