import { useEffect, useRef, useState } from "preact/hooks";
import type { JSX } from "preact";
import clsx from "clsx";
import styles from "../assets/content-editable.module.css";
import { highlight } from "../highlight";

type Props = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'value' | 'onInput'> & {
    value: string;
    placeholder?: string;
    autoLines?: boolean;
    highlight?: boolean;
    onInput?: JSX.EventHandler<JSX.TargetedInputEvent<HTMLDivElement>>;
};

const parseLines = (html: string, keepSpan: boolean = false): string => {
    html = html
        .replace(/\n?<div><br\/?><\/div>/g, '\n\n')
        .replace(/\n?<div[^>]*>/, '\n')
        .replace(/<\/?div[^>]*>/g, '')
        .replace(/<br[^>]*>/g, '');

    if (!keepSpan) {
        html = html.replace(/<[^>]+>/g, '');
    }

    return html;
};

function getCaretOffset(el: HTMLElement): number {
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return 0;

    const range = sel.getRangeAt(0).cloneRange();
    range.selectNodeContents(el);
    range.setEnd(sel.getRangeAt(0).endContainer, sel.getRangeAt(0).endOffset);
    const contents = range.cloneContents();
    const div = document.createElement('div');
    div.appendChild(contents);
    const text = parseLines(div.innerHTML);
    return text.length;
}

function setCaretOffset(el: HTMLElement, offset: number) {
    const sel = window.getSelection();
    if (!sel) return;
    const range = document.createRange();
    let remaining = offset;

    function traverse(node: Node): boolean {
        if (node.nodeType === Node.TEXT_NODE) {
            const text = node.textContent || '';
            const len = text.length;
            if (remaining <= len) {
                range.setStart(node, remaining);
                range.collapse(true);
                return true;
            }
            remaining -= len;
        } else {
            for (const child of Array.from(node.childNodes)) {
                if (traverse(child)) return true;
            }
        }
        return false;
    }

    if (!traverse(el)) {
        range.selectNodeContents(el);
        range.collapse(false);
    }

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

function resizeToContent(el: HTMLElement) {
    el.style.height = 'auto';
    el.style.height = el.scrollHeight + 'px';
}

export const ContentEditable = ({
    value,
    placeholder,
    autoLines,
    highlight: enableHighlight,
    onInput,
    class: externalClass,
    ...props
}: Props) => {
    const ref = useRef<HTMLDivElement>(null);

    useEffect(() => {
        const el = ref.current;
        if (!el) return;

        const newValue = enableHighlight ? highlight(value) : value;
        const oldValue = parseLines(el.innerHTML, true);

        if (oldValue === newValue) return;

        const offset = document.activeElement === el ? getCaretOffset(el) : null;
        el.innerHTML = newValue;
        (el as any).value = value;
        if (offset !== null) setCaretOffset(el, offset);
        if (autoLines) resizeToContent(el);
    }, [value, enableHighlight]);

    const handleInput: JSX.EventHandler<JSX.TargetedInputEvent<HTMLDivElement>> = (e) => {
        if (!ref.current) return;
        if (autoLines) resizeToContent(ref.current);
        const text = parseLines(ref.current.innerHTML);
        // add value prop to work with useInputState/useInputCallback
        (ref.current as any).value = text;
        onInput?.(e);
    };

    return (
        <div
            ref={ref}
            {...props}
            contentEditable
            data-placeholder={value?.replaceAll('\n', '').length ? undefined : placeholder}
            class={clsx(styles.root, autoLines && styles.autoLines, externalClass)}
            onInput={handleInput}
        />
    );
};
