import React, { useCallback, useMemo, useState } from 'react';
import { difference, uniq } from 'lodash';
import { TreeItem } from './components/tree-item';
import { STreeList } from './s-tree-list';
import { bem } from 'utils/bem';
import { AnimatePresence, motion } from 'framer-motion';
import { filterTreeNodes } from './transducers';
import { OptionIdType } from 'common/components/form/select';

interface Props {
  nodes: TreeNode[];
  value: OptionIdType[];
  searchQuery?: string;
  onChange(ids: OptionIdType[]): void;
  className?: string;
  renderTreeItem?(props: TreeItemProps): React.ReactChild;
  renderEmptyPlaceholder?(): React.ReactChild;
  isExpandable?: boolean;
  isSingle?: boolean;
  isExternallyFiltered?: boolean;
  defaultExpandedIds?: OptionIdType[];
  searchQueryFilter?: string;
}

export enum TreeItemType {
  LABEL,
  CHECKBOX,
}

export interface TreeItemProps {
  depth: number;
  isChecked: boolean;
  isHalfChecked?: boolean;
  isExpanded?: boolean;
  isDisabled?: boolean;
  isLeaf: boolean;
  isDivided?: boolean;
  onClick(checked: boolean): void;
  onExpand?(): void;
  name: string;
  type: TreeItemType;
  description?: string;
  disabledDescription?: string;
  nodeId?: OptionIdType;
}

export interface TreeNode {
  id: OptionIdType;
  name: string;
  type: TreeItemType;
  isDisabled?: boolean;
  children: TreeNode[];
  description?: string;
  disabledDescription?: string;
}

interface ProcessedTreeNodeResult {
  element: React.ReactNode;
  isChecked: boolean;
  isHalfChecked: boolean;
}

const classes = bem('tree-list');

const expandAnimationProps = {
  initial: { height: 0 },
  exit: { height: 0, overflow: 'hidden' },
  animate: { height: 'unset' },
  transition: { type: 'spring', damping: 15, mass: 0.5 },
};

export const TreeList = React.memo(
  ({
    nodes,
    value,
    onChange,
    className,
    renderTreeItem,
    searchQuery,
    isExpandable,
    defaultExpandedIds,
    renderEmptyPlaceholder,
    isSingle = false,
    isExternallyFiltered = false,
  }: Props) => {
    const [expandedIds, setExpandedIds] = useState<OptionIdType[]>(defaultExpandedIds || []);

    const isLeaf = useCallback((node: TreeNode) => node.children.length === 0, []);

    const getLeafsIds = useCallback(
      (node: TreeNode): OptionIdType[] => {
        if (isLeaf(node)) return [node.id];

        return node.children.flatMap(getLeafsIds);
      },
      [isLeaf]
    );

    const handleMultiSelectChange = useCallback(
      (node: TreeNode, isSelected: boolean) => {
        const leafsIds = getLeafsIds(node);
        const uniqueLeafsIds = uniq(leafsIds);
        const allLeafsIds = getLeafsIds(nodes[0]);

        if (isSelected) {
          const notSelectedLeafsIds = uniqueLeafsIds.filter(id => !value.includes(id));
          const newValues = [...value, ...notSelectedLeafsIds];
          const isAllLeafsSelected = difference(allLeafsIds, newValues).length === 0 && nodes.length === 1;

          if (isAllLeafsSelected && !isExternallyFiltered) {
            onChange([nodes[0].id]);
          } else {
            onChange(newValues);
          }
        } else {
          const isRootDeselected = nodes[0].id === node.id && nodes.length === 1;
          const wasRootSelected = nodes[0].id === value[0] && nodes.length === 1;
          if (isRootDeselected && !isExternallyFiltered) {
            onChange([]);
            return;
          } else if (wasRootSelected && !isExternallyFiltered) {
            onChange(allLeafsIds.filter(id => !uniqueLeafsIds.includes(id)));
            return;
          }
          onChange(value.filter(id => !uniqueLeafsIds.includes(id)));
        }
      },
      [value, onChange, getLeafsIds, nodes, isExternallyFiltered]
    );

    const handleSingleSelectChange = useCallback(
      (node: TreeNode) => {
        onChange([node.id]);
        return;
      },
      [onChange]
    );

    const onChangeTreeNode = useCallback(
      (node: TreeNode, isSelected: boolean) => {
        if (isSingle) {
          handleSingleSelectChange(node);
        } else {
          handleMultiSelectChange(node, isSelected);
        }
      },
      [handleSingleSelectChange, isSingle, handleMultiSelectChange]
    );

    const toggleNodeExpand = useCallback(
      (nodeId: OptionIdType, childrenIds?: OptionIdType[]) => {
        const isExpanded = expandedIds.includes(nodeId);
        if (isExpanded) {
          setExpandedIds(expandedIds.filter(id => id !== nodeId && !childrenIds?.includes(id)));
        } else {
          setExpandedIds([...expandedIds, nodeId]);
        }
      },
      [expandedIds]
    );

    const processTreeNode = useCallback(
      (node: TreeNode, depth: number, isParentSelected?: boolean): ProcessedTreeNodeResult => {
        const isNodeLeaf = isLeaf(node);
        const shouldCountParentFromValues = isNodeLeaf || isSingle;
        const isSelected = isParentSelected || (value.includes(node.id) && shouldCountParentFromValues);
        const processedChildren = node.children.map(node => processTreeNode(node, depth + 1, isSelected));

        const isEveryChildrenChecked =
          processedChildren.length > 0 && processedChildren.every(result => result.isChecked);
        const hasSomeChildrenChecked =
          processedChildren.length > 0 && processedChildren.some(result => result.isChecked || result.isHalfChecked);

        const isChecked = isSelected || isEveryChildrenChecked;
        const isHalfChecked = !isChecked && hasSomeChildrenChecked;

        const onClick = (isSelected: boolean) => {
          onChangeTreeNode(node, isSelected);
        };

        const isExpanded = expandedIds.includes(node.id);

        const onExpand = () => {
          toggleNodeExpand(
            node.id,
            node.children.map(child => child.id)
          );
        };
        const shouldShowExpanded = (isExpanded || !isExpandable) && !searchQuery;

        return {
          element: (
            <React.Fragment key={node.id}>
              <div className={classes('item-container')}>
                {renderTreeItem ? (
                  renderTreeItem({
                    depth,
                    onClick,
                    isChecked,
                    isHalfChecked,
                    isDisabled: node.isDisabled,
                    name: node.name,
                    description: node.description,
                    disabledDescription: node.disabledDescription,
                    onExpand,
                    isExpanded,
                    isLeaf: isNodeLeaf,
                    nodeId: node.id,
                    type: node.type,
                  })
                ) : (
                  <TreeItem
                    isChecked={isChecked}
                    isHalfChecked={isHalfChecked}
                    name={node.name}
                    description={node.description}
                    depth={depth}
                    onClick={onClick}
                    isEmpty={processedChildren.length === 0}
                  />
                )}
              </div>
              <AnimatePresence exitBeforeEnter initial={false}>
                {shouldShowExpanded && (
                  <motion.div
                    {...expandAnimationProps}
                    className={classes('sub-items', { top: depth === 0 })}
                    data-selector="tree-list-sub-items"
                  >
                    {processedChildren.map(result => result.element)}
                  </motion.div>
                )}
              </AnimatePresence>
            </React.Fragment>
          ),
          isChecked,
          isHalfChecked,
        };
      },
      [
        value,
        onChangeTreeNode,
        isLeaf,
        renderTreeItem,
        expandedIds,
        toggleNodeExpand,
        isExpandable,
        searchQuery,
        isSingle,
      ]
    );

    const items = useMemo(() => {
      if (nodes.length === 0) {
        return [];
      }

      const nodesToDisplay = filterTreeNodes(nodes, searchQuery);
      const isRootSelected = nodes[0].id === value[0] && nodes[0].children.length > 0;
      const isNodesEmpty = nodesToDisplay.length === 0;

      return isNodesEmpty && renderEmptyPlaceholder
        ? renderEmptyPlaceholder()
        : nodesToDisplay.map(node => processTreeNode(node, 0, isRootSelected).element);
    }, [nodes, processTreeNode, searchQuery, value, renderEmptyPlaceholder]);

    return <STreeList className={className}>{items}</STreeList>;
  }
);
