import { ComponentBase, ComponentState } from "./componentBase";
import { ColorSet } from "../dataModels";
import { applyIcon, applySwitcheroo } from "../designApplication";
import { MappingTween } from "../tweens/mappingTweens";
import { BaseTween } from "../tweens/baseTween";
import { KineticTween } from "../tweens/kineticTween";
import { pixelsPerRem } from "../app";
import { firstDefined } from "../utils";

export type SwitcherBarState = ComponentState & {
    _type_: "SwitcherBar-builtin";
    names?: string[];
    icons?: (string | null)[] | null;
    color?: ColorSet;
    orientation?: "horizontal" | "vertical";
    spacing?: number;
    allow_none: boolean;
    selectedName?: string | null;
};

export class SwitcherBarComponent extends ComponentBase {
    declare state: Required<SwitcherBarState>;

    private innerElement: HTMLElement; // Used for alignment
    private markerElement: HTMLElement; // Highlights the selected item
    private backgroundOptionsElement: HTMLElement; // Displays all options
    private markerOptionsElement: HTMLElement; // Options, but different color

    // Animation state
    private animationIsRunning: boolean = false;

    private fadeTween: BaseTween;
    private moveTween: BaseTween;

    private markerAtAnimationStart: [number, number, number, number] = [
        0, 0, 0, 0,
    ];

    private markerAtAnimationEnd: [number, number, number, number] = [
        0, 0, 0, 0,
    ];

    private markerCurrent: [number, number, number, number] = [0, 0, 0, 0];

    // Allows to determine whether this is the first time the element is being
    // updated.
    private isInitialized: boolean = false;

    // Used to update the marker should the element be resized
    private resizeObserver: ResizeObserver;

    createElement(): HTMLElement {
        // Create the elements
        let outerElement = document.createElement("div");
        outerElement.classList.add("rio-switcher-bar");

        // Centers the bar
        this.innerElement = document.createElement("div");
        outerElement.appendChild(this.innerElement);

        // Highlights the selected item
        this.markerElement = document.createElement("div");
        this.markerElement.classList.add("rio-switcher-bar-marker");

        // Prepare animations
        this.fadeTween = new MappingTween({
            mapping: (value: number) => value,
            duration: 0.18,
        });

        this.moveTween = new KineticTween({
            acceleration: 350 * pixelsPerRem,
        });

        // The marker needs updating when the element is resized
        this.resizeObserver = new ResizeObserver(this.onResize.bind(this));
        this.resizeObserver.observe(this.innerElement);

        return outerElement;
    }

    onDestruction(): void {
        super.onDestruction();
        this.resizeObserver.disconnect();
    }

    onResize(): void {
        // Update the marker position
        if (this.state.selectedName !== null) {
            this.markerAtAnimationEnd = this.getMarkerTarget()!;
            this.updateCssToMatchState();
        }

        // Pass on all of the allocated size to the marker options
        let backgroundOptionsRect =
            this.backgroundOptionsElement.getBoundingClientRect();
        this.markerOptionsElement.style.width = `${backgroundOptionsRect.width}px`;
        this.markerOptionsElement.style.height = `${backgroundOptionsRect.height}px`;
    }

    /// Update the HTML & CSS to match the current state
    updateCssToMatchState(): void {
        // Position the marker
        let t = this.moveTween.progress;

        for (let i = 0; i < 4; i++) {
            let start = this.markerAtAnimationStart[i];
            let delta = this.markerAtAnimationEnd[i] - start;
            this.markerCurrent[i] = start + delta * t;
        }

        // Account for the fade animation
        let fade = this.fadeTween.current;
        let markerCurWidth = this.markerCurrent[2] * fade;
        let markerCurHeight = this.markerCurrent[3] * fade;
        let markerCurLeft =
            this.markerCurrent[0] +
            (this.markerCurrent[2] - markerCurWidth) / 2;
        let markerCurTop =
            this.markerCurrent[1] +
            (this.markerCurrent[3] - markerCurHeight) / 2;

        // Move the marker
        this.markerElement.style.left = `${markerCurLeft}px`;
        this.markerElement.style.top = `${markerCurTop}px`;
        this.markerElement.style.width = `${markerCurWidth}px`;
        this.markerElement.style.height = `${markerCurHeight}px`;

        // The inner options are positioned relative to the marker. Move them in
        // the opposite direction so they stay put.
        this.markerOptionsElement.style.left = `-${markerCurLeft}px`;
        this.markerOptionsElement.style.top = `-${markerCurTop}px`;
    }

    animationWorker() {
        // Update the tweens
        let keepGoing = false;

        if (this.fadeTween.isRunning) {
            this.fadeTween.update();
            keepGoing = true;
        }

        if (this.moveTween.isRunning) {
            this.moveTween.update();
            keepGoing = true;
        }

        // Update the CSS
        this.updateCssToMatchState();

        // Keep going?
        if (keepGoing) {
            requestAnimationFrame(this.animationWorker.bind(this));
        } else {
            this.animationIsRunning = false;
        }
    }

    ensureAnimationIsRunning(): void {
        if (this.animationIsRunning) {
            return;
        }

        this.animationIsRunning = true;
        requestAnimationFrame(this.animationWorker.bind(this));
    }

    /// Start moving the marker to match the current state, taking care of
    /// animations and everything
    animateToCurrentTarget(): void {
        // Move the marker
        if (this.state.selectedName !== null) {
            this.markerAtAnimationStart = [...this.markerCurrent];
            this.markerAtAnimationEnd = this.getMarkerTarget()!;

            let animatedPosition =
                this.state.orientation == "horizontal"
                    ? this.markerAtAnimationEnd[0]
                    : this.markerAtAnimationEnd[1];

            // If the marker is currently completely invisible, teleport.
            if (this.fadeTween.current === 0) {
                this.moveTween.teleportTo(animatedPosition);
            } else {
                this.moveTween.transitionTo(animatedPosition);
            }
        }

        // Fade the marker in/out
        if (this.state.selectedName === null) {
            this.fadeTween.transitionTo(0);
        } else {
            this.fadeTween.transitionTo(1);
        }

        // Make sure there's somebody tending to the tweens
        this.ensureAnimationIsRunning();
    }

    /// If an item is selected, returns the position and size the marker should
    /// be in order to highlight the selected item. Returns `null` if no item is
    /// currently selected.
    getMarkerTarget(): [number, number, number, number] | null {
        // Nothing selected
        if (this.state.selectedName === null) {
            return null;
        }

        // Find the selected item
        let selectedIndex = this.state.names.indexOf(this.state.selectedName!);
        console.assert(
            selectedIndex !== -1,
            `Invalid name selected: ${this.state.selectedName} is not in ${this.state.names}`
        );

        // Find the location of the selected item.
        let optionElement =
            this.backgroundOptionsElement.children[selectedIndex];
        let optionRect = optionElement.getBoundingClientRect();
        let parentRect = this.innerElement.getBoundingClientRect();

        return [
            optionRect.left - parentRect.left,
            optionRect.top - parentRect.top,
            optionRect.width,
            optionRect.height,
        ];
    }

    onItemClick(event: MouseEvent, name: string): void {
        // If this item was already selected, the new value may be `None`
        if (this.state.selectedName === name) {
            if (this.state.allow_none) {
                this.state.selectedName = null;
            } else {
                return;
            }
        } else {
            this.state.selectedName = name;
        }

        // Update the marker
        this.animateToCurrentTarget();

        // Notify the backend
        this.sendMessageToBackend({
            name: this.state.selectedName,
        });

        // Eat the event
        event.stopPropagation();
    }

    buildContent(deltaState: SwitcherBarState): HTMLElement {
        let result = document.createElement("div");
        result.classList.add("rio-switcher-bar-options");
        result.style.gap = `${this.state.spacing}rem`;

        let names = deltaState.names ?? this.state.names;
        let icons = firstDefined(deltaState.icons, this.state.icons);

        // Iterate over both
        for (let i = 0; i < names.length; i++) {
            let name = names[i];

            let optionElement = document.createElement("div");
            optionElement.classList.add("rio-switcher-bar-option");
            result.appendChild(optionElement);

            // Icon
            if (icons !== null && icons[i] !== null) {
                let iconContainer = document.createElement("div");
                iconContainer.classList.add("rio-switcher-bar-icon");
                optionElement.appendChild(iconContainer);

                applyIcon(iconContainer, icons[i]!);
            }

            // Text
            let textElement = document.createElement("div");
            optionElement.appendChild(textElement);
            textElement.textContent = name;

            // Detect clicks
            optionElement.addEventListener("click", (event) =>
                this.onItemClick(event, name)
            );
        }

        return result;
    }

    updateElement(
        deltaState: SwitcherBarState,
        latentComponents: Set<ComponentBase>
    ): void {
        super.updateElement(deltaState, latentComponents);

        // Have the options changed?
        if (deltaState.names !== undefined || deltaState.icons !== undefined) {
            this.innerElement.innerHTML = "";
            this.markerElement.innerHTML = "";

            // Background options
            this.backgroundOptionsElement = this.buildContent(deltaState);
            this.innerElement.appendChild(this.backgroundOptionsElement);

            // Marker
            this.innerElement.appendChild(this.markerElement);

            // Marker options
            this.markerOptionsElement = this.buildContent(deltaState);
            this.markerElement.appendChild(this.markerOptionsElement);

            // Pass on all available space to the marker options
            requestAnimationFrame(() => {
                let backgroundOptionsRect =
                    this.backgroundOptionsElement.getBoundingClientRect();

                this.markerOptionsElement.style.width = `${backgroundOptionsRect.width}px`;
                this.markerOptionsElement.style.height = `${backgroundOptionsRect.height}px`;

                // Update the CSS
                this.updateCssToMatchState();
            });
        }

        // Color
        if (deltaState.color !== undefined) {
            applySwitcheroo(
                this.markerElement,
                deltaState.color === "keep" ? "bump" : deltaState.color
            );
        }

        // Orientation
        if (deltaState.orientation !== undefined) {
            let flexDirection =
                deltaState.orientation == "vertical" ? "column" : "row";

            this.element.style.flexDirection = flexDirection;
            this.backgroundOptionsElement.style.flexDirection = flexDirection;
            this.markerOptionsElement.style.flexDirection = flexDirection;
        }

        // Spacing
        if (deltaState.spacing !== undefined) {
            this.backgroundOptionsElement.style.gap = `${deltaState.spacing}rem`;
            this.markerOptionsElement.style.gap = `${deltaState.spacing}rem`;
        }

        // If the selection has changed make sure to move the marker
        if (deltaState.selectedName !== undefined) {
            if (this.isInitialized) {
                if (deltaState.selectedName !== this.state.selectedName) {
                    this.state.selectedName = deltaState.selectedName;
                    this.state.names = deltaState.names ?? this.state.names;
                    this.animateToCurrentTarget();
                }
            } else if (deltaState.selectedName === null) {
                this.fadeTween.teleportTo(0);
            } else {
                this.fadeTween.teleportTo(1);

                requestAnimationFrame(() => {
                    this.markerAtAnimationStart = this.markerAtAnimationEnd =
                        this.getMarkerTarget()!;

                    let animatedPosition =
                        this.state.orientation == "horizontal"
                            ? this.markerAtAnimationEnd[0]
                            : this.markerAtAnimationEnd[1];

                    this.moveTween.teleportTo(animatedPosition);
                    this.moveTween.update();
                    this.updateCssToMatchState();
                });
            }
        }

        // Any future updates are not the first
        this.isInitialized = true;
    }
}
