import React, { useEffect, useRef, useImperativeHandle } from "react";
import classNames from "classnames";
import { useConfig } from "../util";
import { StyledProps } from "../_type";
import { useDefaultValue } from "../form";
import { forwardRefWithStatics } from "../_util/forward-ref-with-statics";
import { MonacoDiffEditor } from "./MonacoDiffEditor";
import { useIsomorphicLayoutEffect } from "../_util/use-isomorphic-layout-effect";

type Obj = Record<any, any>;

export interface MonacoEditorProps<M extends Obj = any> extends StyledProps {
  /**
   * [monaco-editor](https://microsoft.github.io/monaco-editor/index.html)
   *
   * @docType typeof monaco
   */
  monaco: M;

  /**
   * 编辑器高度
   *
   * @default "100%"
   */
  height?: React.CSSProperties["height"];

  /**
   * 编辑器宽度
   *
   * @default "100%"
   */
  width?: React.CSSProperties["width"];

  /**
   * 编辑器初始语言
   */
  language?: string;

  /**
   * 编辑器初始设置，详见 [IStandaloneEditorConstructionOptions](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html)
   *
   * @docType monaco.editor.IStandaloneEditorConstructionOptions
   */
  options?: Parameters<M["editor"]["create"]>[1];

  /**
   * override language service, like editorService, [storageService](https://github.com/microsoft/vscode/blob/ff1e16eebb93af79fd6d7af1356c4003a120c563/src/vs/platform/storage/common/storage.ts#L37) ，详见 [IEditorOverrideServices](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IEditorOverrideServices.html)
   *
   * @docType monaco.editor.IEditorOverrideServices
   * @since 2.7.4
   */
  override?: Parameters<M["editor"]["create"]>[2];

  /**
   * 编辑器默认文本
   */
  defaultValue?: string;

  /**
   * 编辑器文本
   */
  value?: string;

  /**
   * 编辑器文本变化回调
   */
  onChange?: (
    value: string,
    context: {
      event: any;
    }
  ) => void;
}

export const MonacoEditor = forwardRefWithStatics(
  function MonacoEditor<M extends Obj>(
    {
      monaco,
      options = {},
      override = {},
      language,
      className,
      style,
      width,
      height,
      ...props
    }: MonacoEditorProps<M>,
    ref: React.Ref<{
      editor: ReturnType<M["editor"]["create"]>;
    }>
  ) {
    const { classPrefix } = useConfig();
    const { value, onChange } = useDefaultValue(props as any, "");

    const containerRef = useRef<HTMLDivElement>(null);
    const editorRef = useRef<ReturnType<M["editor"]["create"]>>(null);
    const changeEventRef = useRef(null);

    // https://github.com/react-monaco-editor/react-monaco-editor/blob/master/src/editor.js
    const preventTriggerChangeEvent = useRef<boolean>(false);

    useEffect(() => {
      if (editorRef.current) {
        editorRef.current.layout();
      }
    }, [width, height]);

    useEffect(() => {
      if (editorRef.current) {
        const model = editorRef.current.getModel();
        monaco.editor.setModelLanguage(model, language);
      }
    }, [language, monaco]);

    useEffect(() => {
      if (editorRef.current) {
        const editor = editorRef.current;
        const model = editor.getModel();
        if (value != null && value !== model.getValue()) {
          preventTriggerChangeEvent.current = true;
          editor.pushUndoStop();
          model.pushEditOperations(
            [],
            [
              {
                range: model.getFullModelRange(),
                text: value,
              },
            ],
            undefined
          );
          editor.pushUndoStop();
          preventTriggerChangeEvent.current = false;
        }
      }
    }, [value, monaco]);

    useIsomorphicLayoutEffect(() => {
      const editor = monaco.editor.create(
        containerRef.current,
        {
          ...(options as any),
          value,
          language,
        },
        override
      );
      editorRef.current = editor;
      changeEventRef.current = editor.onDidChangeModelContent(event => {
        if (!preventTriggerChangeEvent.current) {
          onChange(editor.getValue(), { event });
        }
      });

      return () => {
        editorRef.current.dispose();
        const model = editorRef.current.getModel();
        if (model) {
          model.dispose();
        }
        if (changeEventRef.current) {
          changeEventRef.current.dispose();
        }
      };
    }, []); // eslint-disable-line react-hooks/exhaustive-deps

    useImperativeHandle(ref, () => ({
      editor: editorRef.current,
    }));

    return (
      <div
        className={classNames(
          `${classPrefix}-monaco-editor-container`,
          className
        )}
        style={{
          ...(style || {}),
          height: height || "100%",
          width: width || "100%",
        }}
        ref={containerRef}
      />
    );
  },
  {
    DiffEditor: MonacoDiffEditor,
  }
) as (<M extends Obj>(
  props: MonacoEditorProps<M> & {
    ref?: React.Ref<{
      editor: ReturnType<M["editor"]["create"]>;
    }>;
  }
) => React.ReactElement) & {
  DiffEditor: typeof MonacoDiffEditor;
  displayName: string;
};

MonacoEditor.displayName = "MonacoEditor";
