/*
 * Copyright (c) 2015-2018, IGN France.
 * Copyright (c) 2018-2026, Giro3D team.
 * SPDX-License-Identifier: MIT
 */

import GUI from 'lil-gui';

import type Instance from '../core/Instance';
import type Panel from './Panel';

import { isDisposable } from '../core/Disposable';
import DrawToolPanel from './DrawToolPanel';
import EntityPanel from './EntityPanel';
import InstanceInspector from './InstanceInspector';
import Outliner from './outliner/Outliner';
import PackageInfoInspector from './PackageInfoInspector';
import ProcessingInspector from './ProcessingInspector';
import SunExposurePanel from './SunExposurePanel';
import ViewInspector from './ViewInspector';

// Here follows the style adaptation to lil-gui
const styles = `
.lil-gui .title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: monospace;
}
`;

function visit(root: Element, callbackFn: (element: Element) => void): void {
    if (root != null) {
        callbackFn(root);
        if (root.childElementCount > 0) {
            for (const child of root.children) {
                visit(child, callbackFn);
            }
        }
    }
}

const styleSheet = document.createElement('style');
styleSheet.type = 'text/css';
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);

export interface InspectorOptions {
    /**
     * The panel width, in pixels.
     *
     * @defaultValue 450
     */
    width?: number;
    /**
     * The title of the inspector.
     *
     * @defaultValue Inspector
     */
    title?: string;
}

/**
 * Provides a user interface to inspect and edit the Giro3D scene.
 * The inspector is made of several {@link Panel}.
 * You can implement custom panels and add them to the inspector with
 * {@link Inspector.addPanel}.
 *
 */
class Inspector {
    public instance: Instance;
    public gui: GUI;
    public folders: Panel[];

    /**
     * Creates an instance of the inspector.
     *
     * @param parent - The HTML element to attach the panel to, or the `id` of this element.
     * @param instance - The Giro3D instance.
     * @param options - The options.
     */
    public constructor(
        parent: HTMLElement | string,
        instance: Instance,
        options: InspectorOptions = {},
    ) {
        this.instance = instance;
        this.gui = new GUI({
            autoPlace: false,
            width: options.width ?? 450,
            title: options.title ?? 'Inspector',
        });
        this.gui.close();
        this.gui.add(this, 'collapse');

        if (typeof parent === 'string') {
            parent = document.getElementById(parent) as HTMLElement;
        }

        parent.appendChild(this.gui.domElement);

        instance.addEventListener('update-end', () => this.update());

        this.folders = [];

        this.addPanel(new PackageInfoInspector(this.gui, instance));
        this.addPanel(new InstanceInspector(this.gui, instance));
        this.addPanel(new ViewInspector(this.gui, instance));
        this.addPanel(new DrawToolPanel(this.gui, instance));
        this.addPanel(new SunExposurePanel(this.gui, instance));
        this.addPanel(new ProcessingInspector(this.gui, instance));
        this.addPanel(new EntityPanel(this.gui, instance));
        this.addPanel(new Outliner(this.gui, instance));
    }

    public collapse(): void {
        this.folders.forEach(f => f.collapse());
    }

    /**
     * Removes all panel from the inspector.
     *
     */
    public clearPanels(): void {
        while (this.folders.length > 0) {
            const gui = this.folders.pop();
            if (isDisposable(gui)) {
                gui.dispose();
            }
        }
    }

    /**
     * Adds a panel to the inspector.
     *
     * @param panel - The panel to add.
     */
    public addPanel(panel: Panel): void {
        this.folders.push(panel);
    }

    /**
     * Attaches the inspector to the specified DOM element.
     *
     * @param parent - The element to attach the panel to, or the `id` to the element.
     * @param instance - The Giro3D instance.
     * @param options - The options.
     * @returns The created inspector.
     */
    public static attach(
        parent: HTMLElement | string,
        instance: Instance,
        options: InspectorOptions = {},
    ): Inspector {
        const inspector = new Inspector(parent, instance, options);
        return inspector;
    }

    /**
     * Detach this Inspector from its instance.
     *
     */
    public detach(): void {
        this.clearPanels();
        this.instance.removeEventListener('update-end', () => this.update());
        this.gui.domElement.remove();
    }

    public update(): void {
        this.folders.forEach(f => f.update());

        // Remove autocomplete on all Input elements to avoid causing issues on some browsers
        // See https://gitlab.com/giro3d/giro3d/-/issues/526
        // Note: we have to do it for each iteration because the content of the inspector can change
        // over time (e.g entities added and removed)
        visit(this.gui.domElement, element => {
            if (element instanceof HTMLInputElement) {
                element.autocomplete = 'off';
            }
        });
    }
}

export default Inspector;
