/*
* 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.
*/

/* global Float32Array */

// TODO Batch by color

import * as graphic from '../../util/graphic';
import {createSymbol} from '../../util/symbol';
import SeriesData from '../../data/SeriesData';
import { PathProps } from 'zrender/src/graphic/Path';
import PathProxy from 'zrender/src/core/PathProxy';
import SeriesModel from '../../model/Series';
import { StageHandlerProgressParams } from '../../util/types';
import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem';
import { getECData } from '../../util/innerStore';
import Element from 'zrender/src/Element';
import Displayable, { BeforeBrushParam } from 'zrender/src/graphic/Displayable';
import { validateUpstreamOutputRange } from '../../util/model';
import { ISymbolDraw, SymbolDrawUpdateOpt } from './baseDraw';


const BOOST_SIZE_THRESHOLD = 4;

class LargeSymbolPathShape {
    points: ArrayLike<number>;
    size: number[];
}

type LargeSymbolPathProps = PathProps & {
    shape?: Partial<LargeSymbolPathShape>
    startIndex?: number
    endIndex?: number
};

type ECSymbol = ReturnType<typeof createSymbol>;

class LargeSymbolPath extends graphic.Path<LargeSymbolPathProps> {

    shape: LargeSymbolPathShape;

    symbolProxy: ECSymbol;

    softClipShape: CoordinateSystemClipArea;

    startIndex: number;
    endIndex: number;

    private _ctx: CanvasRenderingContext2D;
    private _off: number = 0;

    hoverDataIdx: number = -1;

    notClear: boolean;

    constructor(opts?: LargeSymbolPathProps) {
        super(opts);
    }

    getDefaultShape() {
        return new LargeSymbolPathShape();
    }

    setColor: ECSymbol['setColor'];

    reset() {
        this.notClear = false;
        this._off = 0;
    }

    beforeBrush(param: BeforeBrushParam) {
        if (param && !param.contentRetained) {
            this.reset();
        }
    }

    buildPath(path: PathProxy | CanvasRenderingContext2D, shape: LargeSymbolPathShape) {
        const points = shape.points;
        const size = shape.size;

        const symbolProxy = this.symbolProxy;
        const symbolProxyShape = symbolProxy.shape;
        const ctx = (path as PathProxy).getContext
            ? (path as PathProxy).getContext()
            : path as CanvasRenderingContext2D;
        const canBoost = ctx && size[0] < BOOST_SIZE_THRESHOLD;
        const softClipShape = this.softClipShape;
        let i;

        // Do draw in afterBrush.
        if (canBoost) {
            this._ctx = ctx;
            return;
        }

        this._ctx = null;

        for (i = this._off; i < points.length;) {
            const x = points[i++];
            const y = points[i++];

            if (isNaN(x) || isNaN(y)) {
                continue;
            }
            if (softClipShape && !softClipShape.contain(x, y)) {
                continue;
            }

            symbolProxyShape.x = x - size[0] / 2;
            symbolProxyShape.y = y - size[1] / 2;
            symbolProxyShape.width = size[0];
            symbolProxyShape.height = size[1];

            symbolProxy.buildPath(path, symbolProxyShape, true);
        }
        if (this.incremental) {
            this._off = i;
            this.notClear = true;
        }
    }

    afterBrush() {
        const shape = this.shape;
        const points = shape.points;
        const size = shape.size;
        const ctx = this._ctx;
        const softClipShape = this.softClipShape;
        let i;

        if (!ctx) {
            return;
        }

        // PENDING If style or other canvas status changed?
        for (i = this._off; i < points.length;) {
            const x = points[i++];
            const y = points[i++];
            if (isNaN(x) || isNaN(y)) {
                continue;
            }
            if (softClipShape && !softClipShape.contain(x, y)) {
                continue;
            }
            // fillRect is faster than building a rect path and draw.
            // And it support light globalCompositeOperation.
            ctx.fillRect(
                x - size[0] / 2, y - size[1] / 2,
                size[0], size[1]
            );
        }
        if (this.incremental) {
            this._off = i;
            this.notClear = true;
        }
    }

    findDataIndex(x: number, y: number) {
        // TODO ???
        // Consider transform

        const shape = this.shape;
        const points = shape.points;
        const size = shape.size;

        const w = Math.max(size[0], 4);
        const h = Math.max(size[1], 4);

        // Not consider transform
        // Treat each element as a rect
        // top down traverse
        for (let idx = points.length / 2 - 1; idx >= 0; idx--) {
            const i = idx * 2;
            const x0 = points[i] - w / 2;
            const y0 = points[i + 1] - h / 2;
            if (x >= x0 && y >= y0 && x <= x0 + w && y <= y0 + h) {
                return idx;
            }
        }

        return -1;
    }

    contain(x: number, y: number): boolean {
        const localPos = this.transformCoordToLocal(x, y);
        const rect = this.getBoundingRect();
        x = localPos[0];
        y = localPos[1];

        if (rect.contain(x, y)) {
            // Cache found data index.
            const dataIdx = this.hoverDataIdx = this.findDataIndex(x, y);
            return dataIdx >= 0;
        }
        this.hoverDataIdx = -1;
        return false;
    }

    getBoundingRect() {
        // Ignore stroke for large symbol draw.
        let rect = this._rect;
        if (!rect) {
            const shape = this.shape;
            const points = shape.points;
            const size = shape.size;
            const w = size[0];
            const h = size[1];
            let minX = Infinity;
            let minY = Infinity;
            let maxX = -Infinity;
            let maxY = -Infinity;
            for (let i = 0; i < points.length;) {
                const x = points[i++];
                const y = points[i++];
                minX = Math.min(x, minX);
                maxX = Math.max(x, maxX);
                minY = Math.min(y, minY);
                maxY = Math.max(y, maxY);
            }

            rect = this._rect = new graphic.BoundingRect(
                minX - w / 2,
                minY - h / 2,
                maxX - minX + w,
                maxY - minY + h
            );
        }
        return rect;
    }
}

class LargeSymbolDraw implements ISymbolDraw {

    group = new graphic.Group();

    // New add element in this frame of progressive render.
    private _newAdded: LargeSymbolPath[];
    private _data: SeriesData;

    /**
     * Update symbols draw by new data
     */
    updateData(data: SeriesData, opt?: SymbolDrawUpdateOpt) {
        this._clear();
        this._data = data;

        const symbolEl = this._create();
        symbolEl.setShape({
            points: data.getLayout('points')
        });
        this._setCommon(symbolEl, data, opt);
    }

    updateLayout(opt: SymbolDrawUpdateOpt) {
        const data = this._data;
        if (!data) {
            return;
        }
        let points = data.getLayout('points');
        this.group.eachChild(function (child: LargeSymbolPath) {
            if (child.startIndex != null) {
                const len = (child.endIndex - child.startIndex) * 2;
                const byteOffset = child.startIndex * 4 * 2;
                points = new Float32Array(points.buffer, byteOffset, len);
            }
            child.setShape('points', points);
            // Reset draw cursor.
            child.reset();
            child.stopAnimation();
        });
    }

    incrementalPrepareUpdate(data: SeriesData) {
        this._clear();
    }

    incrementalUpdate(
        taskParams: StageHandlerProgressParams,
        data: SeriesData<SeriesModel>,
        incrementalId: Displayable['incremental'],
        opt: SymbolDrawUpdateOpt
    ) {
        const lastAdded = this._newAdded[0];
        const points = data.getLayout('points');
        const oldPoints = lastAdded && lastAdded.shape.points;

        if (__DEV__) {
            validateUpstreamOutputRange(data.getLayout('pointsRange'), taskParams);
        }

        // Merging the exists. Each element has 1e4 points.
        // Consider the performance balance between too much elements and too much points in one shape(may affect hover optimization)
        if (oldPoints && oldPoints.length < 2e4) {
            const oldLen = oldPoints.length;
            const newPoints = new Float32Array(oldLen + points.length);
            // Concat two array
            newPoints.set(oldPoints);
            newPoints.set(points, oldLen);
            // Update endIndex
            lastAdded.endIndex = taskParams.end;
            lastAdded.setShape({ points: newPoints });
        }
        else {
            this._newAdded = [];

            const symbolEl = this._create();
            symbolEl.startIndex = taskParams.start;
            symbolEl.endIndex = taskParams.end;
            symbolEl.incremental = incrementalId;
            symbolEl.setShape({
                points
            });
            this._setCommon(symbolEl, data, opt);
        }
    }

    eachRendered(cb: (el: Element) => boolean | void) {
        this._newAdded[0] && cb(this._newAdded[0]);
    }

    private _create() {
        const symbolEl = new LargeSymbolPath({
            cursor: 'default'
        });
        symbolEl.ignoreCoarsePointer = true;
        this.group.add(symbolEl);
        this._newAdded.push(symbolEl);
        return symbolEl;
    }

    private _setCommon(
        symbolEl: LargeSymbolPath,
        data: SeriesData,
        opt: SymbolDrawUpdateOpt
    ) {
        const hostModel = data.hostModel;

        opt = opt || {};

        const size = data.getVisual('symbolSize');
        symbolEl.setShape('size', (size instanceof Array) ? size : [size, size]);

        symbolEl.softClipShape = opt.clipShape || null;
        // Create symbolProxy to build path for each data
        symbolEl.symbolProxy = createSymbol(
            data.getVisual('symbol'), 0, 0, 0, 0
        );
        // Use symbolProxy setColor method
        symbolEl.setColor = symbolEl.symbolProxy.setColor;

        const extrudeShadow = symbolEl.shape.size[0] < BOOST_SIZE_THRESHOLD;
        symbolEl.useStyle(
            // Draw shadow when doing fillRect is extremely slow.
            hostModel.getModel('itemStyle').getItemStyle(
                extrudeShadow ? ['color', 'shadowBlur', 'shadowColor'] : ['color']
            )
        );

        const globalStyle = data.getVisual('style');
        const visualColor = globalStyle && globalStyle.fill;
        if (visualColor) {
            symbolEl.setColor(visualColor);
        }

        const ecData = getECData(symbolEl);
        // Enable tooltip
        // PENDING May have performance issue when path is extremely large
        ecData.seriesIndex = (hostModel as SeriesModel).seriesIndex;
        symbolEl.on('mousemove', function (e) {
            ecData.dataIndex = null;
            const dataIndex = symbolEl.hoverDataIdx;
            if (dataIndex >= 0) {
                // Provide dataIndex for tooltip
                ecData.dataIndex = dataIndex + (symbolEl.startIndex || 0);
            }
        });
    }

    remove() {
        this._clear();
    }

    private _clear() {
        this._newAdded = [];
        this.group.removeAll();
    }
}


export default LargeSymbolDraw;