/*
* 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 ExtensionAPI from '../../core/ExtensionAPI';
import GlobalModel from '../../model/Global';
import ComponentView from '../../view/Component';
import { ThumbnailModel } from './ThumbnailModel';
import { NullUndefined, RoamOptionMixin, RoamPayload } from '../../util/types';
import BoundingRect from 'zrender/src/core/BoundingRect';
import * as matrix from 'zrender/src/core/matrix';
import RoamController, { RoamEventParams } from '../helper/RoamController';
import tokens from '../../visual/tokens';
import { createBoxLayoutReference, getBoxLayoutParams, getLayoutRect } from '../../util/layout';
import { expandOrShrinkRect, Rect, Group, traverseUpdateZ, retrieveZInfo } from '../../util/graphic';
import { RectShape } from 'zrender/src/graphic/shape/Rect';
import { applyTransform } from 'zrender/src/core/vector';
import View, {
    VIEW_COORD_SYS_TRANS_RAW, viewCoordSysCopyTrans,
    viewCoordSysSetBoundingRect, viewCoordSysSetViewRect,
    viewCoordSysCopyOverallMatrix
} from '../../coord/View';
import { bind, defaults, extend } from 'zrender/src/core/util';
import type ComponentModel from '../../model/Component';
import { ThumbnailBridgeRendered } from './ThumbnailBridgeImpl';


export class ThumbnailView extends ComponentView {

    static type = 'thumbnail' as const;
    type = ThumbnailView.type;

    private _api: ExtensionAPI;
    private _model: ThumbnailModel;
    private _bgRect: Rect;
    private _windowRect: Rect;
    private _contentRect: BoundingRect;
    private _targetGroup: Group;
    private _transThisToTarget: matrix.MatrixArray;
    private _roamController: RoamController;
    private _coordSys: View;
    private _bridgeRendered: ThumbnailBridgeRendered | NullUndefined;

    // The version of rendering result.
    // `render`/`updateContent`/`updateWindow` can be called separatedly and the order
    // is not guaranteed. Use version to ensure the consistency.
    private _renderVersion: number;

    render(thumbnailModel: ThumbnailModel, ecModel: GlobalModel, api: ExtensionAPI): void {
        this._api = api;
        this._model = thumbnailModel;
        if (!this._coordSys) {
            this._coordSys = new View();
        }

        if (!this._isEnabled()) {
            this._clear();
            return;
        }

        this._renderVersion = api.getECUpdateCycleVersion();
        const group = this.group;

        group.removeAll();

        const itemStyleModel = thumbnailModel.getModel('itemStyle');
        const itemStyle = itemStyleModel.getItemStyle();
        if (itemStyle.fill == null) {
            itemStyle.fill = ecModel.get('backgroundColor') || tokens.color.neutral00;
        }

        const refContainer = createBoxLayoutReference(thumbnailModel, api).refContainer;
        const boxRect = getLayoutRect(
            getBoxLayoutParams(thumbnailModel, true),
            refContainer
        );
        const boxBorderWidth = itemStyle.lineWidth || 0;
        const contentRect = this._contentRect = expandOrShrinkRect(
            boxRect.clone(), boxBorderWidth / 2, true, true
        );

        const contentGroup = new Group();
        group.add(contentGroup);
        contentGroup.setClipPath(new Rect({shape: contentRect.plain()}));

        const targetGroup = this._targetGroup = new Group();
        contentGroup.add(targetGroup);

        // Draw border and background and shadow of thumbnail box.
        const borderShape: RectShape = boxRect.plain();
        borderShape.r = itemStyleModel.getShallow('borderRadius', true) as (number | number[]);
        group.add(this._bgRect = new Rect({
            style: itemStyle,
            shape: borderShape,
            silent: false, // Prevent from hovering on the lower elements.
            cursor: 'grab',
        }));

        const windowStyleModel = thumbnailModel.getModel('windowStyle');
        const windowR = windowStyleModel.getShallow('borderRadius', true) as (number | number[]);
        contentGroup.add(this._windowRect = new Rect({
            shape: {x: 0, y: 0, width: 0, height: 0, r: windowR},
            style: windowStyleModel.getItemStyle(),
            silent: false, // Prevent from hovering on the lower elements.
            cursor: 'grab',
        }));

        this._dealRenderContent();
        this._dealUpdateWindow();

        updateZ(thumbnailModel, this);
    }

    /**
     * Can be called asynchronously directly.
     * This method should be idempotent.
     */
    renderContent(bridgeRendered: ThumbnailBridgeRendered): void {
        this._bridgeRendered = bridgeRendered;
        if (this._isEnabled()) {
            this._dealRenderContent();
            this._dealUpdateWindow();
            updateZ(this._model, this);
        }
    }

    private _dealRenderContent(): void {
        const bridgeRendered = this._bridgeRendered;
        if (!bridgeRendered || bridgeRendered.renderVersion !== this._renderVersion) {
            return;
        }

        const targetGroup = this._targetGroup;
        const coordSys = this._coordSys;
        const contentRect = this._contentRect;

        targetGroup.removeAll();

        if (!bridgeRendered) {
            return;
        }

        const bridgeGroup = bridgeRendered.group;
        const bridgeRect = bridgeGroup.getBoundingRect();

        targetGroup.add(bridgeGroup);

        this._bgRect.z2 = bridgeRendered.z2Range.min - 10;

        viewCoordSysSetBoundingRect(coordSys, bridgeRect.x, bridgeRect.y, bridgeRect.width, bridgeRect.height);
        // Use `getLayoutRect` is just to find an appropriate rect in thumbnail.
        const viewRect = getLayoutRect(
            {
                left: 'center',
                top: 'center',
                aspect: bridgeRect.width / bridgeRect.height
            },
            contentRect
        );
        viewCoordSysSetViewRect(coordSys, viewRect.x, viewRect.y, viewRect.width, viewRect.height);
        viewCoordSysCopyTrans(bridgeGroup, coordSys, VIEW_COORD_SYS_TRANS_RAW);
        bridgeGroup.dirty();

        this._windowRect.z2 = bridgeRendered.z2Range.max + 10;

        this._resetRoamController(bridgeRendered.roamType);
    }

    /**
     * Can be called from action handler directly.
     * This method should be idempotent.
     */
    updateWindow(param: Pick<ThumbnailBridgeRendered, 'targetTrans' | 'renderVersion'>): void {
        const bridgeRendered = this._bridgeRendered;
        if (bridgeRendered && bridgeRendered.renderVersion === param.renderVersion) {
            bridgeRendered.targetTrans = param.targetTrans;
        }
        if (this._isEnabled()) {
            this._dealUpdateWindow();
        }
    }

    private _dealUpdateWindow(): void {
        const bridgeRendered = this._bridgeRendered;
        if (!bridgeRendered || bridgeRendered.renderVersion !== this._renderVersion) {
            return;
        }

        const invTargetTrans = matrix.invert([], bridgeRendered.targetTrans);
        const transTargetToThis = matrix.mul([], viewCoordSysCopyOverallMatrix(null, this._coordSys), invTargetTrans);
        this._transThisToTarget = matrix.invert([], transTargetToThis);

        let viewportRect = bridgeRendered.viewportRect;
        if (!viewportRect) {
            viewportRect = new BoundingRect(0, 0, this._api.getWidth(), this._api.getHeight());
        }
        else {
            viewportRect = viewportRect.clone();
        }
        viewportRect.applyTransform(transTargetToThis);
        const windowRect = this._windowRect;
        const r = windowRect.shape.r;
        windowRect.setShape(defaults({r}, viewportRect));
    }

    private _resetRoamController(
        roamType: RoamOptionMixin['roam'],
    ): void {
        const api = this._api;

        let roamController = this._roamController;
        if (!roamController) {
            roamController = this._roamController = new RoamController(api.getZr());
        }

        if (!roamType || !this._isEnabled()) {
            roamController.disable();
            return;
        }

        roamController.enable(roamType, {
            api: api,
            zInfo: {component: this._model},
            triggerInfo: {
                roamTrigger: null,
                isInSelf: (e, x, y) => this._contentRect.contain(x, y)
            }
        });
        roamController
            .off('pan')
            .off('zoom')
            .on('pan', bind(this._onPan, this))
            .on('zoom', bind(this._onZoom, this));
        // PENDING: Currently roaming animation (when roaming args are changed;
        // e.g., `center` and `zoom` are changed) is not supported in thumbnail.
        // Animation should be able to be interrupted on pan and zoom, and synchronize
        // back to the target. See VIEW_COORD_SYS_ANIMATION.
    }

    private _onPan(event: RoamEventParams['pan']): void {
        const trans = this._transThisToTarget;
        if (!this._isEnabled() || !trans) {
            return;
        }
        const oldOffset = applyTransform([], [event.oldX, event.oldY], trans);
        const newOffset = applyTransform([], [event.oldX - event.dx, event.oldY - event.dy], trans);

        this._api.dispatchAction(makeRoamPayload(this._model.getTarget().baseMapProvider, {
            dx: newOffset[0] - oldOffset[0],
            dy: newOffset[1] - oldOffset[1],
        }));
    }

    private _onZoom(event: RoamEventParams['zoom']): void {
        const trans = this._transThisToTarget;
        if (!this._isEnabled() || !trans) {
            return;
        }
        const offset = applyTransform([], [event.originX, event.originY], trans);

        this._api.dispatchAction(makeRoamPayload(this._model.getTarget().baseMapProvider, {
            zoom: 1 / event.scale,
            originX: offset[0],
            originY: offset[1],
        }));
    }

    /**
     * This method is also responsible for check enable in asynchronous situation,
     * e.g., in event listeners that is supposed to be outdated but not be removed.
     */
    private _isEnabled(): boolean {
        const thumbnailModel = this._model;
        if (!thumbnailModel || !thumbnailModel.shouldShow()) {
            return false;
        }
        const baseMapProvider = thumbnailModel.getTarget().baseMapProvider;
        if (!baseMapProvider) {
            return false;
        }
        return true;
    }

    private _clear(): void {
        this.group.removeAll();
        this._bridgeRendered = null;
        if (this._roamController) {
            this._roamController.disable();
        }
    }

    remove() {
        this._clear();
    }

    dispose() {
        this._clear();
    }

}

function makeRoamPayload(baseMapProvider: ComponentModel, params: Partial<RoamPayload>): RoamPayload {
    const type = baseMapProvider.mainType === 'series'
        ? `${baseMapProvider.subType}Roam` // e.g. 'graphRoam'
        : `${baseMapProvider.mainType}Roam`; // e.g., 'geoRoam'
    const payload = {type} as RoamPayload;
    payload[`${baseMapProvider.mainType}Id`] = baseMapProvider.id;
    extend(payload, params);
    return payload;
}

function updateZ(thumbnailModel: ThumbnailModel, thumbnailView: ThumbnailView): void {
    const zInfo = retrieveZInfo(thumbnailModel);
    traverseUpdateZ(thumbnailView.group, zInfo.z, zInfo.zlevel);
}
