import * as core from "@lib/core";
import * as refs from "@lib/refs";
import * as cache from "@lib/cache";

/** @deprecated */
export function readNumberAttr(
    el: HTMLElement | EventTarget | null,
    attr: string,
): number {
    if (el instanceof HTMLElement) {
        return parseInt(el.getAttribute(attr) ?? "");
    } else {
        return Number.NaN;
    }
}

/** @deprecated */
export function readObjectRefAttr<T>(
    el: HTMLElement | EventTarget | null,
    attr: string,
): T | null {
    return refs.objectById<T>(readNumberAttr(el, attr));
}

function onInput(ref: refs.Ref, transform: Transform<any, string> | undefined, event: Event) {
    let target = event.target as HTMLInputElement;
    let refValue = refs.get(ref);

    let value: any = target.value;
    if (target.type === "checkbox") {
        value = target.checked;
    }
    if (target.type === "radio") {
        value = target.value;
    }
    if (target.type === "number" || typeof refValue === "number") {
        value = parseInt(value);
    }

    if (ref.obj instanceof Storage) {
        value = JSON.stringify(value);
    }
    if (transform) {
        transform(ref, value)
    } else {
        refs.set(ref, value);
    }

    core.scheduleRedraw();
}

export type RedrawMode = "regular" | "container" | "off";

export type Transform<T, R> = (r: refs.Ref<T>, v?: R) => R

export interface InputOptions {
    redraw?: RedrawMode;
    transform?: Transform<any, string>;
    radio?: boolean
    initial?: any;
}

export function inputAttrs(ref?: refs.Ref, options: InputOptions = {}): any {
    if (!ref) {
        return {};
    }
    let value = refs.get(ref);
    if (ref.obj instanceof Storage) {
        value = safeJsonParse(value, options.initial);
    }
    if (options?.transform) {
        value = options.transform(ref)
    }

    let isBoolean = typeof value === "boolean" || options.radio
    let valueField = isBoolean ? "checked" : "value";

    return {
        [valueField]: value,
        onInput: cache.partial(onInput, ref, options.transform),
    };
}

export function contentEditableAttrs(ref: refs.Ref<string>): any {
    let value = refs.get(ref);
    return {
        onInput: cache.partial(onInputContentEditable, ref),
        contentEditable: true,
        dangerouslySetInnerHTML: { __html: value },
    };
}

// content editable caret
type CECaret = {
    activeElement: Element | null;

    // -1 for no selection
    start: number;
    end: number;
};

// given a childNode and childNodeOffset, return an offset relative to the focusNode
function offsetWithinFocusNode(
    focusNode: Node,
    childNode: Node,
    childNodeOffset: number,
): number {
    let currentOffset = 0;
    for (let i = 0; i < focusNode.childNodes.length; i++) {
        let n = focusNode.childNodes[i];
        if (childNode === n) {
            return currentOffset + childNodeOffset;
        } else if (childNode.contains(n)) {
            return (
                currentOffset +
                offsetWithinFocusNode(n, childNode, childNodeOffset)
            );
        } else {
            let t = n.textContent;
            if (t) {
                currentOffset += t.length;
            }
        }
    }
    return -1;
}

function offsetContainerOfCaret(
    focusNode: Node,
    offset: number,
): [Node, number] | null {
    if (focusNode.childNodes.length === 0) {
        return [focusNode, offset];
    }
    let currentOffset = 0;
    for (let i = 0; i < focusNode.childNodes.length; i++) {
        let n = focusNode.childNodes[i];
        let t = n.textContent;
        let nextOffset = currentOffset;
        if (t) {
            nextOffset += t.length;
        }
        if (n.nodeName === "BR") {
            nextOffset++;
        }
        if (nextOffset >= offset) {
            // if node has no child nodes, just return the node itself.
            // if node has child nodes, recurse
            let relativeOffset = offset - currentOffset;
            if (n.childNodes.length > 0) {
                return offsetContainerOfCaret(n, relativeOffset);
            } else {
                return [n, relativeOffset];
            }
        }

        currentOffset = nextOffset;
    }
    return null;
}

function getCaret(): CECaret {
    let sel = getSelection();
    let activeElement = document.activeElement;
    let start = -1;
    let end = -1;

    if (activeElement && sel && sel.rangeCount > 0) {
        let range = sel.getRangeAt(0);
        start = offsetWithinFocusNode(
            activeElement,
            range.startContainer,
            range.startOffset,
        );
        if (range.collapsed) {
            end = start;
        } else {
            end = offsetWithinFocusNode(
                activeElement,
                range.endContainer,
                range.endOffset,
            );
        }
    }

    return {
        activeElement,
        start,
        end,
    };
}

function setCaret(caret: CECaret) {
    if (caret.start == -1 || caret.end == -1) {
        return;
    }
    if (!caret.activeElement) {
        return;
    }
    let sel = window.getSelection();
    if (!sel) {
        console.log("can't set caret; no selection");
        return;
    }
    if (caret.activeElement !== document.activeElement) {
        // (caret.activeElement as HTMLElement).focus();
        return;
    }

    let range = document.createRange();
    let rangeStart = offsetContainerOfCaret(caret.activeElement, caret.start);
    let rangeEnd = offsetContainerOfCaret(caret.activeElement, caret.end);
    if (rangeStart && rangeEnd) {
        range.setStart(...rangeStart);
        range.setEnd(...rangeEnd);
    } else {
        return;
    }

    sel.removeAllRanges();
    sel.addRange(range);
}

function onInputContentEditable(ref: refs.Ref, event: InputEvent) {
    const caret = getCaret();
    // console.log("Caret:", caret)

    let currentTarget = event.currentTarget as HTMLElement;
    let content = currentTarget.innerHTML;
    refs.set(ref, content);

    core.scheduleRedraw();

    requestAnimationFrame(() => {
        setCaret(caret);
    });
}

// TODO: support this usecase within inputAttrs
export function radioAttrs<T extends number | string>(
    ref: refs.Ref<T>,
    option: T,
) {
    return {
        value: option,
        checked: refs.get(ref) === option,
        onInput: cache.partial(onInput, ref, undefined),
    };
}

export function toggleButtonAttrs(ref?: refs.Ref<boolean>) {
    if (!ref) {
        return {}
    }
    return {
        onClick: cache.partial(onToggleClick, ref),
    };
}

function onToggleClick(ref: refs.Ref<boolean>, event: Event) {
    refs.set(ref, !refs.get(ref));
    core.scheduleRedraw();
}

// for boolean refs whose purpose is to know about click events
export function consumeBooleanRef(b: refs.Ref<boolean>): boolean {
    let result = refs.get(b);
    if (result) {
        refs.set(b, false);
    }
    return result;
}

function safeJsonParse(
    raw: string | undefined | null,
    fallback?: unknown,
): unknown {
    if (!raw) {
        return fallback;
    }
    try {
        return JSON.parse(raw);
    } catch (e) {
        return fallback;
    }
}

export function elementRefAttrs(ref?: refs.Ref<HTMLElement | null>): any {
    if (!ref) {
        return {}
    }

    return {
        "listen-create": true,
        "listen-mutate": true,
        oncreate: cache.partial(eventSetRef, ref),
        onmutate: cache.partial(eventSetRef, ref),
    };
}

export function eventSetRef(ref: refs.Ref<HTMLElement | null>, event: CustomEvent) {
    refs.set(ref, event.currentTarget);
}

function onHover(ref: refs.Ref<boolean>, event: Event) {
    if (event.type === "mouseenter") {
        refs.set(ref, true);
    } else if (event.type === "mouseleave") {
        refs.set(ref, false);
    }
    core.scheduleRedraw();
}

export function trackHoverAttrs(ref?: refs.Ref<boolean>) {
    if (!ref) {
        return {}
    }
    return {
        onMouseEnter: cache.partial(onHover, ref),
        onMouseLeave: cache.partial(onHover, ref),
    };
}
