import classNames from "classnames";
import scrollIntoView from "dom-scroll-into-view";
import { Component } from "react";

import { Checkbox, Spin } from "./index.js";
import { parents } from "../util/index.js";

import type { ChangeEvent, FormEvent, KeyboardEvent } from "react";

export type TreeNodeId = string | number;

export const toId = (test: any) => (isNaN(parseInt(test)) ? test : parseInt(test)) as TreeNodeId;

export interface TreeNodeProps {
    /**
     * The unique id for this node. Added as data-li-id to the li DOMElement and as data-id to the .aiot-node div-DOMElement
     */
    id?: TreeNodeId;
    /**
     * Use this field to force rerender of the node. This is useful if you use a state management library like mobx-state-tree and try to splice a child node.
     */
    hash?: string;
    className?: string;
    icon?: any;
    iconActive?: any;
    childNodes?: TreeNodeProps[];
    title?: string;
    count?: number;
    /**
     * Additional attributes for the .aiot-node div DOMElement
     */
    attr?: Record<string, any>;
    /**
     * If $rename is true this button text is showed next to the input field
     */
    renameSaveText?: any;
    /**
     * If $create is true this button text is showed next to the input field of the new created node
     */
    renameAddText?: any;
    $busy?: boolean;
    $droppable?: boolean;
    $visible?: boolean;
    /**
     * If true the title is replaced with an input field
     */
    $rename?: boolean;
    /**
     * If setted it must be a property map and it is added as child node to the current node
     */
    $create?: TreeNodeProps;
    $_create?: boolean;
    $checkable?: boolean;
    searchSelected?: boolean;
    expandedState?: any;
    displayChildren?: boolean;
    checked?: boolean;
    indeterminate?: boolean;
    selected?: boolean;
    renderItem?: (
        createNodeItem: (props: TreeNodeProps, additional?: TreeNodeProps) => JSX.Element,
        el: typeof TreeNode,
        node: TreeNodeProps,
    ) => JSX.Element;
    renderIcon?: (icon: TreeNodeProps["icon"], node?: TreeNodeProps) => JSX.Element;
    /**
     * This function is called when a tree node is in rename mode and
     * the rename mode gets closed (ESC), cancelled or saved.
     */
    onRenameClose?: (save: boolean, input: string, props: TreeNodeProps) => void;
    /**
     * This function is called when a new tree node should be saved or the
     * add process is cancelled.
     */
    onAddClose?: TreeNodeProps["onRenameClose"];
    /**
     * This function is called when a tree node is checked or unchecked.
     */
    onCheck?: (id: TreeNodeId) => void;
    /**
     * This function is called when a tree node gets selected.
     */
    onSelect?: (id: TreeNodeId) => void;
    /**
     * This function is called when a tree node is active and F2 is pressed.
     * Useful to activate the rename process.
     */
    onNodePressF2?: (props: TreeNodeProps) => void;
    /**
     * This function is called when a tree node is expanded or collapsed
     */
    onExpand?: (expanded: boolean, props: TreeNodeProps) => void;
    /**
     * This function is called when a tree node is expanded or collapsed
     */
    onUlRef?: (ul: HTMLUListElement, id: TreeNodeId) => void;
}

interface TreeNodeState {
    expanded: boolean;
    inputValue: string;
    initialInputValue: boolean;
}

/**
 * Tree node with child nodes and rename / create mode.
 *
 * @returns
 */
export class TreeNode extends Component<TreeNodeProps, TreeNodeState> {
    public static propKeys: string[];
    public refNode: any;

    public static defaultProps: TreeNodeProps = {
        id: undefined,
        hash: "",
        className: undefined,
        icon: undefined,
        iconActive: undefined,
        childNodes: [],
        title: "",
        count: 0,
        attr: {},
        renameSaveText: "Save",
        renameAddText: "Add",
        $busy: false,
        $droppable: true,
        $visible: true,
        $rename: undefined,
        $create: undefined,
        $checkable: false,
        searchSelected: false,
        expandedState: true,
        displayChildren: true,
        checked: false,
        indeterminate: false,
        selected: false,
        onRenameClose: undefined,
        onAddClose: undefined,
        onCheck: undefined,
        onSelect: undefined,
        onNodePressF2: undefined,
        onExpand: undefined,
        onUlRef: undefined,
    };

    public static stateKeys = "expanded,inputValue,initialInputValue".split(",");

    public constructor(props: TreeNodeProps) {
        super(props);
        !TreeNode.propKeys && (TreeNode.propKeys = Object.keys(TreeNode.defaultProps));

        // Get expanded state
        const { id, expandedState } = props;
        const expanded = id && typeof expandedState[id] === "boolean" ? expandedState[id] : true;

        this.state = {
            expanded,
            inputValue: "",
            initialInputValue: false,
        };
    }

    public shouldComponentUpdate(nextProps: TreeNodeProps, nextState: TreeNodeState) {
        const changedProps = TreeNode.propKeys.filter((k) => this.props[k] !== nextProps[k]);
        const changedState = TreeNode.stateKeys.filter((k) => this.state[k] !== nextState[k]);
        if (!changedProps.length && !changedState.length) {
            // Nothing changed
            return false;
        }
        return true;
    }

    public componentDidUpdate() {
        const { id, title, $rename, $_create, searchSelected, expandedState } = this.props;

        // Scroll to search result
        searchSelected && this.scrollTo();

        // Update the expanded state depending on the expanded property.
        const expanded = id && typeof expandedState[id] === "boolean" ? expandedState[id] : true;
        if (expanded !== this.state.expanded) {
            this.setState({ expanded });
        }

        // Avoid controlled / uncontrolled switch when creating a new node
        if ($_create) {
            return;
        }

        if (this.state.inputValue !== title && $rename && !this.state.initialInputValue) {
            this.setState({ inputValue: title, initialInputValue: true });
        } else if (!$rename && this.state.initialInputValue) {
            this.setState({ inputValue: "", initialInputValue: false });
        }
    }

    public handleInputKeyDown = (e: KeyboardEvent) => {
        if (e.key === "Enter") {
            this.handleButtonSave(true);
        } else if (e.key === "Escape") {
            this.handleButtonSave(false);
        }
    };

    public handleNodeKeyDown = (e: KeyboardEvent) => {
        if (e.key === "F2" && !this.props.$rename) {
            this.props.onNodePressF2 && this.props.onNodePressF2(this.props);
        }
    };

    public handleButtonSave = (save: any) => {
        const _save = typeof save === "boolean" ? save : true;
        const { inputValue } = this.state;
        if (_save === true && !inputValue) {
            return;
        }

        this.props.onRenameClose && this.props.onRenameClose(_save, inputValue, this.props);
    };

    public handleChange = (e: ChangeEvent<HTMLInputElement>) => {
        this.setState({ inputValue: e.target.value });
    };

    public handleSelect = (e: FormEvent) => {
        if (
            !parents(e.target as HTMLElement, ".aiot-disable-links").length &&
            !parents(e.target as HTMLElement, ".ant-checkbox-wrapper").length
        ) {
            this.props.onSelect && this.props.onSelect(this.props.id);
        }
    };

    public handleToggle = (e: any) => {
        const newExpanded = !this.state.expanded;
        const { onExpand } = this.props;
        this.setState({ expanded: newExpanded });
        onExpand && onExpand(newExpanded, this.props);
        e.preventDefault();
    };

    public handleRef = (node: any) => {
        this.refNode = node;
        this.props.$_create && this.scrollTo();
    };

    public handleCheck = () => {
        this.props.onCheck && this.props.onCheck(this.props.id);
    };

    public scrollTo() {
        const container = this.refNode;
        container && scrollIntoView(container, window, { onlyScrollIfNeeded: true, alignWithTop: false });
    }

    public render() {
        const {
            icon,
            childNodes = [],
            id,
            title,
            count,
            selected,
            $rename,
            $busy,
            $droppable = true,
            $create,
            $visible = true,
            $_create,
            searchSelected,
            attr,
            checked,
            indeterminate,
        } = this.props;
        const {
            expandedState,
            displayChildren,
            renderItem,
            renderIcon,
            $checkable,
            onRenameClose,
            onCheck,
            onAddClose,
            onSelect,
            onNodePressF2,
            onExpand,
            onUlRef,
            renameSaveText,
            renameAddText,
        } = this.props;
        const nodeAttr = {
            expandedState,
            displayChildren,
            renderItem,
            renderIcon,
            $checkable,
            onRenameClose,
            onCheck,
            onAddClose,
            onSelect,
            onNodePressF2,
            onExpand,
            onUlRef,
            renameSaveText,
            renameAddText,
        };
        const visibleChildNodes = childNodes && childNodes.filter(({ $visible = true }) => !!$visible);
        const togglable = !!(displayChildren && visibleChildNodes && visibleChildNodes.length);
        const isExpanded = this.state.expanded || !!$create;
        const isActive = $create ? false : $_create ? true : selected;
        const className = classNames("aiot-node", this.props.className, {
            "aiot-active": isActive,
            "aiot-forceEnable": !!$rename,
            "aiot-togglable": togglable,
            "aiot-expanded": this.state.expanded,
            "aiot-search-selected": searchSelected,
            "aiot-droppable": $droppable && !$_create,
            "aiot-checkable": $checkable,
            "aiot-checked": checked,
        });

        // Tree node perhaps deleted?
        if (!$visible) {
            return null;
        }

        // Get icon with multiple checkbox
        const useIcon = selected ? this.props.iconActive || this.props.icon : icon;
        const useIconObj = (
            <div className="aiot-node-icon">
                {renderIcon ? renderIcon(useIcon, this.props) : useIcon}
                {$checkable && (
                    <Checkbox
                        checked={checked && !indeterminate}
                        indeterminate={indeterminate}
                        onChange={this.handleCheck}
                    />
                )}
            </div>
        );

        // Sortable
        const isUlVisible = togglable && isExpanded;
        const isSortable = !!displayChildren /*@TODO && !isTreeLinkDisabled*/ && !$_create;
        const refSortable = (ref: HTMLUListElement) => displayChildren && ref && onUlRef && onUlRef(ref, id);
        !isUlVisible && displayChildren && onUlRef && onUlRef(undefined, id);

        const createTreeNode = (node: TreeNodeProps, opts?: TreeNodeProps) => (
            <TreeNode key={node.id} {...node} {...nodeAttr} {...opts} />
        );

        // Result
        return (
            <li className={classNames({ "aiot-sortable": isSortable })} data-li-id={id}>
                {/* @ts-expect-error outdated @types/react */}
                <Spin spinning={!!$busy} size="small">
                    <div
                        data-id={id}
                        tabIndex={0}
                        className={className}
                        onClick={$_create ? undefined : this.handleSelect}
                        onDoubleClick={$_create || !togglable ? undefined : this.handleToggle}
                        onKeyDown={this.handleNodeKeyDown}
                        {...attr}
                        ref={this.handleRef}
                    >
                        {useIconObj}
                        {$rename ? (
                            <input
                                autoFocus
                                className="aiot-node-name"
                                value={this.state.inputValue}
                                onChange={this.handleChange}
                                onKeyDown={this.handleInputKeyDown}
                            />
                        ) : (
                            <div className="aiot-node-name" title={title}>
                                {title}
                            </div>
                        )}
                        {count > 0 && !$rename && <div className="aiot-node-count">{count}</div>}
                        {$rename && (
                            <button disabled={!this.state.inputValue} onClick={this.handleButtonSave}>
                                {renameSaveText}
                            </button>
                        )}
                    </div>
                </Spin>

                {isUlVisible && (
                    <ul
                        className={classNames({ "aiot-sortable-one": childNodes.length === 1 })}
                        data-childs-for={id}
                        ref={refSortable}
                    >
                        {childNodes.map((node) => {
                            if (renderItem) {
                                return renderItem(createTreeNode, TreeNode, node);
                            } else {
                                return createTreeNode(node);
                            }
                        })}
                        {childNodes.length === 1 && (
                            <li
                                className={classNames("aiot-sortable-placeholder", { "aiot-sortable": isSortable })}
                            ></li>
                        )}
                        {!!$create && (
                            <TreeNode
                                $_create
                                renderIcon={renderIcon}
                                onRenameClose={onAddClose}
                                renameSaveText={renameAddText}
                                {...$create}
                            />
                        )}
                    </ul>
                )}

                {!childNodes.length && isSortable && (
                    <ul data-childs-for={id} ref={refSortable} className="aiot-sortable-empty" />
                )}

                {!!$create && !togglable && (
                    <ul>
                        <TreeNode
                            $_create
                            renderIcon={renderIcon}
                            onRenameClose={onAddClose}
                            renameSaveText={renameAddText}
                            {...$create}
                        />
                    </ul>
                )}

                {togglable && (
                    <div
                        onClick={this.handleToggle}
                        className={classNames("aiot-expander", { "aiot-open": isExpanded })}
                    />
                )}
            </li>
        );
    }
}
