/********************************************************************************
 * Copyright (c) 2017-2018 TypeFox and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the Eclipse
 * Public License v. 2.0 are satisfied: GNU General Public License, version 2
 * with the GNU Classpath Exception which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 ********************************************************************************/

import {
    InitializeCanvasBoundsAction, ServerStatusAction, IActionDispatcher, ModelSource,
    TYPES, ViewerOptions, DiagramServerProxy
} from 'sprotty';
import { CenterAction, RequestModelAction } from 'sprotty-protocol';
import { Widget } from "@phosphor/widgets";
import { Message } from "@phosphor/messaging/lib";
import { BaseWidget, codiconArray } from '@theia/core/lib/browser/widgets/widget';
import { StatefulWidget, Navigatable } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { TheiaSprottyConnector } from '../sprotty/theia-sprotty-connector';
import { Container } from 'inversify';
import { TheiaDiagramServer } from '../sprotty/theia-diagram-server';

export interface DiagramWidgetOptions {
    uri: string
    diagramType: string
    label: string
    iconClass: string
}

export type DiagramWidgetFactory =
    (options: DiagramWidgetOptions, widgetId: string, diContainer: Container, connector?: TheiaSprottyConnector) => DiagramWidget;
export const DiagramWidgetFactory = Symbol('DiagramWidgetFactory');

export namespace DiagramWidgetOptions {
    export function is(options: any): options is DiagramWidgetOptions {
        return options.diagramType
            && options.uri
            && options.label
            && options.iconClass;
    }
}

/**
 * The DiagramWidget is the container for Sprotty diagrams.
 */
export class DiagramWidget extends BaseWidget implements StatefulWidget, Navigatable {

    private diagramContainer?: HTMLDivElement;
    private statusIconDiv?: HTMLDivElement;
    private statusMessageDiv?: HTMLDivElement;

    protected _actionDispatcher: IActionDispatcher;

    protected _modelSource: ModelSource;

    get uri(): URI {
        return new URI(this.options.uri);
    }

    get actionDispatcher(): IActionDispatcher {
        return this.diContainer.get(TYPES.IActionDispatcher);
    }

    get viewerOptions(): ViewerOptions {
        return this.diContainer.get(TYPES.ViewerOptions);
    }

    get modelSource(): ModelSource {
        return this._modelSource;
    }

    get clientId(): string {
        if (this._modelSource instanceof DiagramServerProxy)
            return this._modelSource.clientId;
        else
            return this.widgetId;
    }

    get id(): string {
        return this.widgetId;
    }

    constructor(protected options: DiagramWidgetOptions,
                readonly widgetId: string,
                readonly diContainer: Container,
                readonly connector?: TheiaSprottyConnector) {
        super();
        this.title.closable = true;
        this.title.label = options.label;
        this.title.iconClass = options.iconClass;
    }

    protected onAfterAttach(msg: Message): void {
        if (!this.diagramContainer) {
            // Create the container and initialize its content upon first attachment
            this.createContainer();
            this.initializeSprotty();
        }
        super.onAfterAttach(msg);
    }

    protected createContainer(): void {
        this.diagramContainer = document.createElement('div');
        this.diagramContainer.id = this.viewerOptions.baseDiv;
        this.node.appendChild(this.diagramContainer);

        const hiddenContainer = document.createElement('div');
        hiddenContainer.id = this.viewerOptions.hiddenDiv;
        document.body.appendChild(hiddenContainer);

        const statusDiv = document.createElement('div');
        statusDiv.setAttribute('class', 'sprotty-status');
        this.node.appendChild(statusDiv);

        this.statusIconDiv = document.createElement('div');
        statusDiv.appendChild(this.statusIconDiv);

        this.statusMessageDiv = document.createElement('div');
        this.statusMessageDiv.setAttribute('class', 'sprotty-status-message');
        statusDiv.appendChild(this.statusMessageDiv);
    }

    protected initializeSprotty(): void {
        const modelSource = this.diContainer.get<ModelSource>(TYPES.ModelSource);
        this._modelSource = modelSource;
        if (modelSource instanceof TheiaDiagramServer && this.connector)
            this.connector.connect(modelSource);
        this.disposed.connect(() => {
            if (modelSource instanceof TheiaDiagramServer && this.connector)
                this.connector.disconnect(modelSource);
        });
        this.requestModel();
    }

    protected async requestModel(): Promise<void> {
        try {
            const response = await this.actionDispatcher.request(RequestModelAction.create({
                sourceUri: this.options.uri,
                diagramType: this.options.diagramType
            }));
            await this.actionDispatcher.dispatch(response);
            await this.initializeView();
        } catch (err) {
            const status = new ServerStatusAction();
            status.message = err instanceof Error ? err.message : err.toString();
            status.severity = 'FATAL';
            this.setStatus(status);
        }
    }

    protected initializeView(): Promise<void> {
        return this.actionDispatcher.dispatch(CenterAction.create([], { animate: false }));
    }

    protected getBoundsInPage(element: Element) {
        const bounds = element.getBoundingClientRect();
        return {
            x: bounds.left,
            y: bounds.top,
            width: bounds.width,
            height: bounds.height
        };
    }

    protected onResize(msg: Widget.ResizeMessage): void {
        super.onResize(msg);
        const newBounds = this.getBoundsInPage(this.node as Element);
        this.actionDispatcher.dispatch(InitializeCanvasBoundsAction.create(newBounds));
    }

    protected onActivateRequest(msg: Message): void {
        super.onActivateRequest(msg);
        const svgElement = this.node.querySelector(`#${this.viewerOptions.baseDiv} svg`) as HTMLElement;
        if (svgElement !== null) {
            svgElement.focus();
        } else {
            const tabindex = this.node.getAttribute('tabindex');
            if (tabindex === null)
                this.node.setAttribute('tabindex', '-1');
            this.node.focus();
        }
    }

    /**
     * We cannot activate the widget before the SVG element is there, as it takes the focus.
     * This should happen within two animation frames, as the action dispatcher issues
     * a SetModelCommand in the constructor. OTOH, shell.activateWidget() is synchronous. So
     * after creating the widget and before activating it, we use this method to wait for the
     * SVG to be appended to the DOM.
     */
    async getSvgElement(): Promise<HTMLElement | undefined> {
        return new Promise<HTMLElement | undefined>((resolve) => {
            let frames = 0;
            const waitForSvg = () => {
                requestAnimationFrame(() => {
                    const svgElement = this.node.querySelector(`#${this.viewerOptions.baseDiv} svg`) as HTMLElement;
                    if (svgElement !== null)
                        resolve(svgElement);
                    else if (++frames < 5)
                        waitForSvg();
                    else
                        resolve(undefined);
                });
            };
            waitForSvg();
        });
    }

    setStatus(status: ServerStatusAction): void {
        if (this.statusMessageDiv) {
            this.statusMessageDiv.textContent = status.message;
            this.removeClasses(this.statusMessageDiv, 1);
            this.statusMessageDiv.classList.add(status.severity.toLowerCase());
        }
        if (this.statusIconDiv) {
            this.removeClasses(this.statusIconDiv, 0);
            const classes = this.statusIconDiv.classList;
            classes.add(status.severity.toLowerCase());
            switch (status.severity) {
                case 'FATAL':
                    classes.add(...codiconArray('error'));
                    break;
                case 'ERROR':
                    classes.add(...codiconArray('warning'));
                    break;
                case 'WARNING':
                    classes.add(...codiconArray('warning'));
                    break;
                case 'INFO':
                    classes.add(...codiconArray('info'));
                    break;
            }
        }
    }

    protected removeClasses(element: Element, keep: number) {
        const classes = element.classList;
        while (classes.length > keep) {
            const item = classes.item(classes.length - 1);
            if (item)
                classes.remove(item);
        }
    }

    storeState(): object {
        return this.options;
    }

    restoreState(oldState: object): void {
        if (DiagramWidgetOptions.is(oldState))
            this.options = oldState;
    }

    getResourceUri(): URI | undefined {
        return this.uri;
    }

    createMoveToUri(resourceUri: URI): URI | undefined {
        return this.uri.withPath(resourceUri.path);
    }
}
