/*
* 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 graphic from '../../util/graphic';
import { toggleHoverEmphasis } from '../../util/states';
import HeatmapLayer from './HeatmapLayer';
import * as zrUtil from 'zrender/src/core/util';
import ChartView from '../../view/Chart';
import HeatmapSeriesModel, { HeatmapDataItemOption } from './HeatmapSeries';
import type GlobalModel from '../../model/Global';
import type ExtensionAPI from '../../core/ExtensionAPI';
import type VisualMapModel from '../../component/visualMap/VisualMapModel';
import type PiecewiseModel from '../../component/visualMap/PiecewiseModel';
import type ContinuousModel from '../../component/visualMap/ContinuousModel';
import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem';
import { StageHandlerProgressParams, Dictionary, OptionDataValue } from '../../util/types';
import type Cartesian2D from '../../coord/cartesian/Cartesian2D';
import type Calendar from '../../coord/calendar/Calendar';
import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle';
import type Element from 'zrender/src/Element';

// Coord can be 'geo' 'bmap' 'amap' 'leaflet'...
interface GeoLikeCoordSys extends CoordinateSystem {
    dimensions: ['lng', 'lat']
    getViewRect(): graphic.BoundingRect
}

function getIsInPiecewiseRange(
    dataExtent: number[],
    pieceList: ReturnType<PiecewiseModel['getPieceList']>,
    selected: Dictionary<boolean>
) {
    const dataSpan = dataExtent[1] - dataExtent[0];
    pieceList = zrUtil.map(pieceList, function (piece) {
        return {
            interval: [
                (piece.interval[0] - dataExtent[0]) / dataSpan,
                (piece.interval[1] - dataExtent[0]) / dataSpan
            ]
        };
    });
    const len = pieceList.length;
    let lastIndex = 0;

    return function (val: number) {
        let i;
        // Try to find in the location of the last found
        for (i = lastIndex; i < len; i++) {
            const interval = pieceList[i].interval;
            if (interval[0] <= val && val <= interval[1]) {
                lastIndex = i;
                break;
            }
        }
        if (i === len) { // Not found, back interation
            for (i = lastIndex - 1; i >= 0; i--) {
                const interval = pieceList[i].interval;
                if (interval[0] <= val && val <= interval[1]) {
                    lastIndex = i;
                    break;
                }
            }
        }
        return i >= 0 && i < len && selected[i];
    };
}

function getIsInContinuousRange(dataExtent: number[], range: number[]) {
    const dataSpan = dataExtent[1] - dataExtent[0];
    range = [
        (range[0] - dataExtent[0]) / dataSpan,
        (range[1] - dataExtent[0]) / dataSpan
    ];
    return function (val: number) {
        return val >= range[0] && val <= range[1];
    };
}

function isGeoCoordSys(coordSys: CoordinateSystem): coordSys is GeoLikeCoordSys {
    const dimensions = coordSys.dimensions;
    // Not use coordSys.type === 'geo' because coordSys maybe extended
    return dimensions[0] === 'lng' && dimensions[1] === 'lat';
}

class HeatmapView extends ChartView {

    static readonly type = 'heatmap';
    readonly type = HeatmapView.type;

    private _hmLayer: HeatmapLayer;

    private _progressiveEls: Element[];

    render(seriesModel: HeatmapSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
        let visualMapOfThisSeries;
        ecModel.eachComponent('visualMap', function (visualMap: VisualMapModel) {
            visualMap.eachTargetSeries(function (targetSeries) {
                if (targetSeries === seriesModel) {
                    visualMapOfThisSeries = visualMap;
                }
            });
        });

        if (__DEV__) {
            if (!visualMapOfThisSeries) {
                throw new Error('Heatmap must use with visualMap');
            }
        }

        // Clear previously rendered progressive elements.
        this._progressiveEls = null;

        this.group.removeAll();

        const coordSys = seriesModel.coordinateSystem;
        if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar') {
            this._renderOnCartesianAndCalendar(seriesModel, api, 0, seriesModel.getData().count());
        }
        else if (isGeoCoordSys(coordSys)) {
            this._renderOnGeo(
                coordSys, seriesModel, visualMapOfThisSeries, api
            );
        }
    }

    incrementalPrepareRender(seriesModel: HeatmapSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) {
        this.group.removeAll();
    }

    incrementalRender(
        params: StageHandlerProgressParams,
        seriesModel: HeatmapSeriesModel,
        ecModel: GlobalModel,
        api: ExtensionAPI
    ) {
        const coordSys = seriesModel.coordinateSystem;
        if (coordSys) {
            // geo does not support incremental rendering?
            if (isGeoCoordSys(coordSys)) {
                this.render(seriesModel, ecModel, api);
            }
            else {
                this._progressiveEls = [];
                this._renderOnCartesianAndCalendar(seriesModel, api, params.start, params.end, true);
            }
        }
    }

    eachRendered(cb: (el: Element) => boolean | void) {
        graphic.traverseElements(this._progressiveEls || this.group, cb);
    }

    _renderOnCartesianAndCalendar(
        seriesModel: HeatmapSeriesModel,
        api: ExtensionAPI,
        start: number,
        end: number,
        incremental?: boolean
    ) {

        const coordSys = seriesModel.coordinateSystem as Cartesian2D | Calendar;
        const isCartesian2d = isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d');
        let width;
        let height;
        let xAxisExtent;
        let yAxisExtent;

        if (isCartesian2d) {
            const xAxis = coordSys.getAxis('x');
            const yAxis = coordSys.getAxis('y');

            if (__DEV__) {
                if (!(xAxis.type === 'category' && yAxis.type === 'category')) {
                    throw new Error('Heatmap on cartesian must have two category axes');
                }
                if (!(xAxis.onBand && yAxis.onBand)) {
                    throw new Error('Heatmap on cartesian must have two axes with boundaryGap true');
                }
            }

            // add 0.5px to avoid the gaps
            width = xAxis.getBandWidth() + .5;
            height = yAxis.getBandWidth() + .5;
            xAxisExtent = xAxis.scale.getExtent();
            yAxisExtent = yAxis.scale.getExtent();
        }

        const group = this.group;
        const data = seriesModel.getData();

        let emphasisStyle = seriesModel.getModel(['emphasis', 'itemStyle']).getItemStyle();
        let blurStyle = seriesModel.getModel(['blur', 'itemStyle']).getItemStyle();
        let selectStyle = seriesModel.getModel(['select', 'itemStyle']).getItemStyle();
        let borderRadius = seriesModel.get(['itemStyle', 'borderRadius']);
        let labelStatesModels = getLabelStatesModels(seriesModel);
        const emphasisModel = seriesModel.getModel('emphasis');
        let focus = emphasisModel.get('focus');
        let blurScope = emphasisModel.get('blurScope');
        let emphasisDisabled = emphasisModel.get('disabled');

        const dataDims = isCartesian2d
            ? [
                data.mapDimension('x'),
                data.mapDimension('y'),
                data.mapDimension('value')
            ]
            : [
                data.mapDimension('time'),
                data.mapDimension('value')
            ];

        for (let idx = start; idx < end; idx++) {
            let rect;
            const style = data.getItemVisual(idx, 'style');

            if (isCartesian2d) {
                const dataDimX = data.get(dataDims[0], idx);
                const dataDimY = data.get(dataDims[1], idx);

                // Ignore empty data and out of extent data
                if (isNaN(data.get(dataDims[2], idx) as number)
                    || isNaN(dataDimX as number)
                    || isNaN(dataDimY as number)
                    || dataDimX < xAxisExtent[0]
                    || dataDimX > xAxisExtent[1]
                    || dataDimY < yAxisExtent[0]
                    || dataDimY > yAxisExtent[1]
                ) {
                    continue;
                }

                const point = coordSys.dataToPoint([
                    dataDimX,
                    dataDimY
                ]);

                rect = new graphic.Rect({
                    shape: {
                        x: point[0] - width / 2,
                        y: point[1] - height / 2,
                        width,
                        height
                    },
                    style
                });
            }
            else {
                // Ignore empty data
                if (isNaN(data.get(dataDims[1], idx) as number)) {
                    continue;
                }

                const contentShape = coordSys.dataToRect([data.get(dataDims[0], idx)]).contentShape;
                // Ignore data that are not in range
                if (isNaN(contentShape.x) || isNaN(contentShape.y)) {
                    continue;
                }

                rect = new graphic.Rect({
                    z2: 1,
                    shape: contentShape,
                    style
                });
            }

            // Optimization for large dataset
            if (data.hasItemOption) {
                const itemModel = data.getItemModel<HeatmapDataItemOption>(idx);
                const emphasisModel = itemModel.getModel('emphasis');
                emphasisStyle = emphasisModel.getModel('itemStyle').getItemStyle();
                blurStyle = itemModel.getModel(['blur', 'itemStyle']).getItemStyle();
                selectStyle = itemModel.getModel(['select', 'itemStyle']).getItemStyle();

                // Each item value struct in the data would be firstly
                // {
                //     itemStyle: { borderRadius: [30, 30] },
                //     value: [2022, 02, 22]
                // }
                borderRadius = itemModel.get(['itemStyle', 'borderRadius']);

                focus = emphasisModel.get('focus');
                blurScope = emphasisModel.get('blurScope');
                emphasisDisabled = emphasisModel.get('disabled');

                labelStatesModels = getLabelStatesModels(itemModel);
            }

            rect.shape.r = borderRadius;

            const rawValue = seriesModel.getRawValue(idx) as OptionDataValue[];
            let defaultText = '-';
            if (rawValue && rawValue[2] != null) {
                defaultText = rawValue[2] + '';
            }

            setLabelStyle(
                rect, labelStatesModels,
                {
                    labelFetcher: seriesModel,
                    labelDataIndex: idx,
                    defaultOpacity: style.opacity,
                    defaultText: defaultText
                }
            );

            rect.ensureState('emphasis').style = emphasisStyle;
            rect.ensureState('blur').style = blurStyle;
            rect.ensureState('select').style = selectStyle;

            toggleHoverEmphasis(rect, focus, blurScope, emphasisDisabled);

            rect.incremental = incremental;
            // PENDING
            if (incremental) {
                // Rect must use hover layer if it's incremental.
                rect.states.emphasis.hoverLayer = true;
            }

            group.add(rect);
            data.setItemGraphicEl(idx, rect);

            if (this._progressiveEls) {
                this._progressiveEls.push(rect);
            }
        }
    }

    _renderOnGeo(
        geo: GeoLikeCoordSys,
        seriesModel: HeatmapSeriesModel,
        visualMapModel: VisualMapModel,
        api: ExtensionAPI
    ) {
        const inRangeVisuals = visualMapModel.targetVisuals.inRange;
        const outOfRangeVisuals = visualMapModel.targetVisuals.outOfRange;
        // if (!visualMapping) {
        //     throw new Error('Data range must have color visuals');
        // }

        const data = seriesModel.getData();
        const hmLayer = this._hmLayer || (this._hmLayer || new HeatmapLayer());
        hmLayer.blurSize = seriesModel.get('blurSize');
        hmLayer.pointSize = seriesModel.get('pointSize');
        hmLayer.minOpacity = seriesModel.get('minOpacity');
        hmLayer.maxOpacity = seriesModel.get('maxOpacity');

        const rect = geo.getViewRect().clone();
        const roamTransform = geo.getRoamTransform();
        rect.applyTransform(roamTransform);

        // Clamp on viewport
        const x = Math.max(rect.x, 0);
        const y = Math.max(rect.y, 0);
        const x2 = Math.min(rect.width + rect.x, api.getWidth());
        const y2 = Math.min(rect.height + rect.y, api.getHeight());
        const width = x2 - x;
        const height = y2 - y;

        const dims = [
            data.mapDimension('lng'),
            data.mapDimension('lat'),
            data.mapDimension('value')
        ];

        const points = data.mapArray(dims, function (lng: number, lat: number, value: number) {
            const pt = geo.dataToPoint([lng, lat]);
            pt[0] -= x;
            pt[1] -= y;
            pt.push(value);
            return pt;
        });

        const dataExtent = visualMapModel.getExtent();
        const isInRange = visualMapModel.type === 'visualMap.continuous'
            ? getIsInContinuousRange(dataExtent, (visualMapModel as ContinuousModel).option.range)
            : getIsInPiecewiseRange(
                dataExtent,
                (visualMapModel as PiecewiseModel).getPieceList(),
                (visualMapModel as PiecewiseModel).option.selected
            );

        hmLayer.update(
            points, width, height,
            inRangeVisuals.color.getNormalizer(),
            {
                inRange: inRangeVisuals.color.getColorMapper(),
                outOfRange: outOfRangeVisuals.color.getColorMapper()
            },
            isInRange
        );
        const img = new graphic.Image({
            style: {
                width: width,
                height: height,
                x: x,
                y: y,
                image: hmLayer.canvas
            },
            silent: true
        });
        this.group.add(img);
    }
}

export default HeatmapView;
