/**
 * Render Node of Tree
 *
 * @author: Viktor Magarlamov
 * @date: 2019-06-20
 */

import * as React from 'react';
import {ITreeColumn, ITreeNode} from './TreeType';
import {
    Button,
    DropDown,
    INTENT,
    IStringKeyMap,
    joinClassNames,
    PLACEMENT,
    SIZE
} from '../../index';
import {safeInvoke} from '../../utils/safeInvoke';

import * as styles from './tree.m.scss';
import {Hierarchy} from './Hierarchy';
import {Cell} from './Cell';
import {IconName} from '@unidata/icon';

export type ConditionFunc<T> = (node: ITreeNode<T>, column?: ITreeColumn<T>) => boolean;

export type ActionItem<T> = {
    label: string;
    action: (node: ITreeNode<T>) => void;
    icon?: IconName;
    resolver?: (node: ITreeNode<T>) => boolean;
}

export interface IDropConfig<T> {
    parentNode?: ITreeNode<T>;
    filledParent: boolean;
    previousNode?: ITreeNode<T>;
    filledPrevious: boolean;
    globalDropLevel: number;
    configFilled: boolean;
}

export const ROW_HEIGHT = 24;

const VERTICAL_HIERARCHY_WIDTH = 19;
const HORIZONTAL_HIERARCHY_WIDTH = 17;

interface IProps<T> {
    node: ITreeNode<T>;
    level: number;
    preventChildrenRender?: boolean;
    columns: Array<ITreeColumn<T>>;
    readOnly?: boolean;
    isLastChild: boolean;
    selectedItems: IStringKeyMap;
    disabledItems: IStringKeyMap;
    canSelectDisabled: boolean;
    onNodeSelect?: (node: ITreeNode<T>) => void;
    onNodeExpand?: (node: ITreeNode<T>) => void;
    onNodeCollapse?: (node: ITreeNode<T>) => void;
    onNodeDelete?: (node: ITreeNode<T>) => void;
    onBeforeNodeSelect?: (node: ITreeNode<T>) => boolean;
    // isScrollToMe?: boolean;
    isDeletable?: boolean | ConditionFunc<T>;
    filter?: (node: ITreeNode<T>) => boolean;
    isEditable?: boolean | ConditionFunc<T>;
    onEdit?: (node: ITreeNode<T>, newValue: string, cellId: string) => void;
    actions: Array<ActionItem<T>>;
    isActionsEnabled?: (node: ITreeNode) => boolean;
    rightExtraItem?: (nodeData: T) => React.ReactNode;
    isDraggable?: boolean;
    // Can we drop nodes as child of current node
    canHasChildren?: boolean | ConditionFunc<T>;
    // Fired when node is starts dragged
    onDragStart?: (event: React.DragEvent<HTMLDivElement>, node: ITreeNode<T>) => void;
    // Fired when node is dropped not on the Nodes (Out of the tree)
    onDragEnd?: (event: React.DragEvent<HTMLDivElement>,) => void;
    // Fired when another dragged node is dropped on this
    onBackwardDrop?: (event: React.DragEvent, node: ITreeNode<T>, dropConfig: IDropConfig<T>) => void;
    // Callback to determine which nodes available for drop (by default all nodes allowed)
    allowDragOver?: (event: React.DragEvent<HTMLDivElement>, dropToNode: ITreeNode<T>, draggedNode?: ITreeNode<T>) => boolean;
    // Current dragged node in tree
    draggedNode?: ITreeNode<T>;
    // Length of last child streak
    lastChildStreak: number;
    isExpanded: boolean;
}

interface IState {
    isRowHovered: boolean;
    isDropDownOpen: boolean;
    expanded: boolean;
    isDropAsNext?: boolean;
    localDropLevel: number;
    // onDragEnter, onDragLeave is also fired for children of html element
    // We need a counter to do Enter and Leave logic once
    dragEnterCounter: number;
}

export class Node<T> extends React.PureComponent<IProps<T>, IState> {

    override state: IState = {
        isRowHovered: false,
        expanded: (this.props.node.children) ? this.props.isExpanded : false,
        isDropDownOpen: false,
        isDropAsNext: undefined,
        localDropLevel: 0,
        dragEnterCounter: 0
    };

    hoverWhenDragTimeout: number = 0;
    nodeRef: React.RefObject<HTMLDivElement> = React.createRef();

    override componentDidUpdate (prevProps: Readonly<IProps<T>>, prevState: Readonly<IState>, snapshot?: any): void {
        if (this.props.node.children !== null) {
            if (prevProps.node.children === null) {
                this.setState({expanded: true});
            }

            if (prevProps.isExpanded !== this.props.isExpanded) {
                this.setState({expanded: this.props.node.expanded});
            }
        }
    }

    get selected () {
        return Boolean(this.props.selectedItems[this.props.node.key]);
    }

    get disabled () {
        return Boolean(this.props.disabledItems[this.props.node.key]);
    }

    getFirstCellPositionX (level: number) {
        return level * VERTICAL_HIERARCHY_WIDTH + HORIZONTAL_HIERARCHY_WIDTH;
    }

    onExpandClick = () => {
        if (this.props.node.children !== null) {
            if (this.state.expanded === false) {
                safeInvoke(this.props.onNodeExpand, this.props.node);
            } else {
                safeInvoke(this.props.onNodeCollapse, this.props.node);
            }

            this.setState((prevState) => {
                return {
                    expanded: !prevState.expanded
                };
            });
        }
    };

    onRowClick = (e: React.SyntheticEvent<HTMLDivElement>) => {
        e.stopPropagation();

        this.setState({isRowHovered: true});

        if (!this.disabled || this.props.canSelectDisabled === true) {
            if (this.props.onBeforeNodeSelect && !this.props.onBeforeNodeSelect(this.props.node)) {
                return;
            }

            safeInvoke(this.props.onNodeSelect, this.props.node);
        }
    };

    // ToDo Platon Fedorov Do scroll to dragged element
    // todo Brauer Ilya - no "isScrollToMe" in code
    // override componentDidMount (): void {
    //     if (this.props.item.row.match && this.props.item.row.isScrollToMe && this.ref.current) {
    //         this.ref.current.scrollIntoView();
    //     }
    // }

    onRowDelete = (e: React.SyntheticEvent<HTMLButtonElement>) => {
        e.stopPropagation();

        safeInvoke(this.props.onNodeDelete, this.props.node);
    };

    onMouseEnter = () => {
        this.setState({isRowHovered: true});
    };

    onMouseLeave = () => {
        if (this.state.isDropDownOpen === false) {
            this.setState({isRowHovered: false});
        }
    };
    renderChildrenNodes = () => {
        const node = this.props.node;

        if (this.state.expanded && node.children && !this.props.preventChildrenRender) {
            const childrenList = node.children;

            return childrenList.map((childNode, index) => {
                const isLastChild = index === childrenList.length - 1;

                return (
                    <Node<T>
                        {...this.props}

                        key={childNode.key}
                        node={childNode}
                        isExpanded={childNode.expanded}
                        level={this.props.level + 1}
                        isLastChild={isLastChild}
                        lastChildStreak={isLastChild ? this.props.lastChildStreak + 1 : 0}
                        onBackwardDrop={this.onBackwardDrop}
                    />
                );
            });
        }

        return null;
    };

    handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
        if (!this.props.isDraggable) {
            return;
        }

        if (this.state.expanded) {
            this.onExpandClick();
        }

        safeInvoke(this.props.onDragStart, event, this.props.node);
    };

    handleDragEnd = (event: React.DragEvent<HTMLDivElement>) => {
        safeInvoke(this.props.onDragEnd, event);
        event.dataTransfer.clearData();
    }

    handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
        if (this.props.allowDragOver) {
            if (this.props.allowDragOver(event, this.props.node, this.props.draggedNode)) {
                event.preventDefault();
            } else {
                return;
            }
        } else {
            // always allow drop by default
            event.preventDefault();
        }

        if (!this.nodeRef.current || !this.props.isDraggable) {
            return;
        }

        const node = this.props.node;
        const dragNodeY = event.pageY;
        const dragNodeX = event.pageX;
        const currentNodeMeanY = this.nodeRef.current.getBoundingClientRect().top + Math.floor(ROW_HEIGHT / 2);
        const currentNodeX = this.nodeRef.current.getBoundingClientRect().left + this.getFirstCellPositionX(this.props.level);
        const deltaY = dragNodeY - currentNodeMeanY;
        const deltaX = dragNodeX - currentNodeX;

        const canHasChildren = (typeof this.props.canHasChildren === 'function' ?
            this.props.canHasChildren(node) : this.props.canHasChildren) && !this.disabled;

        if (deltaY < 0) {
            this.setState({
                isDropAsNext: false,
                localDropLevel: 0
            });
        } else {
            let localDropLevel = Math.floor(Math.abs(deltaX) / VERTICAL_HIERARCHY_WIDTH);

            if (this.state.expanded && node.children && node.children.length > 0) {
                localDropLevel = 1;
            } else {
                localDropLevel = deltaX > 0 ?
                    Math.min(localDropLevel, 1) : -1 * Math.min(localDropLevel + 1, this.props.lastChildStreak);
            }

            if (node === this.props.draggedNode || !canHasChildren) {
                localDropLevel = Math.min(localDropLevel, 0);
            }

            this.setState({
                isDropAsNext: true,
                localDropLevel: localDropLevel
            });
        }
    }

    handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
        const {isDropAsNext, localDropLevel} = this.state;
        const {node, level, draggedNode} = this.props;
        const globalDropLevel = level + localDropLevel;

        if (localDropLevel === 1) {
            // Drop node as child of current node
            safeInvoke(this.props.onBackwardDrop, event, node, {
                previousNode: undefined,
                filledPrevious: true,
                parentNode: node,
                filledParent: true,
                globalDropLevel: globalDropLevel,
                configFilled: true
            });
        } else if (localDropLevel < 0) {
            // Drop node as child of one of parent nodes
            safeInvoke(this.props.onBackwardDrop, event, node, {
                previousNode: undefined,
                filledPrevious: false,
                parentNode: undefined,
                filledParent: false,
                globalDropLevel: globalDropLevel,
                configFilled: false
            });
        } else if (localDropLevel === 0 && node !== draggedNode) {
            // Drop node as child of current node
            safeInvoke(this.props.onBackwardDrop, event, node, {
                previousNode: isDropAsNext ? node : undefined,
                filledPrevious: isDropAsNext ? true : false,
                parentNode: undefined,
                filledParent: false,
                globalDropLevel: globalDropLevel,
                configFilled: false
            });
        }

        event.dataTransfer.clearData();
        window.clearTimeout(this.hoverWhenDragTimeout);

        this.setState({
            isDropAsNext: undefined,
            localDropLevel: 0,
            dragEnterCounter: 0
        });
    };

    handleDragEnter = () => {
        this.setState((prevState) => {
            if (prevState.dragEnterCounter === 0 && this.props.draggedNode !== this.props.node) {
                this.hoverWhenDragTimeout = window.setTimeout(() => {
                    if (!this.state.expanded) {
                        this.onExpandClick();
                    }
                }, 600);
            }

            return {
                dragEnterCounter: prevState.dragEnterCounter + 1
            };
        });
    }

    handleDragLeave = () => {
        this.setState((prevState) => {
            const dragEnterCounter = prevState.dragEnterCounter;

            if (dragEnterCounter === 1) {
                window.clearTimeout(this.hoverWhenDragTimeout);

                return {
                    dragEnterCounter: prevState.dragEnterCounter - 1,
                    isDropAsNext: undefined,
                    localDropLevel: 0
                };
            } else {
                return {
                    dragEnterCounter: prevState.dragEnterCounter - 1,
                    isDropAsNext: prevState.isDropAsNext,
                    localDropLevel: prevState.localDropLevel
                };
            }
        });
    }

    onBackwardDrop = (event: React.DragEvent, node: ITreeNode<T>, dropConfig: IDropConfig<T>) => {
        if (dropConfig.configFilled) {
            safeInvoke(this.props.onBackwardDrop, event, node, dropConfig);

            return;
        }

        const currentNode = this.props.node;

        if (dropConfig.globalDropLevel === this.props.level + 1) {
            if (!dropConfig.filledPrevious) {
                const nodeIdx = currentNode.children?.findIndex((child) => child.key === node.key);

                if (nodeIdx === undefined || !currentNode.children) {
                    return;
                }

                if (nodeIdx === 0) {
                    dropConfig.previousNode = undefined;
                } else {
                    dropConfig.previousNode = currentNode.children[nodeIdx - 1];
                }

                dropConfig.filledPrevious = true;
            }

            dropConfig.parentNode = currentNode;
            dropConfig.filledParent = true;
            dropConfig.configFilled = true;
        } else if (dropConfig.globalDropLevel === this.props.level) {
            dropConfig.previousNode = currentNode;
            dropConfig.filledPrevious = true;
        }

        safeInvoke(this.props.onBackwardDrop, event, node, dropConfig);
    }

    handleActionClick = (action: (node: ITreeNode<T>) => void) => () => {
        action(this.props.node);

        this.handleCloseDD();
    };

    handleOpenDD = () => {
        this.setState({isDropDownOpen: true});
    };

    handleCloseDD = () => {
        this.setState({isDropDownOpen: false, isRowHovered: false});
    };

    renderDropPlace = (isDropAsNext: boolean | undefined, dropLevel: number) => {
        if (isDropAsNext === undefined || !this.nodeRef.current) {
            return null;
        }

        const left = this.getFirstCellPositionX(this.props.level + dropLevel);

        const style: React.CSSProperties = {
            left: `${left}px`,
            width: `calc(100% - ${left}px)`
        };

        const className = joinClassNames(
            styles.dropPlaceLine,
            [styles.dropAsPrevious, !isDropAsNext],
            [styles.dropAsNext, isDropAsNext]
        );

        return <div className={className} style={style}><div className={styles.dropPlaceCircle}/></div>;
    }

    getRightExtraItems = () => {
        return typeof this.props.rightExtraItem === 'function' ? this.props.rightExtraItem(this.props.node.row) : null;
    }

    get actions () {

        return this.props.actions.filter((action) => {
            const resolver = action.resolver;

            if (resolver) {
                return resolver(this.props.node);
            }

            return true;
        });
    }

    dropdownMenuActions = () => {
        return (
            <DropDown
                target={(
                    <span>
                        <Button
                            isMinimal={true}
                            leftIcon={'menu2'}
                        />
                    </span>
                )}
                placement={PLACEMENT.BOTTOM_END}
                onOpen={this.handleOpenDD}
                onClose={this.handleCloseDD}
            >
                {this.props.actions.map((actionItem, index) => {
                    return (
                        <DropDown.Item
                            key={index}
                            onClick={this.handleActionClick(actionItem.action)}
                        >
                            {actionItem.label}
                        </DropDown.Item>
                    );
                })}
            </DropDown>
        );
    }

    renderActions = () => {
        let actionsEnabled = true;

        if (this.props.isActionsEnabled) {
            actionsEnabled = this.props.isActionsEnabled(this.props.node);
        }

        if ((this.actions.length === 1) && this.state.isRowHovered && actionsEnabled) {

            return (
                <Button
                    isMinimal={true}
                    leftIcon={this.actions[0].icon}
                    title={this.actions[0].label}
                    onClick={(e) => {
                            e.stopPropagation();

                            return this.actions[0].action(this.props.node);
                        }
                    }/>
            );
        } else if (this.actions.length > 1 && this.state.isRowHovered === true && actionsEnabled) {

            return this.dropdownMenuActions();
        }

        return null;
    }

    override render () {
        const {node, filter, level, isDraggable, isLastChild, draggedNode} = this.props;
        const {isDropAsNext, localDropLevel, isRowHovered} = this.state;

        if (filter !== undefined && filter(node) === false) {
            return null;
        }

        const className = joinClassNames(
            styles.row,
            [styles.hover, isRowHovered],
            [styles.rootNode, level === 0 && node.children !== null],
            [styles.selectable, Boolean(node.path && !this.disabled)],
            [styles.active, this.selected],
            [styles.disabled, this.disabled],
            [styles.isDraggable, isDraggable === true],
            [styles.draggedRow, node === draggedNode]
        );

        const isDeletable = (typeof this.props.isDeletable === 'function' ? this.props.isDeletable(node) : this.props.isDeletable) &&
            !this.disabled;

        return (
            <>
                <div
                    className={className}
                    onMouseEnter={this.onMouseEnter}
                    onMouseLeave={this.onMouseLeave}
                    onClick={this.onRowClick}

                    draggable={isDraggable}
                    onDragStart={this.handleDragStart}
                    onDragEnd={this.handleDragEnd}

                    onDragOver={this.handleDragOver}
                    onDrop={this.handleDrop}
                    onDragEnter={this.handleDragEnter}
                    onDragLeave={this.handleDragLeave}

                    ref={this.nodeRef}
                >
                    {this.props.columns.map((column: ITreeColumn, index) => {
                        let isEdited = (typeof node.isEdited === 'function') ? node.isEdited(node, column) : node.isEdited;

                        return (
                            <React.Fragment key={column.name}>
                                <Cell
                                    node={node}
                                    column={column}
                                    isEditable={this.props.isEditable}
                                    onEdit={this.props.onEdit}
                                    isEdited={isEdited}
                                >
                                    {index === 0 && (
                                        <Hierarchy
                                            nodeChildren={node.children}
                                            level={level}
                                            isLast={isLastChild && !node.children}
                                            expanded={Boolean(this.state.expanded)}
                                            onExpandClick={this.onExpandClick}
                                            node={node}
                                        />
                                    )}
                                </Cell>
                            </React.Fragment>
                        );
                    })}
                    {this.renderActions()}

                    <div key={`delete_${node.key}`} className={styles.deleteButtonContainer}>
                        {isDeletable === true && isRowHovered && (
                            <Button
                                leftIcon={'delete'}
                                size={SIZE.SMALL}
                                intent={INTENT.DANGER}
                                isMinimal={true}
                                onClick={this.onRowDelete}/>
                        )}
                    </div>
                    {Boolean(this.getRightExtraItems()) ?
                        <div className={styles.rightExtraItemContainer}>{this.getRightExtraItems()}</div> : null}
                    {this.renderDropPlace(isDropAsNext, localDropLevel)}
                </div>
                {this.renderChildrenNodes()}
            </>
        );
    }
}
