// Part of the Chili3d Project, under the AGPL-3.0 License.
// See LICENSE file in the project root for full license information.

import {
    type AsyncController,
    type CursorType,
    type I18nKeys,
    type IDisposable,
    type IDocument,
    type IEventHandler,
    type INode,
    type INodeFilter,
    type ISelection,
    type IShapeFilter,
    Logger,
    NodeSelectionHandler,
    PubSub,
    ShapeNode,
    type ShapeType,
    ShapeTypes,
    SubshapeSelectionHandler,
    VisualNode,
    type VisualState,
    VisualStates,
} from "@chili3d/core";

export class Selection implements ISelection, IDisposable {
    private _selectedNodes: INode[] = [];
    private _unselectedNodes: INode[] = [];

    shapeType: ShapeType = ShapeTypes.shape;
    shapeFilter?: IShapeFilter;
    nodeFilter?: INodeFilter;

    constructor(readonly document: IDocument) {}

    async pickShape(
        prompt: I18nKeys,
        controller: AsyncController,
        multiMode: boolean,
        selectedState: VisualState = VisualStates.edgeSelected,
        highlightState: VisualState = VisualStates.edgeHighlight,
    ) {
        const handler = new SubshapeSelectionHandler(
            this.document,
            this.shapeType,
            multiMode,
            controller,
            this.shapeFilter,
            this.nodeFilter,
        );
        handler.selectedState = selectedState;
        handler.highlightState = highlightState;
        await this.pickAsync(handler, prompt, controller, multiMode);
        return handler.shapes();
    }

    async pickNode(prompt: I18nKeys, controller: AsyncController, multiMode: boolean) {
        const handler = new NodeSelectionHandler(this.document, multiMode, controller, this.nodeFilter);
        await this.pickAsync(handler, prompt, controller, multiMode);
        return handler.nodes();
    }

    async pickAsync(
        handler: IEventHandler,
        prompt: I18nKeys,
        controller: AsyncController,
        showControl: boolean,
        cursor: CursorType = "select.default",
    ) {
        const oldHandler = this.document.visual.eventHandler;
        this.document.visual.eventHandler = handler;
        PubSub.default.pub("viewCursor", cursor);
        PubSub.default.pub("statusBarTip", prompt);
        if (showControl) PubSub.default.pub("showSelectionControl", controller);

        await Promise.try(
            () =>
                new Promise((resolve, reject) => {
                    controller.onCompleted(resolve);
                    controller.onCancelled(reject);
                }),
        )
            .catch((e) => {
                Logger.debug("pick status: ", e);
            })
            .finally(() => {
                if (showControl) PubSub.default.pub("clearSelectionControl");
                PubSub.default.pub("clearStatusBarTip");
                this.document.visual.eventHandler = oldHandler;
                PubSub.default.pub("viewCursor", "default");
            });
    }

    dispose(): void {
        this._selectedNodes.length = 0;
        this._unselectedNodes.length = 0;
    }

    getSelectedNodes(): INode[] {
        return this._selectedNodes;
    }

    setSelection(nodes: INode[], toggle: boolean) {
        nodes = nodes.filter(this.shapeNodeFilter);
        if (toggle) {
            this.toggleSelectPublish(nodes, true);
        } else {
            this.removeSelectedPublish(this._selectedNodes, false);
            this.addSelectPublish(nodes, true);
        }
        return this._selectedNodes.length;
    }

    private readonly shapeNodeFilter = (x: INode) => {
        if (x instanceof ShapeNode) {
            const shape = x.shape.value;
            if (!shape || !this.shapeFilter) return true;
            return this.shapeFilter.allow(shape);
        }

        if (this.nodeFilter) {
            return this.nodeFilter.allow(x);
        }

        return true;
    };

    deselect(nodes: INode[]) {
        this.removeSelectedPublish(nodes, true);
    }

    clearSelection() {
        this.removeSelectedPublish(this._selectedNodes, true);
    }

    private updateSelection() {
        this.document.visual.update();
        PubSub.default.pub("selectionChanged", this.document, this._selectedNodes, this._unselectedNodes);
    }

    private toggleSelectPublish(nodes: INode[], publish: boolean) {
        const selected = nodes.filter((m) => this._selectedNodes.includes(m));
        const unSelected = nodes.filter((m) => !this._selectedNodes.includes(m));
        this.removeSelectedPublish(selected, false);
        this.addSelectPublish(unSelected, publish);
    }

    private addSelectPublish(nodes: INode[], publish: boolean) {
        nodes.forEach((m) => {
            if (m instanceof VisualNode) {
                const visual = this.document.visual.context.getVisual(m);
                if (visual)
                    this.document.visual.highlighter.addState(
                        visual,
                        VisualStates.edgeSelected,
                        ShapeTypes.shape,
                    );
            }
        });
        this._selectedNodes.push(...nodes);
        if (publish) this.updateSelection();
    }

    private removeSelectedPublish(nodes: INode[], publish: boolean) {
        for (const node of nodes) {
            if (node instanceof VisualNode) {
                const visual = this.document.visual.context.getVisual(node);
                if (visual)
                    this.document.visual.highlighter.removeState(
                        visual,
                        VisualStates.edgeSelected,
                        ShapeTypes.shape,
                    );
            }
        }
        this._selectedNodes = this._selectedNodes.filter((m) => !nodes.includes(m));
        this._unselectedNodes = nodes;
        if (publish) this.updateSelection();
    }
}
