/********************************************************************************
 * Copyright (c) 2018-2026 TypeFox and others.
 * Modifications: (c) 2019-2024 EclipseSource 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
 ********************************************************************************/
// based on: https://github.com/eclipse-sprotty/sprotty-theia/blob/v0.12.0/src/theia/diagram-manager.ts

import { codiconCSSString, EditMode, IDiagramOptions } from '@eclipse-glsp/client';
import { URI } from '@theia/core';
import {
    ApplicationShell,
    FrontendApplicationContribution,
    OpenHandler,
    OpenWithHandler,
    OpenWithService,
    WidgetFactory,
    WidgetOpenerOptions,
    WidgetOpenHandler
} from '@theia/core/lib/browser';
import { inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser';
import { DiagramServiceProvider } from '../diagram-service-provider';
import { TheiaOpenerOptionsNavigationService } from '../theia-opener-options-navigation-service';
import { DiagramConfiguration } from './glsp-diagram-configuration';
import { GLSPDiagramContextKeyService } from './glsp-diagram-context-key-service';
import { GLSPDiagramWidget, GLSPDiagramWidgetOptions } from './glsp-diagram-widget';

export function registerDiagramManager(
    bind: interfaces.Bind,
    diagramManagerServiceId: interfaces.ServiceIdentifier<GLSPDiagramManager>,
    bindToSelf = true
): void {
    if (bindToSelf) {
        bind(diagramManagerServiceId).toSelf().inSingletonScope();
    }
    bind(FrontendApplicationContribution).toService(diagramManagerServiceId);
    bind(OpenHandler).toService(diagramManagerServiceId);
    bind(WidgetFactory).toService(diagramManagerServiceId);
}

@injectable()
export abstract class GLSPDiagramManager extends WidgetOpenHandler<GLSPDiagramWidget> implements WidgetFactory, OpenWithHandler {
    @inject(TheiaOpenerOptionsNavigationService)
    protected readonly diagramNavigationService: TheiaOpenerOptionsNavigationService;

    @inject(GLSPDiagramContextKeyService)
    protected readonly contextKeyService: GLSPDiagramContextKeyService;

    @inject(DiagramServiceProvider)
    protected readonly diagramServiceProvider: DiagramServiceProvider;

    @inject(EditorManager)
    protected readonly editorManager: EditorManager;

    @inject(OpenWithService)
    protected openWithService: OpenWithService;

    abstract get fileExtensions(): string[];

    abstract get diagramType(): string;

    abstract get contributionId(): string;

    protected widgetCount = 0;

    protected registerOpenWithHandler = true;

    @postConstruct()
    protected override init(): void {
        super.init();
        if (this.registerOpenWithHandler) {
            this.openWithService.registerHandler(this);
        }
    }

    override async doOpen(widget: GLSPDiagramWidget, uri: URI, maybeOptions?: WidgetOpenerOptions): Promise<void> {
        const options: WidgetOpenerOptions = {
            mode: 'activate',
            ...maybeOptions
        };
        if (!widget.isAttached) {
            this.attachWidget(widget, options);
        }
        if (options.mode === 'activate') {
            await widget.getSvgElement();
            await this.shell.activateWidget(widget.id);
        } else if (options.mode === 'reveal') {
            await this.shell.revealWidget(widget.id);
        }
        if (this.handleNavigations(widget, options)) {
            return;
        }
    }

    protected attachWidget(widget: GLSPDiagramWidget, options?: WidgetOpenerOptions): void {
        const currentEditor = this.editorManager.currentEditor;
        const widgetOptions: ApplicationShell.WidgetOptions = {
            area: 'main',
            ...(options && options.widgetOptions ? options.widgetOptions : {})
        };
        if (!!currentEditor && currentEditor.editor.uri.toString(true) === widget.uri.toString(true)) {
            widgetOptions.ref = currentEditor;
            widgetOptions.mode =
                options && options.widgetOptions && options.widgetOptions.mode ? options.widgetOptions.mode : 'open-to-right';
        }
        this.shell.addWidget(widget, widgetOptions);
    }

    protected handleNavigations(widget: GLSPDiagramWidget, options?: WidgetOpenerOptions): boolean {
        const navigations = this.diagramNavigationService.determineNavigations(widget.uri.toString(true), options);
        if (navigations.length > 0) {
            widget.actionDispatcher.dispatchOnceModelInitialized(...navigations);
            return true;
        }
        return false;
    }

    async createWidget(options?: any): Promise<GLSPDiagramWidget> {
        if (GLSPDiagramWidgetOptions.is(options)) {
            const config = this.getDiagramConfiguration(options);
            const diagramOptions = this.createDiagramOptions(options);
            const diContainer = config.createContainer(diagramOptions);
            const diagramWidgetFactory = this.diagramServiceProvider.getDiagramWidgetFactory(this.diagramType);

            const widget = diagramWidgetFactory?.create(options, diContainer);
            widget.listenToFocusState(this.shell);
            return widget;
        }
        throw Error('DiagramWidgetFactory needs DiagramWidgetOptions but got ' + JSON.stringify(options));
    }

    protected createDiagramOptions(options: GLSPDiagramWidgetOptions): IDiagramOptions {
        return {
            clientId: this.createClientId(),
            diagramType: options.diagramType,
            sourceUri: options.uri,
            editMode: options.editMode,
            glspClientProvider: () => this.diagramServiceProvider.getGLSPClientContribution(this.contributionId).glspClient
        };
    }

    protected createClientId(): string {
        return this.diagramType + '_' + this.widgetCount++;
    }

    protected override createWidgetOptions(uri: URI, options?: GLSPWidgetOpenerOptions): GLSPDiagramWidgetOptions {
        return {
            diagramType: this.diagramType,
            kind: 'navigatable',
            uri: uri.toString(true),
            iconClass: this.iconClass,
            label: uri.path.base,
            editMode: options && options.editMode ? options.editMode : EditMode.EDITABLE
        };
    }

    protected getDiagramConfiguration(options: GLSPDiagramWidgetOptions): DiagramConfiguration {
        return this.diagramServiceProvider.getDiagramConfiguration(options.diagramType);
    }

    canHandle(uri: URI, _options?: WidgetOpenerOptions | undefined): number {
        for (const extension of this.fileExtensions) {
            if (uri.path.toString().endsWith(extension)) {
                return 1001;
            }
        }
        return 0;
    }

    override get id(): string {
        return deriveDiagramManagerId(this.diagramType);
    }

    get iconClass(): string {
        return codiconCSSString('type-hierarchy-sub');
    }

    get providerName(): string | undefined {
        return undefined;
    }
}

export interface GLSPWidgetOpenerOptions extends WidgetOpenerOptions {
    editMode?: string;
}

export function deriveDiagramManagerId(diagramType: string): string {
    return diagramType + '-diagram-manager';
}
