/*
* 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 MatrixModel, { MatrixBaseCellOption, MatrixCellStyleOption, MatrixOption } from '../../coord/matrix/MatrixModel';
import ComponentView from '../../view/Component';
import { MatrixCellLayoutInfo, MatrixDim, MatrixXYLocator } from '../../coord/matrix/MatrixDim';
import Model from '../../model/Model';
import { NullUndefined } from '../../util/types';
import BoundingRect, { RectLike } from 'zrender/src/core/BoundingRect';
import * as vectorUtil from 'zrender/src/core/vector';
import { RectShape } from 'zrender/src/graphic/shape/Rect';
import { ItemStyleProps } from '../../model/mixin/itemStyle';
import { LineStyleProps } from '../../model/mixin/lineStyle';
import { LineShape } from 'zrender/src/graphic/shape/Line';
import { subPixelOptimize } from 'zrender/src/graphic/helper/subPixelOptimize';
import { Group, Text, Rect, Line, XY, setTooltipConfig, expandOrShrinkRect } from '../../util/graphic';
import { clearTmpModel, ListIterator } from '../../util/model';
import { getECData } from '../../util/innerStore';
import { clone, retrieve2, isFunction, isString } from 'zrender/src/core/util';
import { formatTplSimple } from '../../util/format';
import { invert } from 'zrender/src/core/matrix';
import { MatrixBodyCorner, MatrixBodyOrCornerKind } from '../../coord/matrix/MatrixBodyCorner';
import { setLabelStyle } from '../../label/labelStyle';
import GlobalModel from '../../model/Global';

const round = Math.round;

// When special border style is defined on cell, it
// should be over all of the other borders.
type Z2CellDefault = {normal: number, special: number};
const Z2_BACKGROUND = 0;
const Z2_OUTER_BORDER = 99;
const Z2_BODY_CORNER_CELL_DEFAULT: Z2CellDefault = {normal: 25, special: 100};
const Z2_DIMENSION_CELL_DEFAULT: Z2CellDefault = {normal: 50, special: 125};

class MatrixView extends ComponentView {

    static type = 'matrix';
    type = MatrixView.type;

    render(matrixModel: MatrixModel, ecModel: GlobalModel) {

        this.group.removeAll();

        const group = this.group;
        const coordSys = matrixModel.coordinateSystem;
        const rect = coordSys.getRect();
        const xDimModel = matrixModel.getDimensionModel('x');
        const yDimModel = matrixModel.getDimensionModel('y');
        const xDim = xDimModel.dim;
        const yDim = yDimModel.dim;

        // PENDING:
        //  reuse the existing text and rect elements for performance?

        renderDimensionCells(
            group,
            matrixModel,
            ecModel
        );

        createBodyAndCorner(
            group,
            matrixModel,
            xDim,
            yDim,
            ecModel
        );

        const borderZ2Option = matrixModel.getShallow('borderZ2', true);
        const outerBorderZ2 = retrieve2(borderZ2Option, Z2_OUTER_BORDER);
        const dividerLineZ2 = outerBorderZ2 - 1;

        // Outer border and overall background. Use separate elements because of z-order:
        // The overall background should appear below any other elements.
        // But in most cases, the outer border and the divider line should be above the normal cell borders -
        // especially when cell borders have different colors. But users may highlight some specific cells by
        // overstirking their border, in which case it should be above the outer border.
        const bgStyle = matrixModel.getModel('backgroundStyle').getItemStyle(
            ['borderWidth']
        );
        bgStyle.lineWidth = 0;
        const borderStyle = matrixModel.getModel('backgroundStyle').getItemStyle(
            ['color', 'decal', 'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY']
        );
        borderStyle.fill = 'none';
        const bgRect = createMatrixRect(rect.clone(), bgStyle, Z2_BACKGROUND);
        const borderRect = createMatrixRect(rect.clone(), borderStyle, outerBorderZ2);
        bgRect.silent = true;
        borderRect.silent = true;
        group.add(bgRect);
        group.add(borderRect);

        // Header split line.
        const xDimCell0 = xDim.getUnitLayoutInfo(0, 0);
        const yDimCell0 = yDim.getUnitLayoutInfo(1, 0);
        if (xDimCell0 && yDimCell0) {
            if (xDim.shouldShow()) {
                group.add(createMatrixLine(
                    {
                        x1: rect.x,
                        y1: yDimCell0.xy,
                        x2: rect.x + rect.width,
                        y2: yDimCell0.xy,
                    },
                    xDimModel.getModel('dividerLineStyle').getLineStyle(),
                    dividerLineZ2,
                ));
            }
            if (yDim.shouldShow()) {
                group.add(createMatrixLine(
                    {
                        x1: xDimCell0.xy,
                        y1: rect.y,
                        x2: xDimCell0.xy,
                        y2: rect.y + rect.height,
                    },
                    yDimModel.getModel('dividerLineStyle').getLineStyle(),
                    dividerLineZ2,
                ));
            }
        }
    }
}

function renderDimensionCells(group: Group, matrixModel: MatrixModel, ecModel: GlobalModel): void {

    renderOnDimension(0);
    renderOnDimension(1);

    function renderOnDimension(dimIdx: 0 | 1) {
        const thisDimModel = matrixModel.getDimensionModel(XY[dimIdx]);
        const thisDim = thisDimModel.dim;

        if (!thisDim.shouldShow()) {
            return;
        }

        const thisDimBgStyleModel = thisDimModel.getModel('itemStyle');
        const thisDimLabelModel = thisDimModel.getModel('label');
        const tooltipOption = matrixModel.getShallow('tooltip', true);
        const xyLocator: MatrixXYLocator[] = [];

        for (const it = thisDim.resetCellIterator(); it.next();) {
            const dimCell = it.item;
            const shape = {} as RectLike;
            BoundingRect.copy(shape, dimCell.rect);

            vectorUtil.set(xyLocator, dimCell.id.x, dimCell.id.y);

            createMatrixCell(
                xyLocator,
                matrixModel,
                group,
                ecModel,
                dimCell.option,
                thisDimBgStyleModel,
                thisDimLabelModel,
                thisDimModel,
                shape,
                dimCell.option.value,
                Z2_DIMENSION_CELL_DEFAULT,
                tooltipOption,
                dimIdx === 0 ? 'x' : 'y'
            );
        }
    }
}

function createBodyAndCorner(
    group: Group,
    matrixModel: MatrixModel,
    xDim: MatrixDim,
    yDim: MatrixDim,
    ecModel: GlobalModel
): void {

    createBodyOrCornerCells('body', matrixModel.getBody(), xDim, yDim);
    if (xDim.shouldShow() && yDim.shouldShow()) {
        createBodyOrCornerCells('corner', matrixModel.getCorner(), yDim, xDim);
    }

    function createBodyOrCornerCells<TBodyOrCornerKind extends MatrixBodyOrCornerKind>(
        bodyCornerOptionRoot: TBodyOrCornerKind,
        bodyOrCorner: MatrixBodyCorner<TBodyOrCornerKind>,
        dimForCoordX: MatrixDim, // Can be `matrix.y` (transposed) for corners.
        dimForCoordY: MatrixDim, // Can be `matrix.x` (trnasposed) for corners.
    ): void {
        // Prevent inheriting from ancestor.
        const parentCellModel = new Model(matrixModel.getShallow(bodyCornerOptionRoot, true));
        const parentItemStyleModel = parentCellModel.getModel('itemStyle');
        const parentLabelModel = parentCellModel.getModel('label');

        const itx = new ListIterator<MatrixCellLayoutInfo>();
        const ity = new ListIterator<MatrixCellLayoutInfo>();
        const xyLocator: number[] = [];
        const tooltipOption = matrixModel.getShallow('tooltip', true);

        for (dimForCoordY.resetLayoutIterator(ity, 1); ity.next();) {
            for (dimForCoordX.resetLayoutIterator(itx, 0); itx.next();) {
                const xLayout = itx.item;
                const yLayout = ity.item;

                vectorUtil.set(xyLocator, xLayout.id.x, yLayout.id.y);
                const bodyCornerCell = bodyOrCorner.getCell(xyLocator);

                // If in span of an other body or corner cell, never render it.
                if (bodyCornerCell && bodyCornerCell.inSpanOf && bodyCornerCell.inSpanOf !== bodyCornerCell) {
                    continue;
                }

                const shape = {} as RectLike;
                if (bodyCornerCell && bodyCornerCell.span) {
                    BoundingRect.copy(shape, bodyCornerCell.spanRect);
                }
                else {
                    xLayout.dim.getLayout(shape, 0, xyLocator[0]);
                    yLayout.dim.getLayout(shape, 1, xyLocator[1]);
                }

                const bodyCornerCellOption = bodyCornerCell ? bodyCornerCell.option : null;

                createMatrixCell(
                    xyLocator,
                    matrixModel,
                    group,
                    ecModel,
                    bodyCornerCellOption,
                    parentItemStyleModel,
                    parentLabelModel,
                    parentCellModel,
                    shape,
                    bodyCornerCellOption ? bodyCornerCellOption.value : null,
                    Z2_BODY_CORNER_CELL_DEFAULT,
                    tooltipOption,
                    bodyCornerOptionRoot
                );
            }
        }
    } // End of createBodyOrCornerCells
}

type MatrixTargetType = 'x' | 'y' | 'body' | 'corner';

function createMatrixCell(
    xyLocator: MatrixXYLocator[],
    matrixModel: MatrixModel,
    group: Group,
    ecModel: GlobalModel,
    cellOption: MatrixBaseCellOption | NullUndefined,
    parentItemStyleModel: Model<MatrixCellStyleOption['itemStyle']>,
    parentLabelModel: Model<MatrixCellStyleOption['label']>,
    parentCellModel: Model<MatrixCellStyleOption>,
    shape: RectLike,
    textValue: unknown,
    zrCellDefault: Z2CellDefault,
    tooltipOption: MatrixOption['tooltip'],
    targetType: MatrixTargetType
): void {
    // Do not use getModel - a quick performance optimization.
    _tmpCellItemStyleModel.option = cellOption ? cellOption.itemStyle : null;
    _tmpCellItemStyleModel.parentModel = parentItemStyleModel;
    _tmpCellModel.option = cellOption;
    _tmpCellModel.parentModel = parentCellModel;

    // Use different z2 because special border may be defined in itemStyle.
    const z2 = retrieve2(
        _tmpCellModel.getShallow('z2'),
        (cellOption && cellOption.itemStyle) ? zrCellDefault.special : zrCellDefault.normal
    );
    const tooltipOptionShow = tooltipOption && tooltipOption.show;

    const cellRect = createMatrixRect(shape, _tmpCellItemStyleModel.getItemStyle(), z2);
    group.add(cellRect);

    const cursorOption = _tmpCellModel.get('cursor');
    if (cursorOption != null) {
        cellRect.attr('cursor', cursorOption);
    }
    let cellText: Text | NullUndefined;

    if (textValue != null) {
        let text = textValue + '';
        _tmpCellLabelModel.option = cellOption ? cellOption.label : null;
        _tmpCellLabelModel.parentModel = parentLabelModel;
        // This is to accept `option.textStyle` as the default.
        _tmpCellLabelModel.ecModel = ecModel;

        const formatter = _tmpCellLabelModel.getShallow('formatter');
        if (formatter) {
            const params = {
                componentType: 'matrix' as const,
                componentIndex: matrixModel.componentIndex,
                name: text,
                value: textValue as unknown,
                coord: xyLocator.slice() as MatrixXYLocator[],
                $vars: ['name', 'value', 'coord'] as const
            };
            if (isString(formatter)) {
                text = formatTplSimple(formatter, params);
            }
            else if (isFunction(formatter)) {
                const formattedText = formatter(params);
                if (formattedText != null) {
                    text = formattedText + '';
                }
            }
        }

        setLabelStyle(
            cellRect,
            // Currently do not support other states (`emphasis`, `select`, `blur`)
            {normal: _tmpCellLabelModel},
            {
                defaultText: text,
                autoOverflowArea: true,
                // By default based on boundingRect. But boundingRect contains borderWidth,
                // and borderWidth is half outside the cell. Thus specific `layoutRect` explicitly.
                layoutRect: clone(cellRect.shape)
            },
        );
        cellText = cellRect.getTextContent();
        if (cellText) {
            cellText.z2 = z2 + 1;

            const style = cellText.style;
            if (style && (style.overflow && style.overflow !== 'none' && style.lineOverflow)) {
                // `overflow: 'break'/'breakAll'/'truncate'` does not guarantee prevention of overflow
                // when space is insufficient. Use a `clipPath` in such case.
                const clipShape = {} as RectLike;
                BoundingRect.copy(clipShape, shape);
                // `lineWidth` is half outside half inside the bounding rect.
                expandOrShrinkRect(clipShape, (cellRect.style?.lineWidth || 0) / 2, true, true);
                cellRect.updateInnerText();
                cellText.getLocalTransform(_tmpInnerTextTrans);
                invert(_tmpInnerTextTrans, _tmpInnerTextTrans);
                BoundingRect.applyTransform(clipShape, clipShape, _tmpInnerTextTrans);
                cellText.setClipPath(new Rect({shape: clipShape}));
            }
        }

        setTooltipConfig({ // At least for text overflow.
            el: cellRect,
            componentModel: matrixModel,
            itemName: text,
            itemTooltipOption: tooltipOption,
            formatterParamsExtra: {
                xyLocator: xyLocator.slice()
            }
        });
    }

    // Set silent
    const triggerEvent = matrixModel.get('triggerEvent', true);
    if (cellText) {
        let labelSilent = _tmpCellLabelModel.get('silent');
        // By default, silent: false is needed for triggerEvent or tooltip interaction.
        if (labelSilent == null) {
            labelSilent = !(triggerEvent || tooltipOptionShow);
        }
        cellText.silent = labelSilent;
        cellText.ignoreHostSilent = true;
    }
    let rectSilent = _tmpCellModel.get('silent');
    if (rectSilent == null) {
        rectSilent = (
            // If no background color in cell, set `rect.silent: false` will cause that only
            // the border response to mouse hovering, which is probably weird.
            // So we deliberately make rect non-interactive if `silent` is not explicitly specified,
            // even if `triggerEvent` is set as `true`.
            !cellRect.style || cellRect.style.fill === 'none' || !cellRect.style.fill
        );
    }
    cellRect.silent = rectSilent;

    // Both `cellRect` (typically non-transparent) and `cellText` may trigger events, depending on both
    // `matrix.triggerEvent`, `matrix.xxx.silent` and `matrix.xxx.label.silent` settings.
    if (triggerEvent && cellRect) {
        const eventData = {
            componentType: 'matrix' as const,
            componentIndex: matrixModel.componentIndex,
            matrixIndex: matrixModel.componentIndex,
            targetType: targetType,
            name: (cellText && cellText.style) ? cellText.style.text : undefined,
            value: textValue,
            coord: xyLocator.slice()
        };
        getECData(cellRect).eventData = eventData;
    }

    clearTmpModel(_tmpCellModel);
    clearTmpModel(_tmpCellItemStyleModel);
    clearTmpModel(_tmpCellLabelModel);
}
const _tmpCellModel = new Model<MatrixCellStyleOption>();
const _tmpCellItemStyleModel = new Model<MatrixCellStyleOption['itemStyle']>();
const _tmpCellLabelModel = new Model<MatrixCellStyleOption['label']>();
const _tmpInnerTextTrans: number[] = [];

// FIXME: move all of the subpixel process to Matrix.ts resize, otherwise the result of
// `dataToLayout` is not consistent with this rendering, and the caller (like heatmap) can
// not precisely align with the matrix border.
function createMatrixRect(
    shape: RectShape, style: ItemStyleProps, z2: number
): Rect {
    // Currently `subPixelOptimizeRect` can not be used here because it will break rect alignment.
    // Optimize line and rect with the same direction.
    const lineWidth = style.lineWidth;
    if (lineWidth) {
        const x2Original = shape.x + shape.width;
        const y2Original = shape.y + shape.height;
        shape.x = subPixelOptimize(shape.x, lineWidth, true);
        shape.y = subPixelOptimize(shape.y, lineWidth, true);
        shape.width = subPixelOptimize(x2Original, lineWidth, true) - shape.x;
        shape.height = subPixelOptimize(y2Original, lineWidth, true) - shape.y;
    }
    return new Rect({
        shape,
        style: style,
        z2,
    });
}

function createMatrixLine(shape: Omit<LineShape, 'percent'>, style: LineStyleProps, z2: number): Line {
    const lineWidth = style.lineWidth;
    if (lineWidth) {
        if (round(shape.x1 * 2) === round(shape.x2 * 2)) {
            shape.x1 = shape.x2 = subPixelOptimize(shape.x1, lineWidth, true);
        }
        if (round(shape.y1 * 2) === round(shape.y2 * 2)) {
            shape.y1 = shape.y2 = subPixelOptimize(shape.y1, lineWidth, true);
        }
    }
    return new Line({
        shape,
        style,
        silent: true,
        z2,
    });
}

export default MatrixView;
