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

type Obj = Record<any, any>;

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

  /**
   * 原始文本
   */
  original: string;

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

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

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

  /**
   * 编辑器初始设置，详见 [IDiffEditorConstructionOptions](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IDiffEditorConstructionOptions.html)
   *
   * @docType monaco.editor.IDiffEditorConstructionOptions
   */
  options?: Parameters<M["editor"]["createDiffEditor"]>[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"]["createDiffEditor"]>[2];

  /**
   * 默认变更文本
   */
  defaultValue?: string;

  /**
   * 变更文本
   */
  value?: string;

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

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

  const containerRef = useRef<HTMLDivElement>(null);
  const editorRef = useRef<ReturnType<M["editor"]["createDiffEditor"]>>(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 { original: originalModel } = editorRef.current.getModel();
      originalModel.setValue(original);
    }
  }, [original, monaco]);

  useEffect(() => {
    if (editorRef.current) {
      const {
        modified: modifiedModel,
        original: originalModel,
      } = editorRef.current.getModel();
      monaco.editor.setModelLanguage(modifiedModel, language);
      monaco.editor.setModelLanguage(originalModel, language);
    }
  }, [language, monaco]);

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

  useIsomorphicLayoutEffect(() => {
    const editor = monaco.editor.createDiffEditor(
      containerRef.current,
      {
        ...(options as any),
      },
      override
    );
    editorRef.current = editor;

    const originalModel = monaco.editor.createModel(original, language);
    const modifiedModel = monaco.editor.createModel(value, language);
    editor.setModel({
      original: originalModel,
      modified: modifiedModel,
    });

    changeEventRef.current = modifiedModel.onDidChangeContent(event => {
      if (!preventTriggerChangeEvent.current) {
        onChange(modifiedModel.getValue(), { event });
      }
    });

    return () => {
      editorRef.current.dispose();
      originalModel.dispose();
      modifiedModel.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}
    />
  );
}) as (<M extends Obj>(
  props: MonacoDiffEditorProps<M> & {
    ref?: React.Ref<{
      editor: ReturnType<M["editor"]["createDiffEditor"]>;
    }>;
  }
) => React.ReactElement) & {
  displayName: string;
};

MonacoDiffEditor.displayName = "MonacoDiffEditor";
