import { Object3D } from "three";

import type { Context } from "./engine_setup";
import { IComponent, isComponent } from "./engine_types.js";



/** Data describing the accessible semantics for a 3D object or component. */
type AccessibilityData = {
    /** ARIA role (e.g. `"button"`, `"img"`, `"region"`). */
    role: string;
    /** Human-readable label announced by screen readers. */
    label: string;
    /** When `true`, the element is hidden from the accessibility tree. */
    hidden?: boolean;
    /** When `true`, indicates the element's content is being updated. */
    busy?: boolean;
}

/**
 * Manages an accessible, screen-reader-friendly overlay for a Needle Engine {@link Context}.
 *
 * The manager maintains a visually-hidden DOM tree that mirrors relevant 3D scene objects
 * with appropriate ARIA roles and labels. It also provides a live region so that hover
 * events in the 3D scene can be announced to assistive technology without stealing focus.
 *
 * ## Automatic integration
 * Several built-in components register accessible elements automatically:
 * - {@link DragControls} — announces draggable objects and drag state
 * - {@link Button} — exposes UI buttons to the accessibility tree
 * - {@link Text} — exposes UI text content to screen readers
 * - {@link ChangeTransformOnClick} — announces clickable transform actions
 * - {@link ChangeMaterialOnClick} — announces clickable material changes
 * - {@link EmphasizeOnClick} — announces clickable emphasis effects
 * - {@link PlayAudioOnClick} — announces clickable audio playback
 * - {@link PlayAnimationOnClick} — announces clickable animation triggers
 *
 * ## What this unlocks
 * - Hovering over buttons and interactive objects with the cursor announces them to screen readers via an ARIA live region — no focus steal required
 * - Screen readers can discover and navigate interactive 3D objects in the scene
 * - Drag operations update the accessibility state (busy, label changes) in real time
 * - Custom components can participate by calling {@link updateElement}, {@link focus}, and {@link hover}
 *
 * Access the manager via `this.context.accessibility` from any component.
 */
export class AccessibilityManager {

    private static readonly _managers: WeakMap<object, AccessibilityManager> = new WeakMap();

    /** Returns the {@link AccessibilityManager} associated with the given context or component. */
    static get(obj: Context | IComponent) {
        if (isComponent(obj)) {
            return this._managers.get(obj.context);
        }
        else {
            return this._managers.get(obj);
        }
    }

    constructor(
        private readonly context: Context
    ) {
        this.root.style.cssText = `
            position: absolute;
            width: 1px; height: 1px;
            padding: 0; margin: -1px;
            overflow: hidden;
            clip: rect(0, 0, 0, 0);
            white-space: nowrap;
            border: 0;
        `
        this.root.setAttribute("role", "region");
        this.root.setAttribute("aria-label", "3D Needle Engine scene");

        // Live region for announcing hovered 3D elements without stealing focus
        this.liveRegion.setAttribute("aria-live", "polite");
        this.liveRegion.setAttribute("aria-atomic", "true");
        this.liveRegion.setAttribute("role", "status");
        this.root.appendChild(this.liveRegion);
        this.enabled = true;
    }

    private _enabled!: boolean;
    /** Enables or disables the accessibility overlay. When disabled, the overlay DOM is removed. */
    set enabled(value: boolean) {
        if (value === this._enabled) return;
        this._enabled = value;
        if (!value) {
            this.root.remove();
        }
        else {
            AccessibilityManager._managers.set(this.context, this);
            const target = this.context.domElement.shadowRoot || this.context.domElement;
            target.prepend(this.root);
        }
    }

    /** Removes all tracked accessibility elements, keeping only the live region. */
    clear() {
        this.root.childNodes.forEach(child => child.remove());
        this.root.appendChild(this.liveRegion);
    }

    /** Removes the overlay from the DOM and unregisters this manager from the context. */
    dispose() {
        this.root.remove();
        AccessibilityManager._managers.delete(this.context);
    }

    private readonly root: HTMLElement = document.createElement("div");
    private readonly liveRegion: HTMLElement = document.createElement("div");
    private readonly treeElements = new WeakMap<object, HTMLElement>();

    /**
     * Creates or updates the accessible DOM element for a 3D object or component.
     * @param obj - The scene object or component to represent.
     * @param data - Partial accessibility data (role, label, hidden, busy) to apply.
     */
    updateElement<T extends Object3D | IComponent>(obj: T, data: Partial<AccessibilityData>) {
        let el = this.treeElements.get(obj);
        if (!el) {
            el = document.createElement("div");
            this.treeElements.set(obj, el);
            this.root.appendChild(el);


            let didSetRole = false;

            if (typeof data === "object") {
                if (data.role) {
                    didSetRole = true;
                    el.setAttribute("role", data.role);
                }
                if (data.label) {
                    el.setAttribute("aria-label", data.label);
                }
                if (data.hidden !== undefined) {
                    el.setAttribute("aria-hidden", String(data.hidden));
                }
                if (data.busy !== undefined) {
                    el.setAttribute("aria-busy", String(data.busy));
                }
            }


            // if (didSetRole) {
            //     const role = el.getAttribute("role");
            //     if (role) {
            //         el.setAttribute("tabindex", "0");
            //     } else {
            //         el.removeAttribute("tabindex");
            //     }
            // }

        }
    }

    /** Moves keyboard focus to the accessible element representing the given object. */
    focus<T extends Object3D | IComponent>(obj: T) {
        const el = this.treeElements.get(obj);
        if (el) {
            // if (!el.hasAttribute("tabindex")) {
            //     el.setAttribute("tabindex", "0");
            // }
            el.focus();
        }
    }
    /** Removes keyboard focus from the accessible element representing the given object. */
    unfocus<T extends Object3D | IComponent>(obj: T) {
        const el = this.treeElements.get(obj);
        if (el) {
            el.blur();
        }
    }

    /**
     * Announces a hover event to screen readers via the ARIA live region.
     * @param obj - The hovered object (used to look up its label if `text` is not provided).
     * @param text - Optional text to announce. Falls back to the element's `aria-label`.
     */
    hover<T extends Object3D | IComponent>(obj: T, text?: string) {
        const el = this.treeElements.get(obj);
        // Update the live region text — screen reader announces this without stealing focus
        this.liveRegion.textContent = text || el?.getAttribute("aria-label") || "";
    }

    /** Removes the accessible DOM element for the given object and stops tracking it. */
    removeElement(obj: Object3D | IComponent) {
        const el = this.treeElements.get(obj);
        el?.remove();
        this.treeElements.delete(obj);
    }

    private set liveRegionMode(mode: "polite" | "assertive") {
        this.liveRegion.setAttribute("aria-live", mode);
    }

}