import { getCodeEditorModeConfig } from "./utils";

/**
 * Source: https://raw.githubusercontent.com/codemirror/CodeMirror/master/mode/clike/clike.js
 * @author arijn Haverbeke and others
 * @author Lucas de Souza
 * @param {CodeMirror} CodeMirror 
 */
export function CodeEditorMode (CodeMirror) {
  "use strict";

  function Context(indented, column, type, info, align, prev) {
    this.indented = indented;
    this.column = column;
    this.type = type;
    this.info = info;
    this.align = align;
    this.prev = prev;
  }

  function pushContext(state, col, type, info) {
    let indent = state.indented;
    if (state.context && state.context.type == "statement" && type != "statement")
      indent = state.context.indented;
    return state.context = new Context(indent, col, type, info, null, state.context);
  }
  
  function popContext(state) {
    const t = state.context.type;
    if (t == ")" || t == "]" || t == "}")
      state.indented = state.context.indented;
    return state.context = state.context.prev;
  }

  function typeBefore(stream, state, pos) {
    if (state.prevToken == "variable" || state.prevToken == "type") return true;
    if (/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(stream.string.slice(0, pos))) return true;
    if (state.typeAtEndOfLine && stream.column() == stream.indentation()) return true;
  }

  function isTopScope(context) {
    for (; ;) {
      if (!context || context.type == "top") return true;
      if (context.type == "}" && context.prev.info != "namespace") return false;
      context = context.prev;
    }
  }

  function contains(words, word) {
    if (typeof words === "function") {
      return words(word);
    } else {
      return Object.propertyIsEnumerable.call(words, word);
    }
  }

  function tokenComment(stream, state) {
    let maybeEnd = false, ch;
    while ((ch = stream.next())) {
      if (ch == "/" && maybeEnd) {
        state.tokenize = null;
        break;
      }
      maybeEnd = (ch == "*");
    }
    return "comment";
  }

  CodeMirror.defineMode("ivprog", function (config, parserConfig) {
    const indentUnit = config.indentUnit,
      statementIndentUnit = parserConfig.statementIndentUnit || indentUnit,
      dontAlignCalls = parserConfig.dontAlignCalls,
      keywords = parserConfig.keywords || {},
      switchKeyword = parserConfig.switchKeyword,
      caseKeyword = parserConfig.caseKeyword,
      defaultKeyword = parserConfig.defaultKeyword,
      caseRegex = new RegExp(`^\\s*(?:${caseKeyword} .*?:|${defaultKeyword}:|\\{\\}?|\\})$`),////,
      types = parserConfig.types || {},
      builtin = parserConfig.builtin || {},
      blockKeywords = parserConfig.blockKeywords || {},
      defKeywords = parserConfig.defKeywords || {},
      atoms = parserConfig.atoms || {},
      hooks = parserConfig.hooks || {},
      multiLineStrings = parserConfig.multiLineStrings,
      indentStatements = false,
      namespaceSeparator = null,
      isPunctuationChar = /[[\]{}(),;:\n]/,
      numberStart = /[\d.]/,
      number = /^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)$/i,
      isOperatorChar = /[+\-*%=<>!/&]/,
      isIdentifierChar = /[a-zA-Z0-9_]/,
      // An optional function that takes a {string} token and returns true if it
      // should be treated as a builtin.
      isReservedIdentifier = parserConfig.isReservedIdentifier || false;

    let curPunc, isDefKeyword;
    let tokenString = function () { /*SKIP*/};
    function tokenBase(stream, state) {
      const ch = stream.next();
      if (hooks[ch]) {
        const result = hooks[ch](stream, state);
        if (result !== false) return result;
      }
      if (ch == '"') {
        state.tokenize = tokenString(ch);
        return state.tokenize(stream, state);
      }
      if (isPunctuationChar.test(ch)) {
        curPunc = ch;
        return null;
      }
      if (numberStart.test(ch)) {
        stream.backUp(1)
        if (stream.match(number)) return "number"
        stream.next()
      }
      if (ch == "/") {
        if (stream.eat("*")) {
          state.tokenize = tokenComment;
          return tokenComment(stream, state);
        }
        if (stream.eat("/")) {
          stream.skipToEnd();
          return "comment";
        }
      }
      if (isOperatorChar.test(ch)) {
        while (!stream.match(/^\/[/*]/, false) && stream.eat(isOperatorChar)) { /* SKIP */}
        return "operator";
      }
      stream.eatWhile(isIdentifierChar);
      if (namespaceSeparator) while (stream.match(namespaceSeparator))
        stream.eatWhile(isIdentifierChar);

      const cur = stream.current();
      if (contains(keywords, cur)) {
        if (contains(blockKeywords, cur)) curPunc = "newstatement";
        if (contains(defKeywords, cur)) isDefKeyword = true;
        return "keyword";
      }
      if (contains(types, cur)) return "type";
      if (contains(builtin, cur)
        || (isReservedIdentifier && isReservedIdentifier(cur))) {
        if (contains(blockKeywords, cur)) curPunc = "newstatement";
        return "builtin";
      }
      if (contains(atoms, cur)) return "atom";
      return "variable";
    }

     tokenString = function (quote) {
      return function (stream, state) {
        let escaped = false, next, end = false;
        while ((next = stream.next()) != null) {
          if (next == quote && !escaped) { end = true; break; }
          escaped = !escaped && next == "\\";
        }
        if (end || !(escaped || multiLineStrings))
          state.tokenize = null;
        return "string";
      };
    }

    function maybeEOL(stream, state) {
      if (parserConfig.typeFirstDefinitions && stream.eol() && isTopScope(state.context))
        state.typeAtEndOfLine = typeBefore(stream, state, stream.pos)
    }

    // Interface

    return {
      startState: function (basecolumn) {
        return {
          tokenize: null,
          context: new Context((basecolumn || 0) - indentUnit, 0, "top", null, false),
          indented: 0,
          startOfLine: true,
          prevToken: null
        };
      },

      token: function (stream, state) {
        let ctx = state.context;
        if (stream.sol()) {
          if (ctx.align == null) ctx.align = false;
          state.indented = stream.indentation();
          state.startOfLine = true;
        }
        if (stream.eatSpace()) { maybeEOL(stream, state); return null; }
        curPunc = isDefKeyword = null;
        let style = (state.tokenize || tokenBase)(stream, state);
        if (style == "comment" || style == "meta") return style;
        if (ctx.align == null) ctx.align = true;

        if (curPunc == ";" || curPunc == ":" || (curPunc == "," && stream.match(/^\s*(?:\/\/.*)?$/, false)))
          while (state.context.type == "statement") popContext(state);
        else if (curPunc == "{") pushContext(state, stream.column(), "}");
        else if (curPunc == "[") pushContext(state, stream.column(), "]");
        else if (curPunc == "(") pushContext(state, stream.column(), ")");
        else if (curPunc == "}") {
          while (ctx.type == "statement") ctx = popContext(state);
          if (ctx.type == "}") ctx = popContext(state);
          while (ctx.type == "statement") ctx = popContext(state);
        }
        else if (curPunc == ctx.type) popContext(state);
        else if (indentStatements &&
          (((ctx.type == "}" || ctx.type == "top") && curPunc != ";")  ||
            (ctx.type == "statement" && curPunc == "newstatement"))) {
          pushContext(state, stream.column(), "statement", stream.current());
        }

        if (style == "variable" &&
          ((state.prevToken == "def" ||
            (parserConfig.typeFirstDefinitions && typeBefore(stream, state, stream.start) &&
              isTopScope(state.context) && stream.match(/^\s*\(/, false)))))
          style = "def";

        if (hooks.token) {
          const result = hooks.token(stream, state, style);
          if (result !== undefined) style = result;
        }

        if (style == "def" && parserConfig.styleDefs === false) style = "variable";

        state.startOfLine = false;
        state.prevToken = isDefKeyword ? "def" : style || curPunc;
        maybeEOL(stream, state);
        return style;
      },

      indent: function (state, textAfter) {
        if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass;
        let ctx = state.context;
        const firstChar = textAfter && textAfter.charAt(0)
        const closing = firstChar == ctx.type;
        if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
        if (parserConfig.dontIndentStatements)
          while (ctx.type == "statement" && parserConfig.dontIndentStatements.test(ctx.info))
            ctx = ctx.prev
        if (hooks.indent) {
          const hook = hooks.indent(state, ctx, textAfter, indentUnit);
          if (typeof hook == "number") return hook
        }
        const switchBlock = ctx.prev && ctx.prev.info == switchKeyword;
        if (parserConfig.allmanIndentation && /[{(]/.test(firstChar)) {
          while (ctx.type != "top" && ctx.type != "}") ctx = ctx.prev
          return ctx.indented
        }
        if (ctx.type == "statement")
          return ctx.indented + (firstChar == "{" ? 0 : statementIndentUnit);
        if (ctx.align && (!dontAlignCalls || ctx.type != ")"))
          return ctx.column + (closing ? 0 : 1);
        if (ctx.type == ")" && !closing)
          return ctx.indented + statementIndentUnit;
        const caseTestRegex = new RegExp(`^(?:${caseKeyword}|${defaultKeyword})\b`)
        return ctx.indented + (closing ? 0 : indentUnit) +
          (!closing && switchBlock && !caseTestRegex.test(textAfter) ? indentUnit : 0);
      },

      electricInput: caseRegex,
      blockCommentStart: "/*",
      blockCommentEnd: "*/",
      blockCommentContinue: " * ",
      lineComment: "//",
      fold: "brace"
    };
  });

  function words(str) {
    const obj = {}, words = str.split(" ");
    for (let i = 0; i < words.length; ++i) obj[words[i]] = true;
    return obj;
  }
  
  const codeConfig = getCodeEditorModeConfig();

  const ivprogKeywords =  codeConfig.keywords.join(" ");

  const basicTypes = words(codeConfig.types.join(" "));

  function ivprogTypes(identifier) {
    return contains(basicTypes, identifier);
  }

  const ivprogBlockKeywords = codeConfig.blocks.join(" ");
  const ivprogDefKeywords = "funcao const";

  function def(mimes, mode) {
    if (typeof mimes == "string") mimes = [mimes];
    const words = [];
    function add(obj) {
      if (obj) {
        for (const prop in obj) if (Object.hasOwnProperty.call(obj, prop)) {
          words.push(prop);
        }
      }
    }
    add(mode.keywords);
    add(mode.types);
    add(mode.builtin);
    add(mode.atoms);
    if (words.length) {
      mode.helperType = mimes[0];
      CodeMirror.registerHelper("hintWords", mimes[0], words);
    }

    for (let i = 0; i < mimes.length; ++i)
      CodeMirror.defineMIME(mimes[i], mode);
  }
  
  def(["text/x-ivprog"], {
    name: "ivprog",
    keywords: words(ivprogKeywords),
    types: ivprogTypes,
    blockKeywords: words(ivprogBlockKeywords),
    defKeywords: words(ivprogDefKeywords),
    typeFirstDefinitions: true,
    atoms: words(codeConfig.atoms.join(" ")),
    switchKeyword: codeConfig.switchString,
    caseKeyword: codeConfig.case_default[0],
    defaultKeyword: codeConfig.case_default[1],
    modeProps: { fold: ["brace"] }
  });

}
