import { Transforms, Node, Editor } from 'slate';
import { MarkdownToken, tokenizeMarkdownLines, TOKEN_TYPE } from './markdown';
import hljs from 'node_modules/highlight.js/lib/common';
import { emojiShortcodeToUnicode } from '../emoji-codes';
declare var twemoji;
export const withMarkdown = (editor) => {
  const {
    onChange,
    onKeydown,
    onError,
    onClick,
    insertTextData,
    insertText,
    insertSoftBreak,
    insertNode,
    insertFragmentData,
    insertFragment,
    deleteBackward,
    deleteCutData,
    deleteForward,
    deleteFragment,
    insertBreak,
    insertData,
    isInline,
    isVoid,
  } = editor;

  let insertInProgress = false;
  editor.insertData = (data) => {
    insertInProgress = true;
    insertData(data);
    reparseEditor();

    insertInProgress = false;
  };

  editor.insertText = (data) => {
    insertText(data);
    if (!insertInProgress) {
      reparseEditor();
    }
  };

  editor.deleteBackward = (unit) => {
    deleteBackward(unit);
    reparseEditor();
  };

  editor.deleteForward = (unit) => {
    deleteForward(unit);
    reparseEditor();
  };

  editor.deleteFragment = (direction) => {
    deleteFragment(direction);
    reparseEditor();
  };

  const reparseEditor = () => {
    //console.log('parseeditor');
    // remember for the selection, later all of the mark operation will move the selection
    let selection = Object.assign({}, editor.selection);
    let textOffset = getTextOffset(selection.anchor); // get the offset as if it was a string
    let lineNumber = selection.anchor.path[0]; // remember for the current line

    let isCodeBlockStarted = false;
    let codeBlockStartRegexp = new RegExp(/^```\S*$/);
    let codeBlockEndRegexp = new RegExp(/^```$/);
    let codeStartPos = 0;
    let codeLanguage = 'none';
    // separate the normal and the codeblocks lines
    for (let i = 0; i < editor.children.length; i++) {
      let childNode = editor.children[i];
      /*if (!isCodeBlockStarted && line.match(codeBlockStartRegexp)) {
          codeStartPos = index + 1;
          codeLanguage = line.slice(3);
          isCodeBlockStarted = true;
          parseMarkdownLines(index);
        } else if (isCodeBlockStarted && line.match(codeBlockEndRegexp)) {
          isCodeBlockStarted = false;
          parseCodeBlocks(codeStartPos, index - 1, codeLanguage);
          parseMarkdownLines(index);
        } else if (!isCodeBlockStarted) {
          parseMarkdownLines(index);
        }*/
      parseMarkdownLines(i);
    }
    /*editor.children.forEach((childNode, index, arr) => {
      let line = childNode.children[0].text;
    });*/

    if (isCodeBlockStarted) {
      parseCodeBlocks(codeStartPos, editor.children.length - 1, codeLanguage);
    }

    // repair the selection
    if (selection && lineNumber < editor.children.length) {
      let realPosition = getRealPositionByOffset(lineNumber, textOffset);
      Transforms.select(editor, {
        offset: realPosition.offset,
        path: [selection.anchor.path[0], realPosition.nodeIndex],
      });
    }
  };

  const parseMarkdownLines = (line: number) => {
    Transforms.setNodes(editor, <any>{ type: 'paragraph' });

    let text = Node.string(Node.get(editor, [line]));

    if (text.length > 0) {
      let tokens = tokenizeMarkdownLines(text);

      if (!isLineEqualsWithMarkdownTokens(line, tokens)) {
        // rebuild the whole line
        // much easier to remove all the nodes and insert it again because of the emojies
        Transforms.insertText(editor, '.', {
          at: [line, editor.children[line].children.length - 1],
        });
        Transforms.select(editor, [line]);
        Transforms.delete(editor);

        let insertingNodes = [];
        tokens.forEach((t) => {
          if (t.tokens.includes(TOKEN_TYPE.EMOJI)) {
            twemoji.parse(t.text, {
              callback: (icon, options) => {
                if (icon) {
                  insertingNodes.push({
                    type: 'emoji',
                    alt: t.text,
                    src: icon,
                    children: [{ text: t.text }],
                    token: [TOKEN_TYPE.EMOJI],
                    unit: true,
                  });
                } else {
                  console.log('can not convert emoji', t.text);
                  insertingNodes.push({ text: t.text });
                }
              },
            });
            // insertingNodes.push({
            //   type: 'emoji',
            //   alt: t.text,
            //   src: twemoji.convert.toCodePoint(t.text),
            //   children: [{ text: t.text }],
            //   token: [TOKEN_TYPE.EMOJI],
            //   unit: true,
            // });
          } else if (t.tokens.includes(TOKEN_TYPE.EMOJI_SHORTCODE)) {
            // replace with an emoji
            let shortcode = t.text.substring(1, t.text.length - 1); // cut the ":"
            let emoji = emojiShortcodeToUnicode[shortcode];
            if (emoji) {
              twemoji.parse(t.text, {
                callback: (icon, options) => {
                  if (icon) {
                    insertingNodes.push({
                      type: 'emoji',
                      alt: emoji,
                      src: icon,
                      children: [{ text: ':' + shortcode + ':' }],
                      token: [TOKEN_TYPE.EMOJI_SHORTCODE],
                      unit: true,
                    });
                  } else {
                    console.warn('can not convert emoji', t.text);
                    insertingNodes.push({ text: t.text });
                  }
                },
              });
              // insertingNodes.push({
              //   type: 'emoji',
              //   alt: emoji,
              //   src: twemoji.convert.toCodePoint(t.text),
              //   children: [{ text: ':' + shortcode + ':' }],
              //   token: [TOKEN_TYPE.EMOJI_SHORTCODE],
              //   unit: true,
              // });
            } else {
              let textNode = { text: t.text };
              if (t.tokens.length > 0) {
                textNode['token'] = t.tokens;
              }
              insertingNodes.push(textNode);
            }
          } else {
            let textNode = { text: t.text };
            if (t.tokens.length > 0) {
              textNode['token'] = t.tokens;
            }
            insertingNodes.push(textNode);
          }
        });

        Transforms.insertNodes(editor, insertingNodes);
      }
    }
  };

  const parseCodeBlocks = (start: number, end: number, lang: string) => {
    for (let i = start; i <= end; i++) {
      Transforms.select(editor, [i]);
      Transforms.setNodes(editor, <any>{ type: CODE_LINE_NODE_TYPE });
      let text = Node.string(Node.get(editor, [i]));
      Transforms.delete(editor);
      Transforms.insertNodes(editor, <any>{ text: text, token: [DEFAULT_TEXT_TOKEN_TYPE] });
    }
  };

  const _parseCodeBlocks = (start: number, end: number, lang: string) => {
    console.log('code', start, end);
    for (let i = start; i <= end; i++) {
      Transforms.select(editor, [i]);
      Transforms.setNodes(editor, <any>{ type: CODE_LINE_NODE_TYPE });
    }

    let str = '';
    for (let i = start; i <= end; i++) {
      str += Node.string(Node.get(editor, [i])) + '\n';
    }

    let hljsResult;
    try {
      hljsResult = hljs.highlight(str, { language: lang, ignoreIllegals: true });
    } catch (e) {
      hljsResult = hljs.highlight(str, { language: 'ini' });
    }

    // hljs store selector nodes in object(kind, children), and directly as a string the texts nodes
    let hljsTokens: ({ kind: string; children: string[] } | string)[] =
      hljsResult._emitter.rootNode.children;

    let tokens = flattenHLJSCodeblockTokens(hljsTokens); // merge the same siblings nodes

    let tokensLineByLine = separateTokensLineByLine(tokens); // split by \n like in the slate tree

    // mark all string in the represented slate tree
    tokensLineByLine.forEach((tokens, line) => {
      // check if changed
      if (
        editor.children[start + line] && // empty new line can do a non valid out of scope ref
        !isTokensMatchWithTheNode(tokens, editor.children[start + line])
      ) {
        console.log('redo', line + start, tokens);

        Transforms.select(editor, [start + line]);
        Transforms.delete(editor);

        let insertingNodes = [];
        tokens.forEach((t) => {
          insertingNodes.push({ text: t.text, token: [t.kind] });
        });
        Transforms.insertNodes(editor, insertingNodes);

        /*
            let part = 0;
            let index = 0;
            let lastKind = 'none'; // null is already used for default
            while (index < tokens.length) {
            let t = tokens[index];
            if (t.text.length > 0) {
              let path = [start + line, part];
              let pos = {
                anchor: { path, offset: 0 },
                focus: { path, offset: t.text.length },
              };
              Transforms.select(editor, pos);
              editor.addMark('token', [t.kind || DEFAULT_TEXT_TOKEN_TYPE]);
              if (t.kind != lastKind) {
                // if the token is eq with the last token
                // slate will merge these nodes, so do not inc the part!
                part++;
                lastKind = t.kind;
              }
            }
            index++;
          }*/
      }
    });
  };

  /**
   * @param tokens
   * @param lineNode must be type: paragraph it is a top level line node
   * @returns
   */
  const isTokensMatchWithTheNode = (
    tokens: HLJSCodeBlockFlattenedTokens[],
    lineNode: { type: 'paragraph'; children: { text: string; token?: string[] }[] }
  ) => {
    if (tokens.length != lineNode.children.length) {
      return false;
    }

    for (let i = 0; i < tokens.length; i++) {
      if (!lineNode.children[i].token || !lineNode.children[i].token.includes(tokens[i].kind)) {
        return false;
      }
    }

    return true;
  };

  /**
   * During insertData (paste) we can only know the "text" offset of the selection
   * After parse it will break and point to an invalid out of node location
   * We have to find the original position
   *
   * From this:
   * "console.log("asd");
   * We will get:
   * 'console', '.', 'log', '(', '"6""', '); '
   * The original 9 offset, which is the "o" from the "log"
   * will shift to the (node: 2, offset: 2) position
   *
   * Moreover we have to figure out from an already parsed node list the new position
   * @param pos
   * @returns
   */
  const getRealPositionByOffset: (
    lineNumber: number,
    offset: number
  ) => {
    nodeIndex: number;
    offset: number;
  } = (lineNumber, offset) => {
    let nodes = editor.children[lineNumber].children;
    let nodeIndex = 0;

    while (
      nodes[nodeIndex] &&
      offset > Node.string(Node.get(editor, [lineNumber, nodeIndex])).length
    ) {
      offset -= Node.string(Node.get(editor, [lineNumber, nodeIndex])).length;
      nodeIndex++;
    }

    if (!nodes[nodeIndex]) {
      return {
        nodeIndex: nodeIndex - 1,
        offset: Node.string(Node.get(editor, [lineNumber, nodeIndex - 1])).length,
      };
    } else {
      return {
        nodeIndex,
        offset,
      };
    }
  };

  /**
   * Handle the node list as a text line and calc the offset by the selection
   * @param pos
   * @returns
   */
  const getTextOffset: (pos: { path: number[]; offset: number }) => number = (pos) => {
    let line = editor.children[pos.path[0]].children;

    let length = 0;
    // calc the string length before the selection
    for (let i = 0; i < pos.path[1]; i++) {
      length += Node.string(Node.get(editor, [pos.path[0], i])).length;
    }

    // return with the text length before the node, and the offset inside the node
    return length + pos.offset;
  };

  /**
   * Needs to merge the same siblings, because slate does that and need to match them later
   * @param hljsResults: ({ kind: string; children: string[] } | string)[]
   */
  const flattenHLJSCodeblockTokens: (hljsResults) => HLJSCodeBlockFlattenedTokens[] = (
    hljsResults
  ) => {
    let result = [];
    hljsResults.forEach((token) => {
      let kind, text;

      if (typeof token == 'string') {
        kind = DEFAULT_TEXT_TOKEN_TYPE;
        text = token;
      } else {
        kind = token.kind.replace(/\./g, '-');
        text = token.children.join('');
      }

      if (result.length > 0 && result[result.length - 1].kind == kind) {
        result[result.length - 1].text = result[result.length - 1].text.concat(text);
      } else {
        result.push({
          kind,
          text,
        });
      }
    });

    return result;
  };

  /**
   * Slate merge the same neighbour tokens, so for the equal check
   * @param tokens
   */
  const isLineEqualsWithMarkdownTokens: (line: number, tokens: MarkdownToken[]) => boolean = (
    line,
    tokens
  ) => {
    let nodes = editor.children[line].children;

    let tokenIndex = 0;
    let nodeIndex = 0;

    while (tokenIndex < tokens.length || nodeIndex < nodes.length) {
      if (
        tokenIndex > 0 &&
        tokenIndex < tokens.length &&
        !tokens[tokenIndex].tokens.includes(TOKEN_TYPE.EMOJI) &&
        isArrayEquals(tokens[tokenIndex].tokens, tokens[tokenIndex - 1].tokens)
      ) {
        tokenIndex++; // slate merges the same text-type neighbours so skip this
      } else if (nodeIndex < nodes.length && nodes[nodeIndex].text == '') {
        nodeIndex++; // slate puts empty string between nodes like emojies
      } else {
        if (
          tokenIndex >= tokens.length ||
          nodeIndex >= nodes.length ||
          !isArrayEquals(tokens[tokenIndex].tokens, nodes[nodeIndex].token || [])
        ) {
          //console.log('false', tokens[tokenIndex].tokens, nodes[nodeIndex].token);
          return false;
        } else {
          tokenIndex++;
          nodeIndex++;
        }
      }
    }
    // end of the tokens
    //console.log('ret false', tokens.length == tokenIndex && nodes.length == nodeIndex);
    //console.log('tokens', tokens, nodes, tokenIndex, nodeIndex);
    return tokens.length == tokenIndex && nodes.length == nodeIndex;
  };

  const separateTokensLineByLine: (
    tokens: HLJSCodeBlockFlattenedTokens[]
  ) => HLJSCodeBlockFlattenedTokens[][] = (tokens) => {
    let result = [[]];
    let currLinePos = 0;

    tokens.forEach((t) => {
      let parts = t.text.split('\n'); // every \n in hljs means a new line in slate

      // this is in the current line
      result[currLinePos].push({
        kind: t.kind,
        text: parts[0],
      });

      // if we can find more elements in the part, it means we have to
      // jump to the next line in slate
      for (let i = 1; i < parts.length; i++) {
        currLinePos++;
        result.push([]);

        if (parts[i].length > 0) {
          // do not put white space other than space
          result[currLinePos].push({
            kind: t.kind,
            text: parts[i],
          });
        }
      }
    });

    return result;
  };

  return editor;
};

const flattenHLJSMarkdownTokens: (hljsResults) => HLJSMarkdownFlattenedTokens[] = (hljsResults) => {
  console.log('flat', hljsResults);
  return _flattenHLJSMarkdownEntryPoint(hljsResults, []);
};

const _flattenHLJSMarkdownEntryPoint: (entry, marks: string[]) => HLJSMarkdownFlattenedTokens[] = (
  entry,
  marks = []
) => {
  console.log('flat in', entry, marks);
  let _marks = [].concat(marks);
  if (entry.kind) {
    _marks.push(entry.kind);
  }

  if (typeof entry == 'string') {
    return [{ text: entry, marks: _marks.length > 0 ? _marks : [DEFAULT_TEXT_TOKEN_TYPE] }];
  }

  let result = [];

  entry.children.forEach((newEntry) => {
    result = result.concat(_flattenHLJSMarkdownEntryPoint(newEntry, _marks));
  });

  return result;
};

export type HLJSCodeBlockFlattenedTokens = { kind: string; text: string };
export type HLJSMarkdownFlattenedTokens = { text: string; tokens: TOKEN_TYPE[] };
export const DEFAULT_TEXT_TOKEN_TYPE = 'def-text';
export const CODE_LINE_NODE_TYPE = 'code-line';

/**
 * Do not check the item orders
 */
function isArrayEquals(arr1: any[], arr2: any[]) {
  if (arr1.length != arr2.length) return false;
  for (let i = 0; i < arr1.length; i++) {
    if (!arr1.includes(arr2[i])) {
      return false;
    }
  }
  return true;
}
