/*
* 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 * as zrUtil from 'zrender/src/core/util';
import * as modelUtil from '../../util/model';
import ComponentModel from '../../model/Component';
import Model from '../../model/Model';
import geoCreator from './geoCreator';
import Geo from './Geo';
import {
    ComponentOption,
    BoxLayoutOptionMixin,
    ItemStyleOption,
    ZRColor,
    LabelOption,
    DisplayState,
    RoamOptionMixin,
    AnimationOptionMixin,
    StatesOptionMixin,
    Dictionary,
    CommonTooltipOption,
    StatesMixinBase
} from '../../util/types';
import { NameMap } from './geoTypes';
import GlobalModel from '../../model/Global';
import geoSourceManager from './geoSourceManager';


export interface GeoItemStyleOption<TCbParams = never> extends ItemStyleOption<TCbParams> {
    areaColor?: ZRColor;
};
interface GeoLabelOption extends LabelOption {
    formatter?: string | ((params: GeoLabelFormatterDataParams) => string);
}
export interface GeoStateOption {
    itemStyle?: GeoItemStyleOption
    // FIXME:TS formatter?
    label?: GeoLabelOption
}
interface GeoLabelFormatterDataParams {
    name: string;
    status: DisplayState;
}

export interface RegoinOption extends GeoStateOption, StatesOptionMixin<GeoStateOption, StatesMixinBase> {
    name?: string
    selected?: boolean
    tooltip?: CommonTooltipOption<GeoTooltipFormatterParams>
}

export interface GeoTooltipFormatterParams {
    componentType: 'geo'
    geoIndex: number
    name: string
    $vars: ['name']
}

export interface GeoCommonOptionMixin extends RoamOptionMixin {
    // Map name
    map: string;

    // Aspect is width / height. Inited to be geoJson bbox aspect
    // This parameter is used for scale this aspect
    aspectScale?: number;

    ///// Layout with center and size
    // If you wan't to put map in a fixed size box with right aspect ratio
    // This two properties may more conveninet
    // Like: `40` or `'50%'`.
    layoutCenter?: (number | string)[];
    // Like: `40` or `'50%'`.
    layoutSize?: number | string;

    // Define left-top, right-bottom coords to control view
    // For example, [ [180, 90], [-180, -90] ]
    // higher priority than center and zoom
    boundingCoords?: number[][];

    nameMap?: NameMap;
    nameProperty?: string;
}

export interface GeoOption extends
    ComponentOption,
    BoxLayoutOptionMixin,
    // For lens animation on geo.
    AnimationOptionMixin,
    GeoCommonOptionMixin,
    StatesOptionMixin<GeoStateOption, StatesMixinBase>, GeoStateOption {
    mainType?: 'geo';

    show?: boolean;
    silent?: boolean;

    regions?: RegoinOption[];

    stateAnimation?: AnimationOptionMixin

    selectedMode?: 'single' | 'multiple' | boolean
    selectedMap?: Dictionary<boolean>

    tooltip?: CommonTooltipOption<GeoTooltipFormatterParams>
}

class GeoModel extends ComponentModel<GeoOption> {

    static type = 'geo';
    readonly type = GeoModel.type;

    coordinateSystem: Geo;

    static layoutMode = 'box' as const;

    private _optionModelMap: zrUtil.HashMap<Model<RegoinOption>>;

    static defaultOption: GeoOption = {

        zlevel: 0,

        z: 0,

        show: true,

        left: 'center',

        top: 'center',

        // Default value:
        // for geoSVG source: 1,
        // for geoJSON source: 0.75.
        aspectScale: null,

        ///// Layout with center and size
        // If you wan't to put map in a fixed size box with right aspect ratio
        // This two properties may more conveninet
        // layoutCenter: [50%, 50%]
        // layoutSize: 100

        silent: false,

        // Map type
        map: '',

        // Define left-top, right-bottom coords to control view
        // For example, [ [180, 90], [-180, -90] ]
        boundingCoords: null,

        // Default on center of map
        center: null,

        zoom: 1,

        scaleLimit: null,

        // selectedMode: false

        label: {
            show: false,
            color: '#000'
        },

        itemStyle: {
            borderWidth: 0.5,
            borderColor: '#444'
            // Default color:
            // + geoJSON: #eee
            // + geoSVG: null (use SVG original `fill`)
            // color: '#eee'
        },

        emphasis: {
            label: {
                show: true,
                color: 'rgb(100,0,0)'
            },
            itemStyle: {
                color: 'rgba(255,215,0,0.8)'
            }
        },

        select: {
            label: {
                show: true,
                color: 'rgb(100,0,0)'
            },
            itemStyle: {
                color: 'rgba(255,215,0,0.8)'
            }
        },

        regions: []

        // tooltip: {
        //     show: false
        // }
    };

    init(option: GeoOption, parentModel: Model, ecModel: GlobalModel): void {
        const source = geoSourceManager.getGeoResource(option.map);
        if (source && source.type === 'geoJSON') {
            const itemStyle = option.itemStyle = option.itemStyle || {};
            if (!('color' in itemStyle)) {
                itemStyle.color = '#eee';
            }
        }

        this.mergeDefaultAndTheme(option, ecModel);

        // Default label emphasis `show`
        modelUtil.defaultEmphasis(option, 'label', ['show']);
    }

    optionUpdated(): void {
        const option = this.option;

        option.regions = geoCreator.getFilledRegions(
            option.regions, option.map, option.nameMap, option.nameProperty
        );

        const selectedMap: Dictionary<boolean> = {};
        this._optionModelMap = zrUtil.reduce(option.regions || [], (optionModelMap, regionOpt) => {
            const regionName = regionOpt.name;
            if (regionName) {
                optionModelMap.set(regionName, new Model(regionOpt, this, this.ecModel));
                if (regionOpt.selected) {
                    selectedMap[regionName] = true;
                }
            }
            return optionModelMap;
        }, zrUtil.createHashMap());

        if (!option.selectedMap) {
            option.selectedMap = selectedMap;
        }
    }

    /**
     * Get model of region.
     */
    getRegionModel(name: string): Model<RegoinOption> {
        return this._optionModelMap.get(name) || new Model(null, this, this.ecModel);
    }

    /**
     * Format label
     * @param name Region name
     */
    getFormattedLabel(name: string, status?: DisplayState) {
        const regionModel = this.getRegionModel(name);
        const formatter = status === 'normal'
            ? regionModel.get(['label', 'formatter'])
            : regionModel.get(['emphasis', 'label', 'formatter']);
        const params = {
            name: name
        } as GeoLabelFormatterDataParams;
        if (typeof formatter === 'function') {
            params.status = status;
            return formatter(params);
        }
        else if (typeof formatter === 'string') {
            return formatter.replace('{a}', name != null ? name : '');
        }
    }

    setZoom(zoom: number): void {
        this.option.zoom = zoom;
    }

    setCenter(center: number[]): void {
        this.option.center = center;
    }

    // PENGING If selectedMode is null ?
    select(name?: string): void {
        const option = this.option;
        const selectedMode = option.selectedMode;
        if (!selectedMode) {
            return;
        }
        if (selectedMode !== 'multiple') {
            option.selectedMap = null;
        }

        const selectedMap = option.selectedMap || (option.selectedMap = {});
        selectedMap[name] = true;
    }

    unSelect(name?: string): void {
        const selectedMap = this.option.selectedMap;
        if (selectedMap) {
            selectedMap[name] = false;
        }
    }

    toggleSelected(name?: string): void {
        this[this.isSelected(name) ? 'unSelect' : 'select'](name);
    }

    isSelected(name?: string): boolean {
        const selectedMap = this.option.selectedMap;
        return !!(selectedMap && selectedMap[name]);
    }

}

export default GeoModel;
