/*
* 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 { isFunction, extend, createHashMap } from 'zrender/src/core/util';
import { StageHandler, CallbackDataParams, ZRColor, Dictionary, InnerDecalObject }
    from '../util/types';
import makeStyleMapper from '../model/mixin/makeStyleMapper';
import { ITEM_STYLE_KEY_MAP } from '../model/mixin/itemStyle';
import { LINE_STYLE_KEY_MAP } from '../model/mixin/lineStyle';
import SeriesModel from '../model/Series';
import Model from '../model/Model';
import { makeInner } from '../util/model';

const inner = makeInner<{scope: object}, SeriesModel>();

const defaultStyleMappers = {
    itemStyle: makeStyleMapper(ITEM_STYLE_KEY_MAP, true),
    lineStyle: makeStyleMapper(LINE_STYLE_KEY_MAP, true)
};

const defaultColorKey = {
    lineStyle: 'stroke',
    itemStyle: 'fill'
} as const;

function getStyleMapper(seriesModel: SeriesModel, stylePath: string) {
    const styleMapper = seriesModel.visualStyleMapper
        || defaultStyleMappers[stylePath as 'itemStyle' | 'lineStyle'];
    if (!styleMapper) {
        console.warn(`Unknown style type '${stylePath}'.`);
        return defaultStyleMappers.itemStyle;
    }
    return styleMapper;
}

function getDefaultColorKey(seriesModel: SeriesModel, stylePath: string): 'stroke' | 'fill' {
    // return defaultColorKey[stylePath] ||
    const colorKey = seriesModel.visualDrawType
        || defaultColorKey[stylePath as 'itemStyle' | 'lineStyle'];

    if (!colorKey) {
        console.warn(`Unknown style type '${stylePath}'.`);
        return 'fill';
    }

    return colorKey;
}

type ColorCallback = (params: CallbackDataParams) => ZRColor;

const seriesStyleTask: StageHandler = {
    createOnAllSeries: true,
    performRawSeries: true,
    reset(seriesModel, ecModel) {
        const data = seriesModel.getData();
        const stylePath = seriesModel.visualStyleAccessPath
            || 'itemStyle';
        // Set in itemStyle
        const styleModel = seriesModel.getModel(stylePath as any);
        const getStyle = getStyleMapper(seriesModel, stylePath);

        const globalStyle = getStyle(styleModel);

        const decalOption = styleModel.getShallow('decal') as InnerDecalObject;
        if (decalOption) {
            data.setVisual('decal', decalOption);
            decalOption.dirty = true;
        }

        // TODO
        const colorKey = getDefaultColorKey(seriesModel, stylePath);
        const color = globalStyle[colorKey];

        // TODO style callback
        const colorCallback = isFunction(color) ? color as unknown as ColorCallback : null;
        const hasAutoColor = globalStyle.fill === 'auto' || globalStyle.stroke === 'auto';
        // Get from color palette by default.
        if (!globalStyle[colorKey] || colorCallback || hasAutoColor) {
            // Note: If some series has color specified (e.g., by itemStyle.color), we DO NOT
            // make it effect palette. Because some scenarios users need to make some series
            // transparent or as background, which should better not effect the palette.
            const colorPalette = seriesModel.getColorFromPalette(
                // TODO series count changed.
                seriesModel.name, null, ecModel.getSeriesCount()
            );
            if (!globalStyle[colorKey]) {
                globalStyle[colorKey] = colorPalette;
                data.setVisual('colorFromPalette', true);
            }
            globalStyle.fill = (globalStyle.fill === 'auto' || isFunction(globalStyle.fill))
                ? colorPalette
                : globalStyle.fill;
            globalStyle.stroke = (globalStyle.stroke === 'auto' || isFunction(globalStyle.stroke))
                ? colorPalette
                : globalStyle.stroke;
        }

        data.setVisual('style', globalStyle);
        data.setVisual('drawType', colorKey);

        // Only visible series has each data be visual encoded
        if (!ecModel.isSeriesFiltered(seriesModel) && colorCallback) {
            data.setVisual('colorFromPalette', false);

            return {
                dataEach(data, idx) {
                    const dataParams = seriesModel.getDataParams(idx);
                    const itemStyle = extend({}, globalStyle);
                    itemStyle[colorKey] = colorCallback(dataParams);
                    data.setItemVisual(idx, 'style', itemStyle);
                }
            };
        }
    }
};

const sharedModel = new Model();
const dataStyleTask: StageHandler = {
    createOnAllSeries: true,
    performRawSeries: true,
    reset(seriesModel, ecModel) {
        if (seriesModel.ignoreStyleOnData || ecModel.isSeriesFiltered(seriesModel)) {
            return;
        }

        const data = seriesModel.getData();
        const stylePath = seriesModel.visualStyleAccessPath
            || 'itemStyle';
        // Set in itemStyle
        const getStyle = getStyleMapper(seriesModel, stylePath);

        const colorKey = data.getVisual('drawType');

        return {
            dataEach: data.hasItemOption ? function (data, idx) {
                // Not use getItemModel for performance considuration
                const rawItem = data.getRawDataItem(idx) as any;
                if (rawItem && rawItem[stylePath]) {
                    sharedModel.option = rawItem[stylePath];
                    const style = getStyle(sharedModel);

                    const existsStyle = data.ensureUniqueItemVisual(idx, 'style');
                    extend(existsStyle, style);

                    if (sharedModel.option.decal) {
                        data.setItemVisual(idx, 'decal', sharedModel.option.decal);
                        sharedModel.option.decal.dirty = true;
                    }

                    if (colorKey in style) {
                        data.setItemVisual(idx, 'colorFromPalette', false);
                    }
                }
            } : null
        };
    }
};

// Pick color from palette for the data which has not been set with color yet.
// Note: do not support stream rendering. No such cases yet.
const dataColorPaletteTask: StageHandler = {
    performRawSeries: true,
    overallReset(ecModel) {
        // Each type of series uses one scope.
        // Pie and funnel are using different scopes.
        const paletteScopeGroupByType = createHashMap<object>();
        ecModel.eachSeries((seriesModel: SeriesModel) => {
            const colorBy = seriesModel.getColorBy();
            if (seriesModel.isColorBySeries()) {
                return;
            }
            const key = seriesModel.type + '-' + colorBy;
            let colorScope = paletteScopeGroupByType.get(key);
            if (!colorScope) {
                colorScope = {};
                paletteScopeGroupByType.set(key, colorScope);
            }
            inner(seriesModel).scope = colorScope;
        });


        ecModel.eachSeries((seriesModel: SeriesModel) => {
            if (seriesModel.isColorBySeries() || ecModel.isSeriesFiltered(seriesModel)) {
                return;
            }

            const dataAll = seriesModel.getRawData();
            const idxMap: Dictionary<number> = {};
            const data = seriesModel.getData();
            const colorScope = inner(seriesModel).scope;

            const stylePath = seriesModel.visualStyleAccessPath
                || 'itemStyle';
            const colorKey = getDefaultColorKey(seriesModel, stylePath);

            data.each(function (idx) {
                const rawIdx = data.getRawIndex(idx);
                idxMap[rawIdx] = idx;
            });

            // Iterate on data before filtered. To make sure color from palette can be
            // Consistent when toggling legend.
            dataAll.each(function (rawIdx) {
                const idx = idxMap[rawIdx];
                const fromPalette = data.getItemVisual(idx, 'colorFromPalette');
                // Get color from palette for each data only when the color is inherited from series color, which is
                // also picked from color palette. So following situation is not in the case:
                // 1. series.itemStyle.color is set
                // 2. color is encoded by visualMap
                if (fromPalette) {
                    const itemStyle = data.ensureUniqueItemVisual(idx, 'style');
                    const name = dataAll.getName(rawIdx) || (rawIdx + '');
                    const dataCount = dataAll.count();
                    itemStyle[colorKey] = seriesModel.getColorFromPalette(name, colorScope, dataCount);
                }
            });
        });
    }
};

export {
    seriesStyleTask,
    dataStyleTask,
    dataColorPaletteTask
};
