import React, { useRef, useEffect, useState } from "react";
import classNames from "classnames";
import { useLast } from "../_util/use-last";
import { StyledProps } from "../_type";
import { mergeStyle } from "../_util/merge-style";
import { ErrorTip, LoadingTip } from "../tips";
import { injectValue } from "../_util/inject-value";
import { useConfig } from "../_util/config-context";

type BasicType = string | boolean | number | (string | boolean | number)[];
type ExtractBasic<T> = Extract<T, BasicType>;
interface Message {
  type: string;
  payload?: any;
}

type PickBasic<T> = {
  [key in keyof T]: T[key] extends { [key: string]: any }
    ? PickBasic<T[key]>
    : ExtractBasic<T[key]>;
};

type IEditorOptions = any;
type IStandaloneEditorConstructionOptions = any;

/**
 * `options` 详见： [IStandaloneEditorConstructionOptions](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html)
 */
export interface CodeEditorOptions
  extends PickBasic<IStandaloneEditorConstructionOptions> {}

export interface CodeEditorProps extends StyledProps {
  /**
   * 编辑器配置，会合并下面的默认值：
    ```js
    { "language": "javascript" }
    ```
   * 该配置只有在初始渲染时传入有效，后续传入不再生效。
   * 支持的配置请参考： [IStandaloneEditorConstructionOptions](https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.ieditorconstructionoptions.html)
   *
   * > 注意：由于 `options` 经过 JSON 序列化给到 iframe，所以配置中传入函数的方式都会无效
   */
  options?: CodeEditorOptions;

  /**
   * 是否自动获得焦点
   * @default false
   */
  autoFocus?: boolean;

  /**
   * 加载时显示的文本
   * @default <LoadingTip />
   */
  loadingPlaceholder?: React.ReactNode;

  /**
   * 出现错误或超时显示的文本
   * @docType React.ReactNode | ((retry: () => void) => React.ReactNode)
   * @default retry => <ErrorTip onRetry={retry} />
   */
  errorPlaceholder?: React.ReactNode | ((retry: () => void) => React.ReactNode);

  /**
   * 发生编辑时回调
   */
  onEdit?: (editor: CodeEditorInstance) => void;

  /**
   * 发生保存（Ctrl + S）时回调
   */
  onSave?: (editor: CodeEditorInstance) => void;

  /**
   * 编辑器可用时回调
   */
  onReady?: (editor: CodeEditorInstance) => void;

  /**
   * 加载编辑器发生错误时回调
   */
  onError?: (error: Error) => void;

  /**
   * 接收到 iframe 消息时回调，若用户调用了 event.preventDefault, 则不执行内部的默认行为
   */
  onMessage?: (message: Message, event: MessageEvent) => void;

  /**
   * 加载编辑器超时时间（ms）
   * @default 10000
   */
  timeout?: number;

  /**
   * 加载编辑器超时时回调
   */
  onTimeout?: () => void;

  /*
   * 供 iframe 加载的 URL 地址
   *
   * 当默认提供的 Editor 不满足要求时，可定制页面供 iframe 加载
   */
  src?: string;
}

export interface CodeEditorInstance {
  /**
   * 异步获取编辑器当前文本
   */
  getValue(options?: {
    preserveBOM: boolean;
    lineEnding: "\n" | "\r\n";
  }): Promise<string>;

  /**
   * 设置编辑器当前文本
   * @param value
   */
  setValue(value: string): void;

  /**
   * 聚焦编辑器
   */
  focus(): void;

  /**
   * 更新配置
   */
  updateOptions(newOptions: IEditorOptions): void;

  /**
   * 向编辑器内部传递消息
   */
  sendMessage(type: string, payload?: any): void;
}

// 获取值时自增引用
let nextValueKey = 0;
export function CodeEditor(props: CodeEditorProps) {
  const { classPrefix } = useConfig();
  const [ready, setReady] = useState(false);
  const [error, setError] = useState(false);
  const timerRef = useRef(null);
  const {
    options,
    autoFocus,
    className,
    style,
    onEdit,
    onReady,
    onSave,
    onError,
    onMessage,
    timeout = 10000,
    onTimeout,
    loadingPlaceholder = <LoadingTip />,
    errorPlaceholder = retry => <ErrorTip onRetry={retry} />,
    src = "https://cloudcache.tencentcs.com/qcloud/vendors/monaco/editor.html",
  } = props;

  const handler = useLast({ onEdit, onReady, onSave, onError, onMessage });
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const valueCallbackMap = useRef(new Map<string, Function>());

  useEffect(() => {
    if (!iframeRef.current) {
      return () => null;
    }
    let instance: CodeEditorInstance;
    const callHandler = (method: keyof typeof handler.current, ...args) => {
      if (handler.current && typeof handler.current[method] === "function") {
        if (method === "onError") {
          handler.current[method](args[0]);
          return;
        }
        if (method === "onMessage") {
          handler.current[method](args[0], args[1]);
          return;
        }

        handler.current[method](instance);
      }
    };
    const receive = (evt: MessageEvent) => {
      if (
        !iframeRef.current ||
        evt.source !== iframeRef.current.contentWindow
      ) {
        return;
      }
      const { source } = evt;
      const message = decodeMessage(evt.data);
      if (!message) {
        return;
      }
      let isEventPrevented = false;
      if (onMessage) {
        const preventDefault = evt.preventDefault.bind(evt);
        callHandler(
          "onMessage",
          message,
          Object.assign(evt, {
            preventDefault() {
              preventDefault();
              isEventPrevented = true;
            },
          })
        );
      }

      if (isEventPrevented) {
        return;
      }
      const send = (type: string, payload?: any) => {
        (source as WindowProxy).postMessage(
          encodeMessage(type, payload),
          src.replace(/(\w)\/(.*)/, "$1")
        );
      };
      const { type, payload } = message;
      switch (type) {
        case "ready": {
          send("create", {
            language: "javascript",
            autoFocus,
            ...(options || null),
          });
          instance = {
            focus: () => send("focus"),
            getValue: option =>
              new Promise(resolve => {
                const key = nextValueKey;
                nextValueKey += 1;
                send("get-value", { key, option });
                valueCallbackMap.current.set(key.toString(), resolve);
              }),
            setValue: value => send("set-value", { value }),
            updateOptions: options => send("update-options", { options }),
            sendMessage: send,
          };
          removeTimeoutListener();
          setReady(true);
          callHandler("onReady");
          break;
        }
        case "value": {
          const { key, value } = payload;
          const resolve = valueCallbackMap.current.get(String(key));
          if (resolve) {
            valueCallbackMap.current.delete(String(key));
            resolve(value);
          }
          break;
        }
        case "edit": {
          callHandler("onEdit");
          break;
        }
        case "save": {
          callHandler("onSave");
          break;
        }
        case "error": {
          removeTimeoutListener();
          setError(true);
          callHandler("onError", new Error(payload.message));
          break;
        }
      }
    };

    addTimeoutListener();
    window.addEventListener("message", receive);
    return () => {
      removeTimeoutListener();
      window.removeEventListener("message", receive);
    };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

  function addTimeoutListener() {
    removeTimeoutListener();
    timerRef.current = setTimeout(() => {
      setError(true);
      if (typeof onTimeout === "function") {
        onTimeout();
      }
    }, timeout);
  }

  function removeTimeoutListener() {
    clearTimeout(timerRef.current);
  }

  if (error) {
    return (
      <div
        className={classNames(`${classPrefix}-code-editor`, className)}
        style={mergeStyle({ position: "relative" }, style)}
      >
        <Tips classPrefix={classPrefix}>
          {injectValue(errorPlaceholder)(() => {
            setError(false);
            addTimeoutListener();
          })}
        </Tips>
      </div>
    );
  }

  return (
    <div
      className={classNames(`${classPrefix}-code-editor`, className)}
      style={mergeStyle({ position: "relative" }, style)}
    >
      <iframe
        title="code-editor"
        ref={iframeRef}
        src={src}
        className={`${classPrefix}-code-editor-frame`}
        style={{ width: "100%", height: "100%" }}
        frameBorder="no"
      />
      {!ready && <Tips classPrefix={classPrefix}>{loadingPlaceholder}</Tips>}
    </div>
  );
}

function decodeMessage(message: string) {
  try {
    const { type, payload } = JSON.parse(message);
    if (type) {
      return { type, payload };
    }
  } catch (err) {
    // continue
  }
  return null;
}

function encodeMessage(type: string, payload?: any) {
  return JSON.stringify({ type, payload });
}

function DefaultTipsWrapper({ children, style = {} }) {
  return (
    <div style={{ padding: "20px 0", textAlign: "center", ...style }}>
      {children}
    </div>
  );
}

function Tips({
  children,
  classPrefix,
}: {
  children: React.ReactNode;
  classPrefix: string;
}) {
  return (
    <div
      className={`${classPrefix}-code-editor-loading`}
      style={{
        position: "absolute",
        left: 0,
        top: 0,
        bottom: 0,
        right: 0,
      }}
    >
      {typeof children === "string" ? (
        <DefaultTipsWrapper
          style={{
            padding: 0,
            height: 30,
            lineHeight: 30,
            opacity: 0.5,
          }}
        >
          {children}
        </DefaultTipsWrapper>
      ) : (
        <DefaultTipsWrapper>{children}</DefaultTipsWrapper>
      )}
    </div>
  );
}
