// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { CodeEdit } from "@/app/view/codeedit";
import { PlotView } from "@/app/view/plotview";
import { PreviewView } from "@/app/view/preview";
import { TerminalView } from "@/app/view/term/term";
import { WaveAi } from "@/app/view/waveai";
import { WebView } from "@/app/view/webview";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
import { ContextMenuModel } from "@/store/contextmenu";
import { atoms, globalStore, setBlockFocus, useBlockAtom } from "@/store/global";
import * as services from "@/store/services";
import * as WOS from "@/store/wos";
import * as util from "@/util/util";
import clsx from "clsx";
import * as jotai from "jotai";
import * as React from "react";

import "./block.less";

const HoverPixels = 15;
const HoverTimeoutMs = 100;

interface BlockProps {
    blockId: string;
    onClose?: () => void;
    dragHandleRef?: React.RefObject<HTMLDivElement>;
}

const colorRegex = /^((#[0-9a-f]{6,8})|([a-z]+))$/;

function processTitleString(titleString: string): React.ReactNode[] {
    if (titleString == null) {
        return null;
    }
    const tagRegex = /<(\/)?([a-z]+)(?::([#a-z0-9@-]+))?>/g;
    let lastIdx = 0;
    let match;
    let partsStack = [[]];
    while ((match = tagRegex.exec(titleString)) != null) {
        const lastPart = partsStack[partsStack.length - 1];
        const before = titleString.substring(lastIdx, match.index);
        lastPart.push(before);
        lastIdx = match.index + match[0].length;
        const [_, isClosing, tagName, tagParam] = match;
        if (tagName == "icon" && !isClosing) {
            if (tagParam == null) {
                continue;
            }
            const iconClass = util.makeIconClass(tagParam, false);
            if (iconClass == null) {
                continue;
            }
            lastPart.push(<i key={match.index} className={iconClass} />);
            continue;
        }
        if (tagName == "c" || tagName == "color") {
            if (isClosing) {
                if (partsStack.length <= 1) {
                    continue;
                }
                partsStack.pop();
                continue;
            }
            if (tagParam == null) {
                continue;
            }
            if (!tagParam.match(colorRegex)) {
                continue;
            }
            let children = [];
            const rtag = React.createElement("span", { key: match.index, style: { color: tagParam } }, children);
            lastPart.push(rtag);
            partsStack.push(children);
            continue;
        }
        if (tagName == "i" || tagName == "b") {
            if (isClosing) {
                if (partsStack.length <= 1) {
                    continue;
                }
                partsStack.pop();
                continue;
            }
            let children = [];
            const rtag = React.createElement(tagName, { key: match.index }, children);
            lastPart.push(rtag);
            partsStack.push(children);
            continue;
        }
    }
    partsStack[partsStack.length - 1].push(titleString.substring(lastIdx));
    return partsStack[0];
}

function getBlockHeaderIcon(blockIcon: string, blockData: Block): React.ReactNode {
    let blockIconElem: React.ReactNode = null;
    if (util.isBlank(blockIcon)) {
        blockIcon = "square";
    }
    let iconColor = blockData?.meta?.["icon:color"];
    if (iconColor && !iconColor.match(colorRegex)) {
        iconColor = null;
    }
    let iconStyle = null;
    if (!util.isBlank(iconColor)) {
        iconStyle = { color: iconColor };
    }
    const iconClass = util.makeIconClass(blockIcon, true);
    if (iconClass != null) {
        blockIconElem = <i key="icon" style={iconStyle} className={clsx(`block-frame-icon`, iconClass)} />;
    }
    return blockIconElem;
}

function getBlockHeaderText(blockIcon: string, blockData: Block, settings: SettingsConfigType): React.ReactNode {
    if (!blockData) {
        return "no block data";
    }
    let blockIdStr = "";
    if (settings?.blockheader?.showblockids) {
        blockIdStr = ` [${blockData.oid.substring(0, 8)}]`;
    }
    let blockIconElem = getBlockHeaderIcon(blockIcon, blockData);
    if (!util.isBlank(blockData?.meta?.title)) {
        try {
            const rtn = processTitleString(blockData.meta.title) ?? [];
            return [blockIconElem, ...rtn, blockIdStr == "" ? null : blockIdStr];
        } catch (e) {
            console.error("error processing title", blockData.meta.title, e);
            return [blockIconElem, blockData.meta.title + blockIdStr];
        }
    }
    let viewString = blockData?.view;
    if (blockData.controller == "cmd") {
        viewString = "cmd";
    }
    return [blockIconElem, viewString + blockIdStr];
}

function handleHeaderContextMenu(e: React.MouseEvent<HTMLDivElement>, blockData: Block, onClose: () => void) {
    e.preventDefault();
    e.stopPropagation();
    let menu: ContextMenuItem[] = [];
    menu.push({
        label: "Focus Block",
        click: () => {
            alert("Not Implemented");
        },
    });
    menu.push({
        label: "Move to New Window",
        click: () => {
            let currentTabId = globalStore.get(atoms.activeTabId);
            try {
                services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
            } catch (e) {
                console.error("error moving block to new window", e);
            }
        },
    });
    menu.push({ type: "separator" });
    menu.push({
        label: "Copy BlockId",
        click: () => {
            navigator.clipboard.writeText(blockData.oid);
        },
    });
    menu.push({ type: "separator" });
    menu.push({
        label: "Close Block",
        click: onClose,
    });
    ContextMenuModel.showContextMenu(menu, e);
}

interface FramelessBlockHeaderProps {
    blockId: string;
    onClose?: () => void;
    dragHandleRef?: React.RefObject<HTMLDivElement>;
}

const FramelessBlockHeader = React.memo(({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => {
    const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
    const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);

    return (
        <div
            key="header"
            className="block-header"
            ref={dragHandleRef}
            onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
        >
            <div className="block-header-text text-fixed">{getBlockHeaderText(null, blockData, settingsConfig)}</div>
            {onClose && (
                <div className="close-button" onClick={onClose}>
                    <i className="fa fa-solid fa-xmark-large" />
                </div>
            )}
        </div>
    );
});

const hoverStateOff = "off";
const hoverStatePending = "pending";
const hoverStateOn = "on";

interface BlockFrameProps {
    blockId: string;
    onClose?: () => void;
    onClick?: () => void;
    onFocusCapture?: React.FocusEventHandler<HTMLDivElement>;
    preview: boolean;
    numBlocksInTab?: number;
    children?: React.ReactNode;
    blockRef?: React.RefObject<HTMLDivElement>;
    dragHandleRef?: React.RefObject<HTMLDivElement>;
}

const BlockFrame_Tech_Component = ({
    blockId,
    onClose,
    onClick,
    onFocusCapture,
    preview,
    blockRef,
    dragHandleRef,
    children,
}: BlockFrameProps) => {
    const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
    const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
    const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
        return jotai.atom((get) => {
            const winData = get(atoms.waveWindow);
            return winData?.activeblockid === blockId;
        });
    });
    let isFocused = jotai.useAtomValue(isFocusedAtom);
    const blockIcon = useBlockIcon(blockId);
    if (preview) {
        isFocused = true;
    }
    let style: React.CSSProperties = {};
    if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
        style.borderColor = blockData.meta["frame:bordercolor"];
    }
    if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
        style.borderColor = blockData.meta["frame:bordercolor:focused"];
    }
    return (
        <div
            className={clsx(
                "block",
                "block-frame-tech",
                isFocused ? "block-focused" : null,
                preview ? "block-preview" : null
            )}
            onClick={onClick}
            onFocusCapture={onFocusCapture}
            ref={blockRef}
            style={style}
        >
            <div
                className="block-frame-tech-header"
                ref={dragHandleRef}
                onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
            >
                {getBlockHeaderText(blockIcon, blockData, settingsConfig)}
            </div>
            <div className={clsx("block-frame-tech-close")} onClick={onClose}>
                <i className="fa fa-solid fa-xmark fa-fw" />
            </div>
            {preview ? <div className="block-frame-preview" /> : children}
        </div>
    );
};

const BlockFrame_Tech = React.memo(BlockFrame_Tech_Component) as typeof BlockFrame_Tech_Component;

const BlockFrame_Default_Component = ({
    blockId,
    onClose,
    onClick,
    preview,
    blockRef,
    dragHandleRef,
    numBlocksInTab,
    onFocusCapture,
    children,
}: BlockFrameProps) => {
    const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
    const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
    const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
        return jotai.atom((get) => {
            const winData = get(atoms.waveWindow);
            return winData?.activeblockid === blockId;
        });
    });
    let isFocused = jotai.useAtomValue(isFocusedAtom);
    const blockIcon = useBlockIcon(blockId);
    if (preview) {
        isFocused = true;
    }
    let style: React.CSSProperties = {};
    if (!isFocused && blockData?.meta?.["frame:bordercolor"]) {
        style.borderColor = blockData.meta["frame:bordercolor"];
    }
    if (isFocused && blockData?.meta?.["frame:bordercolor:focused"]) {
        style.borderColor = blockData.meta["frame:bordercolor:focused"];
    }
    let handleSettings = (e: React.MouseEvent<any>) => {
        let menuItems: ContextMenuItem[] = [];
        menuItems.push({
            label: "Focus Block",
            click: () => {
                alert("Not Implemented");
            },
        });
        menuItems.push({ label: "Minimize" });
        menuItems.push({ type: "separator" });
        menuItems.push({
            label: "Move to New Window",
            click: () => {
                let currentTabId = globalStore.get(atoms.activeTabId);
                try {
                    services.WindowService.MoveBlockToNewWindow(currentTabId, blockData.oid);
                } catch (e) {
                    console.error("error moving block to new window", e);
                }
            },
        });
        menuItems.push({ type: "separator" });
        menuItems.push({
            label: "Copy BlockId",
            click: () => {
                navigator.clipboard.writeText(blockData.oid);
            },
        });
        menuItems.push({ type: "separator" });
        menuItems.push({ label: "Close", click: onClose });
        ContextMenuModel.showContextMenu(menuItems, e);
    };
    if (preview) {
        handleSettings = null;
    }

    return (
        <div
            className={clsx(
                "block",
                "block-frame-default",
                isFocused ? "block-focused" : null,
                preview ? "block-preview" : null,
                numBlocksInTab == 1 ? "block-no-highlight" : null
            )}
            onClick={onClick}
            onFocusCapture={onFocusCapture}
            ref={blockRef}
            style={style}
        >
            <div
                className="block-frame-default-header"
                ref={dragHandleRef}
                onContextMenu={(e) => handleHeaderContextMenu(e, blockData, onClose)}
            >
                <div className="block-frame-default-header-iconview">
                    <div className="block-frame-view-icon">{getBlockHeaderIcon(blockIcon, blockData)}</div>
                    <div className="block-frame-view-type">{blockViewToName(blockData?.view)}</div>
                    {settingsConfig?.blockheader?.showblockids && (
                        <div className="block-frame-blockid">[{blockId.substring(0, 8)}]</div>
                    )}
                </div>
                <div className="flex-spacer"></div>
                <div className="block-frame-end-icons">
                    <div className="block-frame-settings" onClick={handleSettings}>
                        <i className="fa fa-solid fa-cog fa-fw" />
                    </div>
                    <div className={clsx("block-frame-default-close")} onClick={onClose}>
                        <i className="fa fa-solid fa-xmark-large fa-fw" />
                    </div>
                </div>
            </div>

            {preview ? <div className="block-frame-preview" /> : children}
        </div>
    );
};

const BlockFrame_Default = React.memo(BlockFrame_Default_Component) as typeof BlockFrame_Default_Component;

const BlockFrame_Frameless = ({
    blockId,
    onClose,
    onClick,
    preview,
    blockRef,
    dragHandleRef,
    children,
}: BlockFrameProps) => {
    const localBlockRef = React.useRef<HTMLDivElement>(null);
    const [showHeader, setShowHeader] = React.useState(preview ? true : false);
    const hoverState = React.useRef(hoverStateOff);

    // this forward the lcal ref to the blockRef
    React.useImperativeHandle(blockRef, () => localBlockRef.current);

    React.useEffect(() => {
        if (preview) {
            return;
        }
        const block = localBlockRef.current;
        let hoverTimeout: NodeJS.Timeout = null;
        const handleMouseMove = (event) => {
            const rect = block.getBoundingClientRect();
            if (event.clientY - rect.top <= HoverPixels) {
                if (hoverState.current == hoverStateOff) {
                    hoverTimeout = setTimeout(() => {
                        if (hoverState.current == hoverStatePending) {
                            hoverState.current = hoverStateOn;
                            setShowHeader(true);
                        }
                    }, HoverTimeoutMs);
                    hoverState.current = hoverStatePending;
                }
            } else {
                if (hoverTimeout) {
                    if (hoverState.current == hoverStatePending) {
                        hoverState.current = hoverStateOff;
                    }
                    clearTimeout(hoverTimeout);
                    hoverTimeout = null;
                }
            }
        };
        block.addEventListener("mousemove", handleMouseMove);
        return () => {
            block.removeEventListener("mousemove", handleMouseMove);
        };
    });
    let mouseLeaveHandler = () => {
        if (preview) {
            return;
        }
        setShowHeader(false);
        hoverState.current = hoverStateOff;
    };
    return (
        <div
            className="block block-frame-frameless"
            ref={localBlockRef}
            onMouseLeave={mouseLeaveHandler}
            onClick={onClick}
        >
            <div
                className={clsx("block-header-animation-wrap", showHeader ? "is-showing" : null)}
                onMouseLeave={mouseLeaveHandler}
            >
                <FramelessBlockHeader blockId={blockId} onClose={onClose} dragHandleRef={dragHandleRef} />
            </div>
            {preview ? <div className="block-frame-preview" /> : children}
        </div>
    );
};

const BlockFrame = React.memo((props: BlockFrameProps) => {
    const blockId = props.blockId;
    const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
    const tabData = jotai.useAtomValue(atoms.tabAtom);

    if (!blockId || !blockData) {
        return null;
    }
    let FrameElem = BlockFrame_Default;
    const numBlocks = tabData?.blockids?.length ?? 0;
    // Preview should always render as the full tech frame
    if (!props.preview) {
        // if 0 or 1 blocks, use frameless, otherwise use tech
        // if (numBlocks <= 1) {
        //     FrameElem = BlockFrame_Frameless;
        // }
        if (blockData?.meta?.["frame"] === "tech") {
            FrameElem = BlockFrame_Tech;
        } else if (blockData?.meta?.["frame"] === "frameless") {
            FrameElem = BlockFrame_Frameless;
        }
    }
    return <FrameElem {...props} numBlocksInTab={numBlocks} />;
});

function blockViewToIcon(view: string): string {
    console.log("blockViewToIcon", view);
    if (view == "term") {
        return "terminal";
    }
    if (view == "preview") {
        return "file";
    }
    if (view == "web") {
        return "globe";
    }
    if (view == "waveai") {
        return "sparkles";
    }
    return null;
}

function blockViewToName(view: string): string {
    if (view == "term") {
        return "Terminal";
    }
    if (view == "preview") {
        return "Preview";
    }
    if (view == "web") {
        return "Web";
    }
    if (view == "waveai") {
        return "WaveAI";
    }
    return view;
}

function useBlockIcon(blockId: string): string {
    const blockIconOverrideAtom = useBlockAtom<string>(blockId, "blockicon:override", () => {
        return jotai.atom<string>(null);
    });
    const blockIconAtom = useBlockAtom<string>(blockId, "blockicon", () => {
        return jotai.atom((get) => {
            console.log("atom-blockicon", blockId);
            const blockAtom = WOS.getWaveObjectAtom<Block>(WOS.makeORef("block", blockId));
            const blockData = get(blockAtom);
            const metaIcon = blockData?.meta?.icon;
            if (!util.isBlank(metaIcon)) {
                console.log("atom-blockicon-meta", metaIcon);
                return metaIcon;
            }
            const overrideVal = get(blockIconOverrideAtom);
            if (overrideVal != null) {
                return overrideVal;
            }
            return blockViewToIcon(blockData?.view);
        });
    });
    const blockIcon = jotai.useAtomValue(blockIconAtom);
    return blockIcon;
}

const wm = new WeakMap();
let wmCounter = 0;
function getObjectId(obj: any): number {
    if (!wm.has(obj)) {
        wm.set(obj, wmCounter++);
    }
    return wm.get(obj);
}

const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
    let blockElem: JSX.Element = null;
    const focusElemRef = React.useRef<HTMLInputElement>(null);
    const blockRef = React.useRef<HTMLDivElement>(null);
    const [blockClicked, setBlockClicked] = React.useState(false);
    const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
    const [focusedChild, setFocusedChild] = React.useState(null);
    const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
        return jotai.atom((get) => {
            const winData = get(atoms.waveWindow);
            return winData.activeblockid === blockId;
        });
    });
    let isFocused = jotai.useAtomValue(isFocusedAtom);

    React.useLayoutEffect(() => {
        setBlockClicked(isFocused);
    }, [isFocused]);

    React.useLayoutEffect(() => {
        if (!blockClicked) {
            return;
        }
        setBlockClicked(false);
        const focusWithin = blockRef.current?.contains(document.activeElement);
        if (!focusWithin) {
            setFocusTarget();
        }
        setBlockFocus(blockId);
    }, [blockClicked]);

    React.useLayoutEffect(() => {
        if (focusedChild == null) {
            return;
        }
        setBlockFocus(blockId);
    }, [focusedChild, blockId]);

    // treat the block as clicked on creation
    const setBlockClickedTrue = React.useCallback(() => {
        setBlockClicked(true);
    }, []);

    const determineFocusedChild = React.useCallback(
        (event: React.FocusEvent<HTMLDivElement, Element>) => {
            setFocusedChild(event.target);
        },
        [setFocusedChild]
    );

    const getFocusableChildren = React.useCallback(() => {
        if (blockRef.current == null) {
            return [];
        }
        return Array.from(
            blockRef.current.querySelectorAll(
                'a[href], area[href], input:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex="0"]'
            )
        ).filter((elem) => elem.id != `${blockId}-dummy-focus`);
    }, [blockRef.current]);

    const setFocusTarget = React.useCallback(() => {
        const focusableChildren = getFocusableChildren();
        if (focusableChildren.length == 0) {
            focusElemRef.current.focus({ preventScroll: true });
        } else {
            (focusableChildren[0] as HTMLElement).focus({ preventScroll: true });
        }
    }, [focusElemRef.current, getFocusableChildren]);

    if (!blockId || !blockData) return null;
    if (blockDataLoading) {
        blockElem = <CenteredDiv>Loading...</CenteredDiv>;
    } else if (blockData.view === "term") {
        blockElem = <TerminalView key={blockId} blockId={blockId} />;
    } else if (blockData.view === "preview") {
        blockElem = <PreviewView key={blockId} blockId={blockId} />;
    } else if (blockData.view === "plot") {
        blockElem = <PlotView key={blockId} />;
    } else if (blockData.view === "codeedit") {
        blockElem = <CodeEdit key={blockId} text={null} filename={null} />;
    } else if (blockData.view === "web") {
        blockElem = <WebView key={blockId} parentRef={blockRef} initialUrl={blockData.meta.url} />;
    } else if (blockData.view === "waveai") {
        blockElem = <WaveAi key={blockId} parentRef={blockRef} />;
    }

    return (
        <BlockFrame
            key={blockId}
            blockId={blockId}
            onClose={onClose}
            preview={false}
            onClick={setBlockClickedTrue}
            blockRef={blockRef}
            dragHandleRef={dragHandleRef}
            onFocusCapture={(e) => determineFocusedChild(e)}
        >
            <div key="focuselem" className="block-focuselem">
                <input
                    type="text"
                    value=""
                    ref={focusElemRef}
                    id={`${blockId}-dummy-focus`}
                    onChange={() => {}}
                    disabled={getFocusableChildren().length > 0}
                />
            </div>
            <div key="content" className="block-content">
                <ErrorBoundary>
                    <React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{blockElem}</React.Suspense>
                </ErrorBoundary>
            </div>
        </BlockFrame>
    );
});

export { Block, BlockFrame };
