import { useContext, useEffect } from 'react';
import { shouldTriggerRun } from './shouldTriggerRun';
import { syntaxTree } from '@codemirror/language';
import { SyntaxNode, SyntaxNodeRef } from '@lezer/common';
import { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { VizFileId } from '@vizhub/viz-types';
import { PaneId } from '../types';
import { editorCacheKey } from './useEditorCache';

/*
  The following is a helpful resource for the following Code Mirror Syntax Tree methods below
  https://lezer.codemirror.net/docs/ref/#common
*/

// Store the element in the current editor DOM to highlight, indicating a potential jump to definition is possible
let activeJumpingElement: HTMLSpanElement = null;

// Store the syntax node representing the destination within the syntax tree
let definingNode: SyntaxNode = null;

// Example nesting types for the specific language to find level in the syntax tree
const nestingTypes = new Set<string>([
  'FunctionDeclaration',
  'ClassDeclaration',
  'MethodDeclaration',
  'Block',
  'IfStatement',
  'ForStatement',
  'WhileStatement',
  'SwitchStatement',
  'TryStatement',
  'CatchClause',
  'WithStatement',
  'ArrowFunction',
  'ImportGroup',
]);

// Example declaration types for the specific language to find definitions in the syntax tree
const declarationTypes = new Set<string>([
  'VariableDeclaration',
  'FunctionDeclaration',
  'ClassDeclaration',
  'MethodDeclaration',
  'PropertyDeclaration',
  'ImportDeclaration',
  'ExportDeclaration',
  'CallExpression',
  'ArrayPattern',
  'ObjectPattern',
  'PatternProperty',
  'ArgList',
  'TypeArgList',
  'ParamList',
  'ForOfSpec',
]);

function getIdentifierContext(
  identifier: SyntaxNode,
): number {
  let current: SyntaxNode = identifier;
  let levels: number = 0;

  // Traverse up the tree to find the total depth, only counting valid nesting types
  while (current && current.type) {
    const parentType: string = current.type.name;

    if (nestingTypes.has(parentType)) {
      levels++;
    }

    current = current.parent;
  }

  return levels;
}

function jumpToDefinition(
  editor: EditorView,
  node: SyntaxNode,
): SyntaxNode {
  const state: EditorState = editor.state;
  const definitions: Array<{
    identifier: SyntaxNode;
    context: number;
  }> = [];

  // From an identifier in the syntax tree, fetch the name and context to find closest defining syntax node
  const identifier: SyntaxNode = node;
  const identifierName: string = state.doc.sliceString(
    identifier.from,
    identifier.to,
  );
  const context: number = getIdentifierContext(identifier);

  if (identifier) {
    syntaxTree(state).iterate({
      enter(tree: SyntaxNodeRef) {
        // Traverse syntax tree to find positions of respective identifier definitions within context
        if (
          declarationTypes.has(tree.name) ||
          nestingTypes.has(tree.name)
        ) {
          const parent: SyntaxNode = tree.node;

          // Fetch a host of potential identifiers in an attempt to find the defining syntax node
          const children: Array<SyntaxNode> = [
            ...parent.getChildren('VariableDefinition'),
            ...parent.getChildren('PropertyDefinition'),
            ...parent.getChildren('PropertyName'),
            ...parent.getChildren('Identifier'),
          ];

          children.forEach((child: SyntaxNode) => {
            const name: string = state.doc.sliceString(
              child.from,
              child.to,
            );

            // Ensure no jump to self by comparing syntax node id's
            if (
              name === identifierName &&
              child.type.id !== identifier.type.id
            ) {
              definitions.push({
                identifier: child,
                context: getIdentifierContext(child),
              });
            }
          });
        }
      },
    });

    if (definitions.length > 0) {
      // Sort definitions by their context in the syntax tree
      definitions.sort((a, b) => a.context - b.context);

      let closestDefinition: SyntaxNode =
        definitions[0].identifier;

      for (let i = definitions.length - 1; i >= 0; i--) {
        if (definitions[i].context <= context) {
          closestDefinition = definitions[i].identifier;
          break;
        }
      }

      return closestDefinition;
    }

    return null;
  }
}

// Sidebar keyboard shortcuts in form Ctrl + Shift + <key>
const sideBarKeyBoardMap = {
  E: 'files-icon',
  F: 'search-icon',
  K: 'shortcut-icon',
  B: 'bug-icon',
  S: 'settings-icon',
  N: 'new-file-icon',
  D: 'new-directory-icon',
  A: 'auto-focus-icon',
};

// This module implements the keyboard shortcuts
// for the VZCode editor.
// These include:
// * Alt-w: Close the current tab
// * Alt-n: Open the create file modal
// * Alt-PageUp: Change the active tab to the previous one
// * Alt-PageDown: Change the active tab to the next one
// * Ctrl-s or Shift-Enter: Run the code and format it with Prettier
// * Ctrl-Click: Jump to closest definition for a potential identifier
export const useKeyboardShortcuts = ({
  closeTabs,
  activeFileId,
  activePaneId,
  handleOpenCreateFileModal,
  setActiveFileLeft,
  setActiveFileRight,
  toggleSearchFocused,
  runPrettierRef,
  runCodeRef,
  sidebarRef,
  editorCache,
  codeEditorRef,
}: {
  closeTabs: (fileIds: VizFileId[]) => void;
  activeFileId: VizFileId | null;
  activePaneId: PaneId;
  handleOpenCreateFileModal: () => void;
  setActiveFileLeft: () => void;
  setActiveFileRight: () => void;
  toggleSearchFocused: () => void;
  runPrettierRef: React.MutableRefObject<() => void>;
  runCodeRef: React.MutableRefObject<() => void>;
  sidebarRef: React.RefObject<HTMLDivElement>;
  editorCache: Map<string, { editor: EditorView }>;
  codeEditorRef: React.RefObject<HTMLDivElement>;
}) => {
  useEffect(() => {
    // This key is needed to look up the current editor in the editor cache.
    const cacheKey = editorCacheKey(
      activeFileId,
      activePaneId,
    );

    const handleKeyPress = (event: KeyboardEvent) => {
      if (shouldTriggerRun(event)) {
        event.preventDefault();

        // Run Prettier
        const runPrettier = runPrettierRef.current;
        if (runPrettier !== null) {
          runPrettier();
        }

        // Run the code
        const runCode = runCodeRef.current;

        if (runCode !== null) {
          runCode();
        }
        return;
      }

      if (event.ctrlKey && event.shiftKey) {
        // Handle keyboard shortcuts related to the side bar icons
        document
          .getElementById(sideBarKeyBoardMap[event.key])
          ?.click();

        // Ensure the search input is always focused
        if (event.key === 'F') {
          toggleSearchFocused();
        }
      } else if (event.ctrlKey && event.key === ',') {
        document
          .getElementById(sideBarKeyBoardMap['S'])
          ?.click();
      }

      if (event.ctrlKey === true) {
        // On holding CTRL key, search for a potential definition jump using mouse location
        document.addEventListener(
          'mouseover',
          handleMouseOver,
        );
      }

      if (event.altKey === true) {
        // Alt-w: Close the current tab
        if (event.key === 'w') {
          // TODO clean this up so we can remove `activeFileId`
          // as a dependency
          // TODO closeActiveTab()
          if (activeFileId) {
            closeTabs([activeFileId]);
          }
          return;
        }

        // Alt-n: Open the create file modal
        if (event.key === 'n') {
          handleOpenCreateFileModal();
          return;
        }

        // Alt-PageUp: Change the active tab to the previous one
        if (event.key === 'PageUp') {
          setActiveFileLeft();
          return;
        }

        // Alt-PageDown: Change the active tab to the next one
        if (event.key === 'PageDown') {
          setActiveFileRight();
          return;
        }

        if (event.key === '1') {
          if (sidebarRef.current) {
            sidebarRef.current.focus();
          }
        }

        if (event.key === '2') {
          if (codeEditorRef.current) {
            codeEditorRef.current.focus();
          }
        }
      }
    };

    const resetActiveJumpingElement = (): void => {
      if (activeJumpingElement) {
        activeJumpingElement.style.cursor = 'initial';
        activeJumpingElement.style.textDecoration = 'none';
        activeJumpingElement = definingNode = null;
      }

      document.removeEventListener(
        'mouseover',
        handleMouseOver,
      );
      document.removeEventListener(
        'mousedown',
        jumpToDefinitionHandler,
      );
    };

    const jumpToDefinitionHandler = (
      event: MouseEvent,
    ): void => {
      // Ensure the current destination node is defined
      // and current cursor position matches highlighted element
      if (
        !definingNode ||
        (event.target as HTMLSpanElement) !==
          activeJumpingElement
      ) {
        return;
      }

      // Don't crash if no active file
      if (!activeFileId) {
        return;
      }

      // Move current cursor and center view in the editor to destination node

      const editor: EditorView =
        editorCache.get(cacheKey).editor;
      const closestDefinition: SyntaxNode = definingNode;

      editor.dispatch({
        selection: {
          anchor: closestDefinition.from,
          head: closestDefinition.to,
        },
        scrollIntoView: true,
        effects: EditorView.scrollIntoView(
          closestDefinition.from,
          {
            y: 'center',
          },
        ),
      });

      resetActiveJumpingElement();
    };

    const handleKeyRelease = (event: KeyboardEvent) => {
      // On releasing CTRL key, reset all active definition jumping elements and listeners
      if (!event.ctrlKey) {
        resetActiveJumpingElement();
      }
    };

    const handleMouseOver = (event: MouseEvent) => {
      if (!activeFileId) {
        return;
      }

      const editor: EditorView =
        editorCache.get(cacheKey).editor;
      const tree = syntaxTree(editor.state);
      const element = event.target as HTMLSpanElement;

      // Ensure the identifier element can be found and is within the current editor DOM
      if (
        element == null ||
        !editor.dom.contains(element)
      ) {
        return;
      }

      const position: number = editor.posAtDOM(element);
      const identifier: SyntaxNode = tree.resolveInner(
        position,
        1,
      );

      // All valid identifiers must be span elements to find a potential defining jump in editor
      if (
        identifier &&
        element instanceof HTMLSpanElement
      ) {
        const potentialJump: SyntaxNode = jumpToDefinition(
          editor,
          identifier,
        );

        // Only allowing to jump to other definition nodes
        if (
          potentialJump &&
          identifier.type.id !== potentialJump.type.id
        ) {
          if (activeJumpingElement) {
            activeJumpingElement.style.cursor = 'initial';
            activeJumpingElement.style.textDecoration =
              'none';
          }

          activeJumpingElement = element;
          definingNode = potentialJump;

          activeJumpingElement.style.cursor = 'pointer';
          activeJumpingElement.style.textDecoration =
            'underline';

          // CTRL + Click: Jump to relative definition, which is removed on CTRL key release
          document.addEventListener(
            'mousedown',
            jumpToDefinitionHandler,
            { once: true },
          );
        }
      }
    };

    document.addEventListener('keydown', handleKeyPress);
    document.addEventListener('keyup', handleKeyRelease);

    return () => {
      document.removeEventListener(
        'keydown',
        handleKeyPress,
      );
      document.removeEventListener(
        'keyup',
        handleKeyRelease,
      );
    };
  }, [
    handleOpenCreateFileModal,
    closeTabs,
    activeFileId,
    setActiveFileLeft,
    setActiveFileRight,
  ]);
};
