import { EditorView } from 'codemirror';
import {
  Compartment,
  EditorState,
} from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { svelte } from '@replit/codemirror-lang-svelte';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { json1Sync } from 'codemirror-ot';

import { autocompletion } from '@codemirror/autocomplete';
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
// import { showMinimap } from '@replit/codemirror-minimap';
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
import { Diagnostic, linter } from '@codemirror/lint';

import { json1Presence, textUnicode } from '../../ot';
import {
  PaneId,
  ShareDBDoc,
  TabState,
  Username,
} from '../../types';
import { VizFileId, VizContent } from '@vizhub/viz-types';
import { json1PresenceBroadcast } from './json1PresenceBroadcast';
import { json1PresenceDisplay } from './json1PresenceDisplay';
import {
  colorsInTextPlugin,
  rotationIndicator,
  widgets,
} from './InteractiveWidgets';
import {
  EditorCache,
  editorCacheKey,
  EditorCacheValue,
} from '../useEditorCache';
import { ThemeLabel, themeOptionsByLabel } from '../themes';
import { keymap } from '@codemirror/view';
import { basicSetup } from './basicSetup';
import { InteractRule } from '@replit/codemirror-interact';
import rainbowBrackets from '../CodeEditor/rainbowBrackets';
import { cssLanguage } from '@codemirror/lang-css';
import { javascriptLanguage } from '@codemirror/lang-javascript';
import { copilot } from './Copilot';

const DEBUG = false;

// Enables TypeScript +JSX support in CodeMirror.
const tsx = () =>
  javascript({ jsx: true, typescript: true });

const htmlConfig = {
  matchClosingTags: true,
  selfClosingTags: false,
  autoCloseTags: true,
  extraTags: {},
  extraGlobalAttributes: {},
  nestedLanguages: [
    {
      tag: 'script',
      language: javascript,
      parser: javascriptLanguage.parser,
    },
    {
      tag: 'style',
      language: css,
      parser: cssLanguage.parser,
    },
  ],
  nestedAttributes: [],
};

// Language extensions for CodeMirror.
// Keys are file extensions.
// Values are CodeMirror extensions.
// TODO consider moving this to a separate file.
const languageExtensions = {
  json,
  tsx,
  js: tsx,
  jsx: tsx,
  ts: tsx,
  html: () => html(htmlConfig),
  css,
  md: markdown,
  svelte,
};

// Gets a value at a path in an object.
// e.g. getAtPath({a: {b: 1}}, ['a', 'b']) === 1
const getAtPath = (obj, path) => {
  let current = obj;
  for (const key of path) {
    if (!current) return undefined;
    current = current[key];
  }
  return current;
};

// Extend the EditorCacheValue type to include the compartments and the updateRainbowBrackets method
interface ExtendedEditorCacheValue
  extends EditorCacheValue {
  themeCompartment: Compartment;
  rainbowBracketsCompartment: Compartment;
  updateRainbowBrackets: (enabled: boolean) => void;
}

// Gets or creates an `editorCache` entry for the given file id.
// Looks in `editorCache` first, and if not found, creates a new editor.
export const getOrCreateEditor = ({
  paneId = 'root',
  fileId,
  shareDBDoc,
  content,
  filesPath,
  localPresence,
  docPresence,
  theme,
  onInteract,
  editorCache,
  usernameRef,
  customInteractRules,
  enableAutoFollowRef,
  openTab,
  aiCopilotEndpoint,
  rainbowBracketsEnabled = true,
}: {
  // TODO pass this in from the outside
  paneId?: PaneId;
  fileId: VizFileId;

  // The ShareDB document that contains the file.
  // Used when the editor is created for:
  //  * Figuring out the initial text
  //  * Figuring out the initial language extension
  //    based on the file extension.
  // It's also passed into the `json1Sync` extension,
  // which is used for multiplayer editing.
  // This can be `undefined` in the case where we are
  // viewing files read-only, in which case multiplayer
  // editing is not enabled.
  shareDBDoc: ShareDBDoc<VizContent> | undefined;

  // The initial content to present.
  content: VizContent;

  filesPath: string[];
  localPresence: any;
  docPresence: any;
  theme: ThemeLabel;
  onInteract?: () => void;
  editorCache: EditorCache;
  usernameRef: React.MutableRefObject<Username>;
  aiAssistEndpoint?: string;
  customInteractRules?: Array<InteractRule>;

  // Ref to a boolean that determines whether to
  // enable auto-following the cursors of remote users.
  enableAutoFollowRef: React.MutableRefObject<boolean>;
  openTab: (tabState: TabState) => void;
  aiCopilotEndpoint?: string;
  rainbowBracketsEnabled?: boolean; // New parameter type
}): ExtendedEditorCacheValue => {
  // Cache hit

  const cacheKey = editorCacheKey(fileId, paneId);

  if (editorCache.has(cacheKey)) {
    return editorCache.get(
      cacheKey,
    ) as ExtendedEditorCacheValue;
  }

  // Cache miss

  // Compute `text` and `fileExtension` from the ShareDB document.
  const textPath = [...filesPath, fileId, 'text'];
  const namePath = [...filesPath, fileId, 'name'];
  const text = getAtPath(content, textPath);
  const name = getAtPath(content, namePath);
  const fileExtension = name.split('.').pop();

  // Create a compartment for the theme so that it can be changed dynamically.
  // Inspired by: https://github.com/craftzdog/cm6-themes/blob/main/example/index.ts
  let themeCompartment = new Compartment();

  // Create a compartment for rainbow brackets so that it can be enabled/disabled dynamically.
  let rainbowBracketsCompartment = new Compartment();

  // The CodeMirror extensions to use.
  // const extensions = [autocompletion(), html(htmlConfig)]
  const extensions = [];

  // This plugin implements multiplayer editing,
  // real-time synchronozation of changes across clients.
  // Does not deal with showing others' cursors.
  if (shareDBDoc) {
    extensions.push(
      json1Sync({
        shareDBDoc,
        path: textPath,
        json1: json1Presence,
        textUnicode,
      }),
    );

    // Deals with broadcasting changes in cursor location and selection.
    if (localPresence) {
      extensions.push(
        json1PresenceBroadcast({
          path: textPath,
          localPresence,
          usernameRef,
        }),
      );
    }

    // Deals with receiving the broadcast from other clients and displaying them.
    if (docPresence) {
      extensions.push(
        json1PresenceDisplay({
          path: textPath,
          docPresence,
          enableAutoFollowRef,
          openTab,
        }),
      );
    }
  } else {
    // If the ShareDB document is not provided,
    // then we do not allow editing.
    extensions.push(EditorView.editable.of(false));
  }

  extensions.push(colorsInTextPlugin);

  // This is the "basic setup" for CodeMirror,
  // which actually adds a ton on functionality.
  // TODO vet this functionality, and determine how much
  // we want to replace with
  // https://github.com/vizhub-core/vzcode/issues/134
  extensions.push(basicSetup);

  // This supports dynamic changing of the theme.
  extensions.push(
    themeCompartment.of(themeOptionsByLabel[theme].value),
  );

  // Adds compartment for rainbow brackets with initial toggle state.
  extensions.push(
    rainbowBracketsCompartment.of(
      rainbowBracketsEnabled ? rainbowBrackets() : [],
    ),
  );

  // TODO handle dynamic changing of the file extension.
  // TODO handle dynamic file extensions by making
  // the CodeMirror language extension dynamic
  // using a Compartment.
  const languageCompartment = new Compartment();
  // See https://github.com/vizhub-core/vzcode/issues/55
  const getLanguageExtension = (fileExtension: string) => {
    const languageFunc = languageExtensions[fileExtension];
    return languageFunc ? languageFunc() : undefined;
  };

  const languageExtension =
    getLanguageExtension(fileExtension);
  if (languageExtension) {
    extensions.push(
      languageCompartment.of(languageExtension),
    );
  } else {
    // Not sure if this case even works.
    // TODO manually test this case by creating a file
    // that has no extension, opening it up,
    // and then adding an extension.
    // console.warn(
    //   `No language extension for file extension: ${fileExtension}`,
    // );
    // We still need to push the compartment,
    // otherwise the compartment won't work when
    // a file extension _is_ added later on.
    extensions.push(languageCompartment.of([]));
  }

  // Add interactive widgets.
  // Includes the Alt+drag functionality for numbers.
  // Calls `onInteract` when one of those widgets is interacted with.
  // This can be used to trigger a transition to throttled mode
  // for hot reloading.
  // TODO consider leveraging the new `dragEnd` event handler.
  // and removing the `onInteract` callback, replacing it with
  // `onInteractStart` and `onInteractEnd`.
  // That may be tricky for one-off interactions though, like
  // the boolean checkboxes. The color pickers are also tricky,
  // as they would also need to be able to handle `onInteractEnd`.
  // See https://github.com/replit/codemirror-interact/issues/14
  extensions.push(
    widgets({ onInteract, customInteractRules }),
  );

  // TODO fix the bugginess in this one where
  // the highlight persists after the mouse leaves.
  // extensions.push(highlightWidgets);

  extensions.push(rotationIndicator);

  // extensions.push(
  //   AIAssistCodeMirrorKeyMap({
  //     shareDBDoc,
  //     fileId,
  //     tabList,
  //     aiAssistEndpoint,
  //     aiAssistOptions,
  //   }),
  // );

  // Add the extension that provides indentation markers.
  extensions.push(
    indentationMarkers({
      // thickness: 2,
      colors: {
        light: '#4d586b',
        dark: '#4d586b',
        activeLight: '#8e949f',
        activeDark: '#8e949f',
      },
    }),
  );

  // Show the minimap
  // See https://github.com/replit/codemirror-minimap#usage
  // This extension has poor performance, so it's disabled for now.
  // extensions.push(
  //   showMinimap.compute(['doc'], () => ({
  //     create: () => ({
  //       dom: document.createElement('div'),
  //     }),
  //     // Without this, performance is terrible.
  //     displayText: 'blocks',
  //   })),
  // );

  // VSCode keybindings
  // See https://github.com/replit/codemirror-vscode-keymap#usage
  // extensions.push(keymap.of(vscodeKeymap));
  extensions.push(
    keymap.of(
      vscodeKeymap.map((binding) => {
        // Here we override the Shift+Enter behavior specifically,
        // as that can be used to trigger a manual save/Prettier,
        // and the default behavior from the keymap interferes.
        if (binding.key === 'Enter') {
          delete binding.shift;
        }
        return binding;
      }),
    ),
  );

  // Adds copilot completions
  DEBUG &&
    console.log(
      '[getOrCreateEditor] aiCopilotEndpoint: ',
      aiCopilotEndpoint,
    );
  if (aiCopilotEndpoint) {
    extensions.push(copilot({ aiCopilotEndpoint }));
  }

  const editor = new EditorView({
    state: EditorState.create({
      doc: text,
      extensions,
    }),
  });

  const editorCacheValue: ExtendedEditorCacheValue = {
    editor,
    themeCompartment,
    rainbowBracketsCompartment,
    updateRainbowBrackets: (enabled) => {
      editor.dispatch({
        effects: rainbowBracketsCompartment.reconfigure(
          enabled ? rainbowBrackets() : [],
        ),
      });
    },
  };

  // Populate the cache.
  editorCache.set(cacheKey, editorCacheValue);

  // Initialize rainbow brackets based on the toggle state
  editorCacheValue.updateRainbowBrackets(
    rainbowBracketsEnabled,
  );

  return editorCacheValue;
};
