import TrackUnit, { TrackUnitEvent, TrackUnitStickingType, TrackUnitType } from "@/TrackUnit";
import "./TrackUnit.css";
import { Capsule, h, IPublisher, ISubscriber, ISubscription, Publisher, Rung, RungOptions } from "@djledda/ladder";
import IconView, { IconName } from "@/ui/Widgets/Icon/IconView";

export type TrackUnitUINodeOptions = RungOptions & {
    trackUnit: TrackUnit,
};

const EventTypeSubscriptions = [
    TrackUnitEvent.On,
    TrackUnitEvent.Off,
    TrackUnitEvent.TypeChange,
];
type EventTypeSubscriptions = typeof EventTypeSubscriptions[number];

export const StickingTypeIconMap = {
    none: null,
    lf: 'lf', 
    lh: 'lh',
    rf: 'rf',
    rh: 'rh',
} as const satisfies Readonly<Record<TrackUnitStickingType, IconName | null>>;

const TypeClasses = [ "Ghost", "Accent" ] as const;
export const TrackUnitTypeClassMap = {
    "Normal": [],
    "GhostNote": ["Ghost"],
    "Accent": ["Accent"],
    "GhostNoteAccent": ["Ghost", "Accent"],
} as const satisfies Readonly<Record<TrackUnitType, Readonly<string[]>>>;

export default class TrackUnitView extends Rung<HTMLDivElement> implements ISubscriber<EventTypeSubscriptions> {
    private trackUnit: TrackUnit;
    private subscription: ISubscription | null = null;
    private publisher: IPublisher<TrackUnitEvent> = new Publisher<TrackUnitEvent, TrackUnitView>(this);
    private rotationTimeout: ReturnType<typeof setTimeout> | null = null;
    private mouseDownListeners: ((ev: MouseEvent) => void)[] = [];
    private hoverListeners: ((ev: MouseEvent) => void)[] = [];
    private blockNextMouseUp = false;
    private icon: IconView = new IconView({ iconName: 'list' });

    constructor(options: TrackUnitUINodeOptions) {
        super(options);
        this.trackUnit = options.trackUnit;
        this.setUnit(this.trackUnit);
    }

    setUnit(trackUnit: TrackUnit | null): void {
        if (trackUnit) {
            this.trackUnit = trackUnit;
            this.setupBindings();
            this.notify(this.publisher, trackUnit.isOn() ? TrackUnitEvent.On : TrackUnitEvent.Off);
            this.notify(this.publisher, TrackUnitEvent.TypeChange);
        } else {
            this.subscription?.unbind();
        }
    }

    private setupBindings() {
        this.subscription?.unbind();
        this.subscription = this.trackUnit.addSubscriber(this, EventTypeSubscriptions);
        this.hoverListeners.forEach(listener => this.render().removeEventListener("mouseover", listener));
        this.mouseDownListeners.forEach(listener => this.render().removeEventListener("mousedown", listener));
        this.redraw();
        this.hoverListeners.forEach(listener => this.render().addEventListener("mouseover", listener));
        this.mouseDownListeners.forEach(listener => this.render().addEventListener("mousedown", listener));
        this.render().addEventListener("mousedown", (ev) => this.handleMouseDown(ev));
        this.render().addEventListener("mouseout", (ev) => this.handleMouseOut(ev));
        this.render().addEventListener("mouseup", (ev) => this.handleMouseUp(ev));
        this.render().addEventListener("touchstart", (ev) => this.handleTouchStart(ev));
        this.render().addEventListener("touchend", (ev) => this.handleTouchEnd(ev));
    }

    private handleMouseDown(ev: MouseEvent): void {
        if (ev.button === 1) {
            this.trackUnit.rotateType();
        }
    }

    private handleMouseOut(ev: MouseEvent): void {
        if (this.rotationTimeout) {
            clearTimeout(this.rotationTimeout);
            this.rotationTimeout = null;
        }
    }

    private handleMouseUp(ev: MouseEvent): void {
        if (!this.blockNextMouseUp) {
            this.mouseDownListeners.forEach(listener => listener(ev));
        }
        this.blockNextMouseUp = false;
    }

    private handleTouchStart(ev: TouchEvent): void {
        this.rotationTimeout = this.rotationTimeout || setTimeout(() => {
            this.trackUnit.rotateType();
            this.rotationTimeout = null;
        }, 400);
    }

    private handleTouchEnd(ev: TouchEvent): void {
        if (this.rotationTimeout) {
            clearTimeout(this.rotationTimeout);
            this.rotationTimeout = null;
        }
    }

    toggle(): void {
        this.trackUnit.toggle();
    }

    setStickingType(type: TrackUnitStickingType): void {
        if (this.trackUnit.isOn()) {
            this.trackUnit.setStickingType(type);
        }
    }

    setType(type: TrackUnitType): void {
        this.trackUnit.setType(type);
    }

    turnOn(): void {
        this.trackUnit.setOn(true);
    }

    turnOff(): void {
        this.trackUnit.setStickingType("none");
        this.trackUnit.setOn(false);
    }

    notify(publisher: unknown, event: EventTypeSubscriptions): void {
        switch (event) {
        case TrackUnitEvent.On:
            this.render().classList.add("on");
            break;
        case TrackUnitEvent.Off:
            this.render().classList.remove("on");
            break;
        case TrackUnitEvent.TypeChange:
            this.syncTrackUnitType();
            this.syncStickingType(this.trackUnit.getStickingType());
        }
    }

    private syncStickingType(type: TrackUnitStickingType) {
        if (StickingTypeIconMap[this.trackUnit.getStickingType()]) {
            this.render().classList.add("icon-visible");
        } else {
            this.render().classList.remove("icon-visible");
        }
        const icon = StickingTypeIconMap[this.trackUnit.getStickingType()];
        if (icon) {
            this.icon.setIcon(icon);
        } 
    }

    private syncTrackUnitType() {
        for (const className of TypeClasses) {
            this.render().classList.remove(className);
        }
        for (const className of TrackUnitTypeClassMap[this.trackUnit.getType()]) {
            this.render().classList.add(className);
        }
    }

    static getClasses(options: { on: boolean, stickingType: TrackUnitStickingType, type: TrackUnitType, highlightable?: boolean }) {
        const classes = ["track-unit"];
        if (options.on) {
            classes.push("on");
        }
        if (StickingTypeIconMap[options.stickingType]) {
            classes.push("icon-visible");
        }
        if (options.type) {
            classes.push(...TrackUnitTypeClassMap[options.type]);
        }
        if (options.highlightable) {
            classes.push("highlightable");
        }
        return classes;
    }

    build() {
        const classes = TrackUnitView.getClasses({ 
            on: this.trackUnit.isOn(), 
            stickingType: this.trackUnit.getStickingType(),
            type: this.trackUnit.getType(),
            highlightable: true,
        });
        return (
            <div classes={classes} oncontextmenu={() => false}>
                {this.icon}
            </div>
        ) as HTMLDivElement;
    }

    onHover(cb: () => void): void {
        this.hoverListeners.push(cb);
        this.render().addEventListener("mouseover", cb);
    }

    onMouseDown(cb: (ev: MouseEvent) => void): void {
        this.mouseDownListeners.push(cb);
        this.render().addEventListener("mousedown", cb);
    }
}
