import { observer } from "mobx-react";
import * as React from "react";
import { useDrag, useDrop } from "react-dnd";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { MAX_TITLE_LENGTH } from "@shared/constants";
import Collection from "~/models/Collection";
import Document from "~/models/Document";
import Fade from "~/components/Fade";
import useBoolean from "~/hooks/useBoolean";
import useStores from "~/hooks/useStores";
import DocumentMenu from "~/menus/DocumentMenu";
import { NavigationNode } from "~/types";
import Disclosure from "./Disclosure";
import DropCursor from "./DropCursor";
import DropToImport from "./DropToImport";
import EditableTitle from "./EditableTitle";
import SidebarLink, { DragObject } from "./SidebarLink";

type Props = {
  node: NavigationNode;
  canUpdate: boolean;
  collection?: Collection;
  activeDocument: Document | null | undefined;
  prefetchDocument: (documentId: string) => Promise<any>;
  depth: number;
  index: number;
  parentId?: string;
};

function DocumentLink(
  {
    node,
    canUpdate,
    collection,
    activeDocument,
    prefetchDocument,
    depth,
    index,
    parentId,
  }: Props,
  ref: React.RefObject<HTMLAnchorElement>
) {
  const { documents, policies } = useStores();
  const { t } = useTranslation();
  const isActiveDocument = activeDocument && activeDocument.id === node.id;
  const hasChildDocuments = !!node.children.length;
  const document = documents.get(node.id);
  const { fetchChildDocuments } = documents;

  React.useEffect(() => {
    if (isActiveDocument && hasChildDocuments) {
      fetchChildDocuments(node.id);
    }
  }, [fetchChildDocuments, node, hasChildDocuments, isActiveDocument]);

  const pathToNode = React.useMemo(
    () =>
      collection && collection.pathToDocument(node.id).map((entry) => entry.id),
    [collection, node]
  );

  const showChildren = React.useMemo(() => {
    return !!(
      hasChildDocuments &&
      activeDocument &&
      collection &&
      (collection
        .pathToDocument(activeDocument.id)
        .map((entry) => entry.id)
        .includes(node.id) ||
        isActiveDocument)
    );
  }, [hasChildDocuments, activeDocument, isActiveDocument, node, collection]);
  const [expanded, setExpanded] = React.useState(showChildren);

  React.useEffect(() => {
    if (showChildren) {
      setExpanded(showChildren);
    }
  }, [showChildren]);

  // when the last child document is removed,
  // also close the local folder state to closed
  React.useEffect(() => {
    if (expanded && !hasChildDocuments) {
      setExpanded(false);
    }
  }, [expanded, hasChildDocuments]);

  const handleDisclosureClick = React.useCallback(
    (ev: React.SyntheticEvent) => {
      ev.preventDefault();
      ev.stopPropagation();
      setExpanded(!expanded);
    },
    [expanded]
  );

  const handleMouseEnter = React.useCallback(() => {
    prefetchDocument(node.id);
  }, [prefetchDocument, node]);

  const handleTitleChange = React.useCallback(
    async (title: string) => {
      if (!document) return;
      await documents.update(
        {
          id: document.id,
          text: document.text,
          title,
        },
        {
          lastRevision: document.revision,
        }
      );
    },
    [documents, document]
  );
  const [menuOpen, handleMenuOpen, handleMenuClose] = useBoolean();
  const isMoving = documents.movingDocumentId === node.id;
  const manualSort = collection?.sort.field === "index";

  // Draggable
  const [{ isDragging }, drag] = useDrag({
    type: "document",
    item: () => ({
      ...node,
      depth,
      active: isActiveDocument,
      collectionId: collection?.id || "",
    }),
    collect: (monitor) => ({
      isDragging: !!monitor.isDragging(),
    }),
    canDrag: () => {
      return (
        policies.abilities(node.id).move ||
        policies.abilities(node.id).archive ||
        policies.abilities(node.id).delete
      );
    },
  });

  const hoverExpanding = React.useRef<ReturnType<typeof setTimeout>>();

  // We set a timeout when the user first starts hovering over the document link,
  // to trigger expansion of children. Clear this timeout when they stop hovering.
  const resetHoverExpanding = React.useCallback(() => {
    if (hoverExpanding.current) {
      clearTimeout(hoverExpanding.current);
      hoverExpanding.current = undefined;
    }
  }, []);

  // Drop to re-parent
  const [{ isOverReparent, canDropToReparent }, dropToReparent] = useDrop({
    accept: "document",
    drop: (item: DragObject, monitor) => {
      if (monitor.didDrop()) return;
      if (!collection) return;
      documents.move(item.id, collection.id, node.id);
    },
    canDrop: (_item, monitor) =>
      !!pathToNode && !pathToNode.includes(monitor.getItem<DragObject>().id),
    hover: (item, monitor) => {
      // Enables expansion of document children when hovering over the document
      // for more than half a second.
      if (
        hasChildDocuments &&
        monitor.canDrop() &&
        monitor.isOver({
          shallow: true,
        })
      ) {
        if (!hoverExpanding.current) {
          hoverExpanding.current = setTimeout(() => {
            hoverExpanding.current = undefined;

            if (
              monitor.isOver({
                shallow: true,
              })
            ) {
              setExpanded(true);
            }
          }, 500);
        }
      }
    },
    collect: (monitor) => ({
      isOverReparent: !!monitor.isOver({
        shallow: true,
      }),
      canDropToReparent: monitor.canDrop(),
    }),
  });

  // Drop to reorder
  const [{ isOverReorder, isDraggingAnyDocument }, dropToReorder] = useDrop({
    accept: "document",
    drop: (item: DragObject) => {
      if (!collection) return;
      if (item.id === node.id) return;

      if (expanded) {
        documents.move(item.id, collection.id, node.id, 0);
        return;
      }

      documents.move(item.id, collection.id, parentId, index + 1);
    },
    collect: (monitor) => ({
      isOverReorder: !!monitor.isOver(),
      isDraggingAnyDocument: !!monitor.canDrop(),
    }),
  });

  return (
    <>
      <Relative onDragLeave={resetHoverExpanding}>
        <Draggable
          key={node.id}
          ref={drag}
          $isDragging={isDragging}
          $isMoving={isMoving}
        >
          <div ref={dropToReparent}>
            <DropToImport documentId={node.id} activeClassName="activeDropZone">
              <SidebarLink
                onMouseEnter={handleMouseEnter}
                to={{
                  pathname: node.url,
                  state: {
                    title: node.title,
                  },
                }}
                label={
                  <>
                    {hasChildDocuments && (
                      <Disclosure
                        expanded={expanded && !isDragging}
                        onClick={handleDisclosureClick}
                      />
                    )}
                    <EditableTitle
                      title={node.title || t("Untitled")}
                      onSubmit={handleTitleChange}
                      canUpdate={canUpdate}
                      maxLength={MAX_TITLE_LENGTH}
                    />
                  </>
                }
                isActive={(match, location) =>
                  !!match && location.search !== "?starred"
                }
                isActiveDrop={isOverReparent && canDropToReparent}
                depth={depth}
                exact={false}
                showActions={menuOpen}
                scrollIntoViewIfNeeded={!document?.isStarred}
                ref={ref}
                menu={
                  document && !isMoving ? (
                    <Fade>
                      <DocumentMenu
                        document={document}
                        onOpen={handleMenuOpen}
                        onClose={handleMenuClose}
                      />
                    </Fade>
                  ) : undefined
                }
              />
            </DropToImport>
          </div>
        </Draggable>
        {manualSort && isDraggingAnyDocument && (
          <DropCursor isActiveDrop={isOverReorder} innerRef={dropToReorder} />
        )}
      </Relative>
      {expanded && !isDragging && (
        <>
          {node.children.map((childNode, index) => (
            <ObservedDocumentLink
              key={childNode.id}
              collection={collection}
              node={childNode}
              activeDocument={activeDocument}
              prefetchDocument={prefetchDocument}
              depth={depth + 1}
              canUpdate={canUpdate}
              index={index}
              parentId={node.id}
            />
          ))}
        </>
      )}
    </>
  );
}

const Relative = styled.div`
  position: relative;
`;

const Draggable = styled.div<{ $isDragging?: boolean; $isMoving?: boolean }>`
  opacity: ${(props) => (props.$isDragging || props.$isMoving ? 0.5 : 1)};
  pointer-events: ${(props) => (props.$isMoving ? "none" : "all")};
`;

const ObservedDocumentLink = observer(React.forwardRef(DocumentLink));

export default ObservedDocumentLink;
