// *****************************************************************************
// Copyright (C) 2023 Ericsson 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { injectable, postConstruct } from '@theia/core/shared/inversify';
import { TreeModelImpl, CompositeTreeNode, SelectableTreeNode, DepthFirstTreeIterator, TreeNode } from '@theia/core/lib/browser';
import { Emitter, nls } from '@theia/core';
import { TerminalManagerTreeTypes, SpecialPageConfig } from './terminal-manager-types';

@injectable()
export class TerminalManagerTreeModel extends TreeModelImpl {
    activePageNode: TerminalManagerTreeTypes.PageNode | undefined;
    activeGroupNode: TerminalManagerTreeTypes.TerminalGroupNode | undefined;
    activeTerminalNode: TerminalManagerTreeTypes.TerminalNode | undefined;

    protected onDidChangeTreeSelectionEmitter = new Emitter<TerminalManagerTreeTypes.SelectionChangedEvent>();
    readonly onDidChangeTreeSelection = this.onDidChangeTreeSelectionEmitter.event;

    protected onDidAddPageEmitter = new Emitter<{ pageId: TerminalManagerTreeTypes.PageId, terminalKey: TerminalManagerTreeTypes.TerminalKey }>();
    readonly onDidAddPage = this.onDidAddPageEmitter.event;
    protected onDidDeletePageEmitter = new Emitter<TerminalManagerTreeTypes.PageId>();
    readonly onDidDeletePage = this.onDidDeletePageEmitter.event;

    protected onDidRenameNodeEmitter = new Emitter<TerminalManagerTreeTypes.TerminalManagerTreeNode>();
    readonly onDidRenameNode = this.onDidRenameNodeEmitter.event;

    protected onDidAddTerminalGroupEmitter = new Emitter<{
        groupId: TerminalManagerTreeTypes.GroupId,
        pageId: TerminalManagerTreeTypes.PageId,
        terminalKey: TerminalManagerTreeTypes.TerminalKey,
    }>();
    readonly onDidAddTerminalGroup = this.onDidAddTerminalGroupEmitter.event;

    protected onDidDeleteTerminalGroupEmitter = new Emitter<TerminalManagerTreeTypes.GroupId>();
    readonly onDidDeleteTerminalGroup = this.onDidDeleteTerminalGroupEmitter.event;

    protected onDidAddTerminalToGroupEmitter = new Emitter<{
        terminalId: TerminalManagerTreeTypes.TerminalKey,
        groupId: TerminalManagerTreeTypes.GroupId,
    }>();
    readonly onDidAddTerminalToGroup = this.onDidAddTerminalToGroupEmitter.event;

    protected onDidDeleteTerminalFromGroupEmitter = new Emitter<{
        terminalId: TerminalManagerTreeTypes.TerminalKey,
        groupId: TerminalManagerTreeTypes.GroupId,
    }>();
    readonly onDidDeleteTerminalFromGroup = this.onDidDeleteTerminalFromGroupEmitter.event;

    protected readonly specialPageConfigs = new Map<string, SpecialPageConfig>();

    @postConstruct()
    protected override init(): void {
        super.init();
        this.toDispose.push(this.selectionService.onSelectionChanged(selectionEvent => {
            const selectedNode = selectionEvent.find(node => node.selected);
            if (selectedNode) {
                this.handleSelectionChanged(selectedNode);
            }
        }));
        this.root = { id: 'root', parent: undefined, children: [], visible: false } as CompositeTreeNode;
        this.configureSpecialPages();
    }

    /**
     * Register the default special pages. Subclasses can override this to
     * add, remove, or replace entries before the model is used.
     */
    protected configureSpecialPages(): void {
        this.registerSpecialPage('task', { pageId: 'page-tasks' as TerminalManagerTreeTypes.PageId, label: nls.localizeByDefault('Tasks'), icon: 'tasklist' });
        this.registerSpecialPage('debug', { pageId: 'page-debug' as TerminalManagerTreeTypes.PageId, label: nls.localizeByDefault('Debug'), icon: 'debug-alt' });
    }

    registerSpecialPage(kind: string, config: SpecialPageConfig): void {
        this.specialPageConfigs.set(kind, config);
    }

    getSpecialPageConfig(kind: string): SpecialPageConfig | undefined {
        return this.specialPageConfigs.get(kind);
    }

    getSpecialPageConfigs(): ReadonlyMap<string, SpecialPageConfig> {
        return this.specialPageConfigs;
    }

    addTerminalPage(
        terminalKey: TerminalManagerTreeTypes.TerminalKey,
        groupId: TerminalManagerTreeTypes.GroupId,
        pageId: TerminalManagerTreeTypes.PageId,
        label?: string,
    ): void {
        const { page: pageNode, isNewlyCreated } = this.getOrCreatePage(pageId);
        const groupNode = this.createGroupNode(groupId, pageId);
        const terminalNode = this.createTerminalNode(terminalKey, groupId, label);
        if (this.root && CompositeTreeNode.is(this.root)) {
            this.activePageNode = pageNode;
            CompositeTreeNode.addChild(groupNode, terminalNode);
            CompositeTreeNode.addChild(pageNode, groupNode);
            if (isNewlyCreated) {
                this.root = CompositeTreeNode.addChild(this.root, pageNode);
                this.onDidAddPageEmitter.fire({ pageId: pageNode.id, terminalKey });
            }
            this.onDidAddTerminalGroupEmitter.fire({ groupId: groupNode.id, pageId: pageNode.id, terminalKey });
            this.refreshWithSelection(pageNode, terminalNode);
        }
    }

    protected createPageNode(pageId: TerminalManagerTreeTypes.PageId): TerminalManagerTreeTypes.PageNode {
        const currentPageNumber = this.getNextPageCounter();
        return {
            id: pageId,
            label: `${nls.localize('theia/terminal-manager/page', 'Page')} (${currentPageNumber})`,
            parent: undefined,
            selected: false,
            children: [],
            page: true,
            isEditing: false,
            expanded: true,
            counter: currentPageNumber,
        };
    }

    protected getNextPageCounter(): number {
        return Math.max(0, ...Array.from(this.pages.values(), page => page.counter > 0 ? page.counter : 0)) + 1;
    }

    deleteTerminalPage(pageId: TerminalManagerTreeTypes.PageId): void {
        const pageNode = this.getNode(pageId);
        if (TerminalManagerTreeTypes.isPageNode(pageNode) && CompositeTreeNode.is(this.root)) {
            const isActive = this.activePageNode === pageNode;
            const fallback = isActive
                ? this.findSelection(pageNode.previousSibling) ?? this.findSelection(pageNode.nextSibling)
                : undefined;
            this.onDidDeletePageEmitter.fire(pageNode.id);
            CompositeTreeNode.removeChild(this.root, pageNode, this.tree);
            this.refreshWithSelection(this.root, fallback);
        }
    }

    addTerminalGroup(
        terminalKey: TerminalManagerTreeTypes.TerminalKey,
        groupId: TerminalManagerTreeTypes.GroupId,
        pageId: TerminalManagerTreeTypes.PageId,
        label?: string,
    ): void {
        const groupNode = this.createGroupNode(groupId, pageId);
        const terminalNode = this.createTerminalNode(terminalKey, groupId, label);
        const pageNode = this.getNode(pageId);
        if (this.root && CompositeTreeNode.is(this.root) && TerminalManagerTreeTypes.isPageNode(pageNode)) {
            this.onDidAddTerminalGroupEmitter.fire({ groupId: groupNode.id, pageId, terminalKey });
            CompositeTreeNode.addChild(groupNode, terminalNode);
            CompositeTreeNode.addChild(pageNode, groupNode);
            this.refreshWithSelection(pageNode, terminalNode);
        }
    }

    protected createGroupNode(
        groupId: TerminalManagerTreeTypes.GroupId,
        pageId: TerminalManagerTreeTypes.PageId,
    ): TerminalManagerTreeTypes.TerminalGroupNode {
        const currentGroupNum = this.getNextGroupCounterForPage(pageId);
        return {
            id: groupId,
            label: `${nls.localizeByDefault('Group')} (${currentGroupNum})`,
            parent: undefined,
            selected: false,
            children: [],
            terminalGroup: true,
            isEditing: false,
            parentPageId: pageId,
            expanded: true,
            counter: currentGroupNum,
        };
    }

    protected getNextGroupCounterForPage(pageId: TerminalManagerTreeTypes.PageId): number {
        const page = this.pages.get(pageId);
        if (page) {
            return Math.max(0, ...page.children.map(group => group.counter)) + 1;
        }
        return 1;
    }

    deleteTerminalGroup(groupId: TerminalManagerTreeTypes.GroupId): void {
        const groupNode = this.tree.getNode(groupId);
        const parentPageNode = groupNode?.parent;
        if (TerminalManagerTreeTypes.isGroupNode(groupNode) && TerminalManagerTreeTypes.isPageNode(parentPageNode)) {
            if (parentPageNode.children.length === 1) {
                this.deleteTerminalPage(parentPageNode.id);
            } else {
                const isActive = this.activeGroupNode === groupNode;
                const fallback = isActive
                    ? this.findSelection(groupNode.previousSibling) ?? this.findSelection(groupNode.nextSibling)
                    : undefined;
                this.doDeleteTerminalGroup(groupNode, parentPageNode);
                this.refreshWithSelection(parentPageNode, fallback);
            }
        }
    }

    protected doDeleteTerminalGroup(group: TerminalManagerTreeTypes.TerminalGroupNode, page: TerminalManagerTreeTypes.PageNode): void {
        this.onDidDeleteTerminalGroupEmitter.fire(group.id);
        CompositeTreeNode.removeChild(page, group, this.tree);
    }

    addTerminal(newTerminalId: TerminalManagerTreeTypes.TerminalKey, groupId: TerminalManagerTreeTypes.GroupId, label?: string): void {
        const groupNode = this.getNode(groupId);
        if (groupNode && TerminalManagerTreeTypes.isGroupNode(groupNode)) {
            const terminalNode = this.createTerminalNode(newTerminalId, groupId, label);
            CompositeTreeNode.addChild(groupNode, terminalNode);
            this.onDidAddTerminalToGroupEmitter.fire({ terminalId: newTerminalId, groupId });
            this.refreshWithSelection(undefined, terminalNode);
        }
    }

    createTerminalNode(
        terminalId: TerminalManagerTreeTypes.TerminalKey,
        groupId: TerminalManagerTreeTypes.GroupId,
        label?: string,
    ): TerminalManagerTreeTypes.TerminalNode {
        return {
            id: terminalId,
            label: label || nls.localizeByDefault('Terminal'),
            parent: undefined,
            children: [],
            selected: false,
            terminal: true,
            isEditing: false,
            parentGroupId: groupId,
        };
    }

    deleteTerminalNode(terminalId: TerminalManagerTreeTypes.TerminalKey): void {
        const terminalNode = this.getNode(terminalId);
        const parentGroupNode = terminalNode?.parent;
        if (TerminalManagerTreeTypes.isTerminalNode(terminalNode) && TerminalManagerTreeTypes.isGroupNode(parentGroupNode)) {
            if (parentGroupNode.children.length === 1) {
                this.deleteTerminalGroup(parentGroupNode.id);
            } else {
                const isActive = this.activeTerminalNode === terminalNode;
                const fallback = isActive
                    ? this.findSelection(terminalNode.previousSibling) ?? this.findSelection(terminalNode.nextSibling)
                    : undefined;
                this.doDeleteTerminalNode(terminalNode, parentGroupNode);
                this.refreshWithSelection(parentGroupNode, fallback);
            }
        }
    }

    protected doDeleteTerminalNode(node: TerminalManagerTreeTypes.TerminalNode, parent: TerminalManagerTreeTypes.TerminalGroupNode): void {
        this.onDidDeleteTerminalFromGroupEmitter.fire({
            terminalId: node.id,
            groupId: parent.id,
        });
        CompositeTreeNode.removeChild(parent, node, this.tree);
    }

    toggleRenameTerminal(entityId: TerminalManagerTreeTypes.TerminalManagerValidId): void {
        const node = this.getNode(entityId);
        if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node)) {
            node.isEditing = true;
            this.fireChanged();
        }
    }

    acceptRename(nodeId: string, newName: string): void {
        const node = this.getNode(nodeId);
        if (TerminalManagerTreeTypes.isTerminalManagerTreeNode(node)) {
            const trimmedName = newName.trim();
            node.label = trimmedName === '' ? node.label : newName;
            node.isEditing = false;
            this.fireChanged();
            this.onDidRenameNodeEmitter.fire(node);
        }
    }

    handleSelectionChanged(selectedNode: SelectableTreeNode): void {
        let activeTerminal: TerminalManagerTreeTypes.TerminalNode | undefined = undefined;
        let activeGroup: TerminalManagerTreeTypes.TerminalGroupNode | undefined = undefined;
        let activePage: TerminalManagerTreeTypes.PageNode | undefined = undefined;
        if (TerminalManagerTreeTypes.isTerminalNode(selectedNode)) {
            activeTerminal = selectedNode;
            const { parent } = activeTerminal;
            if (TerminalManagerTreeTypes.isGroupNode(parent)) {
                activeGroup = parent;
                const grandparent = activeGroup.parent;
                if (TerminalManagerTreeTypes.isPageNode(grandparent)) {
                    activePage = grandparent;
                }
            } else if (TerminalManagerTreeTypes.isPageNode(parent)) {
                activePage = parent;
            }
        } else if (TerminalManagerTreeTypes.isGroupNode(selectedNode)) {
            const { parent } = selectedNode;
            activeGroup = selectedNode;
            if (TerminalManagerTreeTypes.isPageNode(parent)) {
                activePage = parent;
            }
        } else if (TerminalManagerTreeTypes.isPageNode(selectedNode)) {
            activePage = selectedNode;
        }

        this.activeTerminalNode = activeTerminal;
        this.activeGroupNode = activeGroup;
        this.activePageNode = activePage;
        this.onDidChangeTreeSelectionEmitter.fire({
            activePageId: activePage?.id,
            activeTerminalId: activeTerminal?.id,
            activeGroupId: activeGroup?.id,
        });
    }

    get pages(): Map<TerminalManagerTreeTypes.PageId, TerminalManagerTreeTypes.PageNode> {
        const pages = new Map<TerminalManagerTreeTypes.PageId, TerminalManagerTreeTypes.PageNode>();
        if (!this.root) {
            return pages;
        }
        for (const node of new DepthFirstTreeIterator(this.root)) {
            if (TerminalManagerTreeTypes.isPageNode(node)) {
                pages.set(node.id, node);
            }
        }
        return pages;
    }

    getPageIdForTerminal(terminalKey: TerminalManagerTreeTypes.TerminalKey): TerminalManagerTreeTypes.PageId | undefined {
        const terminalNode = this.getNode(terminalKey);
        if (!TerminalManagerTreeTypes.isTerminalNode(terminalNode)) {
            return undefined;
        }
        const { parentGroupId } = terminalNode;
        const groupNode = this.getNode(parentGroupId);
        if (!TerminalManagerTreeTypes.isGroupNode(groupNode)) {
            return undefined;
        }
        return groupNode.parentPageId;
    }

    selectTerminalNode(terminalKey: TerminalManagerTreeTypes.TerminalKey): void {
        const node = this.getNode(terminalKey);
        if (node && TerminalManagerTreeTypes.isTerminalNode(node)) {
            this.selectNode(node);
        }
    }

    updateTerminalLabel(terminalKey: TerminalManagerTreeTypes.TerminalKey, newLabel: string): void {
        const node = this.getNode(terminalKey);
        if (TerminalManagerTreeTypes.isTerminalNode(node)) {
            node.label = newLabel;
            this.fireChanged();
        }
    }

    protected async refreshWithSelection(refreshTarget?: CompositeTreeNode, selectionTarget?: SelectableTreeNode): Promise<void> {
        await this.refresh(refreshTarget);
        if (selectionTarget) {
            return this.selectNode(selectionTarget);
        }
    }

    protected findSelection(start?: TreeNode): SelectableTreeNode | undefined {
        if (!start) { return undefined; }
        if (TerminalManagerTreeTypes.isTerminalNode(start)) { return start; }
        if (TerminalManagerTreeTypes.isGroupNode(start)) { return start.children.at(0); }
        if (TerminalManagerTreeTypes.isPageNode(start)) { return start.children.at(0)?.children.at(0); }
    }

    /**
     * Get or create a page by ID. If a special page config exists for this ID,
     * its label is used; otherwise an auto-numbered label is generated.
     *
     * An existing page is only reused if it is currently attached to the root
     * of the tree. A page that has been detached (e.g., because its last
     * terminal was removed) is treated as missing and recreated.
     */
    protected getOrCreatePage(pageId: TerminalManagerTreeTypes.PageId): { page: TerminalManagerTreeTypes.PageNode, isNewlyCreated: boolean } {
        const existing = this.getNode(pageId);
        if (TerminalManagerTreeTypes.isPageNode(existing) && existing.parent === this.root) {
            return { page: existing, isNewlyCreated: false };
        }
        // Check if this is a special page with a configured label
        const specialConfig = [...this.specialPageConfigs.values()].find(c => c.pageId === pageId);
        const pageNode = specialConfig
            ? this.createSpecialPageNode(pageId, specialConfig.label)
            : this.createPageNode(pageId);
        return { page: pageNode, isNewlyCreated: true };
    }

    protected createSpecialPageNode(pageId: TerminalManagerTreeTypes.PageId, label: string): TerminalManagerTreeTypes.PageNode {
        return {
            id: pageId,
            label,
            parent: undefined,
            selected: false,
            children: [],
            page: true,
            isEditing: false,
            expanded: true,
            counter: 0,
        };
    }

}
