import * as Expressions from "./expressions";
import * as Commands from "./commands";
import * as Parsers from "../typeSystem/parsers";
import { Types } from "../typeSystem/types";
import { ArrayType } from "../typeSystem/array_type";
import { SourceInfo } from "./sourceInfo";
import { convertFromString } from "./operators";
import { SyntaxErrorFactory } from "./error/syntaxErrorFactory";
import { LanguageDefinedFunction } from "../processor/definedFunctions";
import { LanguageService } from "../services/languageService";

export class IVProgParser {
  static createParser (input, fill = true) {
    const lexer = LanguageService.getCurrentLexer();
    const parser = new IVProgParser(input, lexer);
    if (fill) {
      parser.fill();
    }
    return parser;
  }

  // <BEGIN scope consts>
  static get BASE () {
    return 0;
  }
  static get FUNCTION () {
    return 1;
  }
  static get COMMAND () {
    return 2;
  }
  static get BREAKABLE () {
    return 4;
  }
  // </ END scope consts>
  //
  static get EOF_TOKEN () {
    return {
      text: null,
      line: -1,
      col: -1,
      value: null,
      offset: -1,
      lineBreaks: false,
      type: "EOF",
    };
  }

  /**
   * @param {string} input
   * @param {IVProgLexer} ivprogLexer
   **/
  constructor (input, ivprogLexer) {
    this.ivprogLexer = ivprogLexer;
    this.inputStream = input;
    this.lexer = ivprogLexer.lexer;
    this.tokenStream = [];
    this.pos = 0;
    this.ruleNames = ivprogLexer.getRules();
    this.variableTypes = Object.entries(this.ivprogLexer.getTypeKeys()).map(
      ([key, _]) => key
    );
    this.functionTypes = this.variableTypes.concat(this.ruleNames.RK_VOID);

    this.parsingArrayDimension = 0;
    this.scope = [];
    this.langFuncs = this.ivprogLexer.getLangFuncs();
    this.definedFuncsNameList = [];
    this.definedVariablesStack = [];
  }

  fill (stream = null) {
    if (stream) {
      this.tokenStream = stream;
      return;
    }
    this.tokenStream = Array.from(this.lexer.reset(this.inputStream));
    this.tokenStream = this.tokenStream.filter((token) => {
      // Throws an exception in case of invalid syntax
      if (token.type === this.ruleNames.ERROR) {
        let text = token.text;
        const line = token.line;
        const column = token.col;
        throw SyntaxErrorFactory.invalid_syntax(text, line, column);
      }
      // remove all whitespaces token and comments
      return (
        token.type !== this.ruleNames.WHITESPACE &&
        token.type !== this.ruleNames.COMMENTS
      );
    });
  }

  parseTree () {
    return this.parseProgram();
  }

  /**
   * @param {number} index
   * @return {moo.Token}
   * */
  getToken (index = this.pos) {
    // if(index === null)
    //   index = this.pos;
    if (index >= this.tokenStream.length) {
      return IVProgParser.EOF_TOKEN;
    }
    return this.tokenStream[index];
  }

  insideScope (scope) {
    if (this.scope.length <= 0) {
      return IVProgParser.BASE === scope;
    } else {
      return this.scope[this.scope.length - 1] === scope;
    }
  }

  pushScope (scope) {
    this.scope.push(scope);
  }

  pushVariableStack () {
    this.definedVariablesStack.push([]);
  }

  popScope () {
    return this.scope.pop();
  }

  popVariableStack () {
    return this.definedVariablesStack.pop();
  }

  getCurrentVariableStack () {
    return this.definedVariablesStack[this.definedVariablesStack.length - 1];
  }

  isEOF () {
    return this.getToken(this.pos).type === IVProgParser.EOF_TOKEN.type;
  }

  parseProgram () {
    this.consumeNewLines();
    const token = this.getToken();
    let globalVars = [];
    let functions = [];

    if (this.ruleNames.RK_PROGRAM === token.type) {
      this.pos++;
      this.consumeNewLines();
      this.checkOpenCurly();
      this.pos++;
      this.pushVariableStack();
      for (;;) {
        this.consumeNewLines();
        const token = this.getToken();
        if (
          token.type === this.ruleNames.RK_CONST ||
          this.isVariableType(token)
        ) {
          globalVars = globalVars.concat(this.parseGlobalVariables());
        } else if (token.type === this.ruleNames.RK_FUNCTION) {
          this.pushVariableStack();
          functions = functions.concat(this.parseFunction());
          this.popVariableStack();
        } else {
          break;
        }
      }
      this.consumeNewLines();
      this.checkCloseCurly();
      this.pos++;
      this.consumeNewLines();
      if (!this.isEOF()) {
        console.log(this.getToken());
        throw SyntaxErrorFactory.extra_lines();
      }
      this.popVariableStack();
      return { global: globalVars, functions: functions };
    } else {
      throw SyntaxErrorFactory.token_missing_one(
        this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_PROGRAM],
        token
      );
    }
  }

  checkOpenCurly (attempt = false) {
    const token = this.getToken();
    if (this.ruleNames.OPEN_CURLY !== token.type) {
      if (!attempt) throw SyntaxErrorFactory.token_missing_one("{", token);
      else return false;
    }
    return true;
  }

  checkCloseCurly (attempt = false) {
    const token = this.getToken();
    if (this.ruleNames.CLOSE_CURLY !== token.type) {
      if (!attempt) throw SyntaxErrorFactory.token_missing_one("}", token);
      else return false;
    }
    return true;
  }

  /* It checks if the current token at position pos is a ']'.
   * As a check function it doesn't increment pos.
   *
   * @params bool:attempt, indicates that the token is optional. Defaults: false
   *
   * @returns true if the attempt is true and current token is '[',
   *   false is attempt is true and current token is not '['
   **/
  checkOpenBrace (attempt = false) {
    const token = this.getToken();
    if (this.ruleNames.OPEN_BRACE !== token.type) {
      if (!attempt) {
        throw SyntaxErrorFactory.token_missing_one("[", token);
      } else {
        return false;
      }
    }
    return true;
  }

  checkCloseBrace (attempt = false) {
    const token = this.getToken();
    if (this.ruleNames.CLOSE_BRACE !== token.type) {
      if (!attempt) {
        throw SyntaxErrorFactory.token_missing_one("]", token);
      } else {
        return false;
      }
    }
    return true;
  }

  checkOpenParenthesis (attempt = false) {
    const token = this.getToken();
    if (this.ruleNames.OPEN_PARENTHESIS !== token.type) {
      if (!attempt) {
        throw SyntaxErrorFactory.token_missing_one("(", token);
      } else {
        return false;
      }
    }
    return true;
  }

  checkCloseParenthesis (attempt = false) {
    const token = this.getToken();
    if (this.ruleNames.CLOSE_PARENTHESIS !== token.type) {
      if (!attempt) {
        throw SyntaxErrorFactory.token_missing_one(")", token);
      } else {
        return false;
      }
    }
    return true;
  }

  checkEOS (attempt = false) {
    const eosToken = this.getToken();
    if (eosToken.type !== this.ruleNames.EOS) {
      if (!attempt) throw SyntaxErrorFactory.eos_missing(eosToken);
      else return false;
    }
    return true;
  }

  checkFunctionDuplicate (functionID, funcIDToken) {
    const id = functionID === null ? "$main" : functionID;
    const index = this.definedFuncsNameList.indexOf(id);
    if (index !== -1) {
      throw SyntaxErrorFactory.duplicate_function(funcIDToken);
    }
    this.definedFuncsNameList.push(id);
  }
  checkVariableDuplicate (variableID, sourceInfo) {
    const index = this.getCurrentVariableStack().indexOf(variableID);
    if (index !== -1) {
      throw SyntaxErrorFactory.duplicate_variable(sourceInfo);
    }
    this.getCurrentVariableStack().push(variableID);
  }

  consumeForSemiColon () {
    const eosToken = this.getToken();
    if (eosToken.type === this.ruleNames.EOS && eosToken.text.match("^;$")) {
      this.pos++;
      return;
    }
    throw SyntaxErrorFactory.token_missing_one(";", eosToken);
  }

  parseGlobalVariables () {
    const decl = this.parseMaybeConst();
    this.checkEOS();
    this.pos++;
    return decl;
  }

  /*
   * Checks if the next token is PR_CONST. It's only available
   * at global variables declaration level
   * @returns Declararion(const, type, id, initVal?)
   **/
  parseMaybeConst () {
    const constToken = this.getToken();
    if (constToken.type === this.ruleNames.RK_CONST) {
      this.pos++;
      const typeString = this.parseType();
      return this.parseDeclaration(typeString, true);
    } else if (this.isVariableType(constToken)) {
      const typeString = this.parseType();
      return this.parseDeclaration(typeString);
    } else {
      throw SyntaxErrorFactory.token_missing_list(
        [this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_CONST]].concat(
          this.getTypeArray()
        ),
        constToken
      );
    }
  }

  /**
   * Parses a declarion of the form: type --- id --- (= --- EAnd)?
   * @return {Commands.Declartion[]} a list of Declararion(const, type, id, initVal?)
   **/
  parseDeclaration (typeString, isConst = false) {
    let initial = null;
    let dim1 = null;
    let dim2 = null;
    let dimensions = 0;
    const sourceInfo = SourceInfo.createSourceInfo(this.getToken());
    const idString = this.parseID();
    this.checkVariableDuplicate(idString, sourceInfo);
    // Check for array or vector
    // ID[int/IDi?][int/IDj?]
    if (this.checkOpenBrace(true)) {
      this.pos += 1;
      this.consumeNewLines();
      dim1 = this.parseArrayDimension();
      this.consumeNewLines();
      this.checkCloseBrace();
      this.pos += 1;
      dimensions += 1;
      if (this.checkOpenBrace(true)) {
        this.pos += 1;
        this.consumeNewLines();
        dim2 = this.parseArrayDimension();
        this.consumeNewLines();
        this.checkCloseBrace();
        this.pos += 1;
        dimensions += 1;
      }
      return this.parseArrayDeclaration(
        typeString,
        isConst,
        idString,
        sourceInfo,
        dimensions,
        dim1,
        dim2
      );
    } else {
      const assignmentToken = this.getToken();
      if (isConst && assignmentToken.type !== this.ruleNames.ASSIGNMENT) {
        throw SyntaxErrorFactory.const_not_init(sourceInfo);
      }
      if (assignmentToken.type === this.ruleNames.ASSIGNMENT) {
        this.pos++;
        initial = this.parseExpressionOR();
      }
      const declaration = new Commands.Declaration(
        idString,
        typeString,
        initial,
        isConst
      );
      declaration.sourceInfo = sourceInfo;
      const commaToken = this.getToken();
      if (commaToken.type === this.ruleNames.COMMA) {
        this.pos++;
        this.consumeNewLines();
        return [declaration].concat(this.parseDeclaration(typeString, isConst));
      } else {
        return [declaration];
      }
    }
  }

  parseArrayDeclaration (
    typeString,
    isConst,
    idString,
    sourceInfo,
    dimensions,
    dim1,
    dim2
  ) {
    const assignmentToken = this.getToken();
    let n_lines = dim1;
    let n_columns = dim2;
    let initial = null;
    let dim_is_id = false;
    if (
      dim1 instanceof Expressions.VariableLiteral ||
      dim2 instanceof Expressions.VariableLiteral
    ) {
      dim_is_id = true;
      if (dimensions > 1 && (dim1 == null || dim2 == null)) {
        throw SyntaxErrorFactory.invalid_matrix_id_dimension(
          SourceInfo.createSourceInfo(assignmentToken)
        );
      }
    }
    if (isConst && assignmentToken.type !== this.ruleNames.ASSIGNMENT) {
      throw SyntaxErrorFactory.const_not_init(sourceInfo);
    }
    if (assignmentToken.type === this.ruleNames.ASSIGNMENT) {
      if (dim_is_id) {
        if (dimensions == 1) {
          throw SyntaxErrorFactory.invalid_vector_init(
            SourceInfo.createSourceInfo(assignmentToken)
          );
        } else {
          throw SyntaxErrorFactory.invalid_matrix_init(
            SourceInfo.createSourceInfo(assignmentToken)
          );
        }
      }
      this.pos += 1;
      initial = this.parseArrayLiteral(typeString);
    }
    if (initial == null && dim1 == null) {
      if (dimensions > 1) {
        throw SyntaxErrorFactory.cannot_infer_matrix_line(idString, sourceInfo);
      }
      throw SyntaxErrorFactory.cannot_infer_vector_size(idString, sourceInfo);
    }
    if (dimensions > 1) {
      if (initial == null && dim2 == null) {
        throw SyntaxErrorFactory.cannot_infer_matrix_column(
          idString,
          sourceInfo
        );
      }
    }

    if (dimensions === 1 && initial != null && !initial.isVector) {
      const expString = initial.toString();
      throw SyntaxErrorFactory.matrix_to_vector_literal_attr(
        idString,
        expString,
        initial.sourceInfo
      );
    } else if (dimensions > 1 && initial != null && initial.isVector) {
      const expString = initial.toString();
      throw SyntaxErrorFactory.vector_to_matrix_literal_attr(
        idString,
        expString,
        initial.sourceInfo
      );
    }

    if (dim1 == null) {
      n_lines = new Expressions.IntLiteral(Parsers.toInt(initial.lines));
      n_lines.sourceInfo = sourceInfo;
    }

    if (dimensions > 1) {
      if (dim2 == null) {
        n_columns = new Expressions.IntLiteral(Parsers.toInt(initial.columns));
        n_columns.sourceInfo = sourceInfo;
      }
    }
    const declaration = new Commands.ArrayDeclaration(
      idString,
      new ArrayType(typeString, dimensions),
      n_lines,
      n_columns,
      initial,
      isConst
    );
    declaration.sourceInfo = sourceInfo;
    const commaToken = this.getToken();
    if (commaToken.type === this.ruleNames.COMMA) {
      this.pos++;
      this.consumeNewLines();
      return [declaration].concat(this.parseDeclaration(typeString, isConst));
    } else {
      return [declaration];
    }
  }

  consumeNewLines () {
    let token = this.getToken();
    while (
      token &&
      token.type === this.ruleNames.EOS &&
      token.text.match("^[\r\n]+$")
    ) {
      this.pos++;
      token = this.getToken();
    }
  }

  isVariableType (token) {
    return this.variableTypes.find((v) => v === token.type);
  }

  /**
   * Reads the next token of the stream to check if it is a Integer or an ID.
   * @returns Integer | ID
   **/
  parseArrayDimension () {
    const dimToken = this.getToken();
    if (dimToken.type === this.ruleNames.INTEGER) {
      //parse as int literal
      this.pos++;
      return this.getIntLiteral(dimToken);
    } else if (dimToken.type === this.ruleNames.ID) {
      //parse as variable
      this.pos++;
      return this.parseVariable(dimToken);
    } else if (dimToken.type === this.ruleNames.CLOSE_BRACE) {
      return null;
    } else {
      throw SyntaxErrorFactory.invalid_array_dimension(
        this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_INTEGER],
        dimToken
      );
    }
  }

  /**
   * Returns an object {type: 'int', value: value}.
   * It checks for binary and hexadecimal integers.
   * @param {moo.Token} token
   * @return {Object} object with fields type and value
   **/
  getIntLiteral (token) {
    const text = token.text;
    const sourceInfo = SourceInfo.createSourceInfo(token);
    const exp = new Expressions.IntLiteral(Parsers.toInt(text));
    exp.sourceInfo = sourceInfo;
    return exp;
  }

  getRealLiteral (token) {
    const sourceInfo = SourceInfo.createSourceInfo(token);
    const exp = new Expressions.RealLiteral(Parsers.toReal(token.text));
    exp.sourceInfo = sourceInfo;
    return exp;
  }

  getStringLiteral (token) {
    const text = token.text;
    const sourceInfo = SourceInfo.createSourceInfo(token);
    const exp = new Expressions.StringLiteral(Parsers.toString(text));
    exp.sourceInfo = sourceInfo;
    return exp;
  }

  getCharLiteral (token) {
    const text = token.text;
    const exp = new Expressions.CharLiteral(Parsers.toChar(text));
    exp.sourceInfo = SourceInfo.createSourceInfo(token);
    return exp;
  }

  getBoolLiteral (token) {
    const val = Parsers.toBool(token.text);
    const exp = new Expressions.BoolLiteral(val);
    exp.sourceInfo = SourceInfo.createSourceInfo(token);
    return exp;
  }

  parseArrayLiteral (typeString) {
    const openCurly = this.checkOpenCurly(true);
    if (!openCurly) {
      const invalid_token = this.getToken();
      throw SyntaxErrorFactory.array_init_not_literal(
        SourceInfo.createSourceInfo(invalid_token)
      );
    }
    const beginArray = this.getToken();
    if (this.parsingArrayDimension >= 2) {
      throw SyntaxErrorFactory.array_exceeds_2d(
        SourceInfo.createSourceInfo(beginArray)
      );
    }
    this.pos += 1;
    this.parsingArrayDimension += 1;
    this.consumeNewLines();
    let data = null;
    const maybeCurlyOpen = this.checkOpenCurly(true);
    if (maybeCurlyOpen) {
      // This is potentially a list of vectors
      data = this.parseVectorList(typeString);
    } else {
      data = this.parseExpressionList();
    }
    this.consumeNewLines();
    this.checkCloseCurly();
    const endArray = this.getToken();
    this.pos += 1;
    this.parsingArrayDimension -= 1;
    const sourceInfo = SourceInfo.createSourceInfoFromList(
      beginArray,
      endArray
    );
    let dataDim = 1;
    if (data[0] instanceof Expressions.ArrayLiteral) {
      dataDim += 1;
    } else if (data.length == 1) {
      console.log("Talvez uma variável seja uma melhor opção");
    }
    const type = new ArrayType(typeString, dataDim);
    const exp = new Expressions.ArrayLiteral(type, data);
    exp.sourceInfo = sourceInfo;
    return exp;
  }

  /**
   * Returns a list of ArrayLiterals. Helper function for parsing matrices
   */
  parseVectorList (typeString) {
    const list = [];
    let lastSize = null;
    for (;;) {
      this.checkOpenCurly();
      const beginArray = this.getToken();
      if (this.parsingArrayDimension >= 2) {
        throw SyntaxErrorFactory.array_exceeds_2d(
          SourceInfo.createSourceInfo(beginArray)
        );
      }
      this.pos += 1;
      this.parsingArrayDimension += 1;
      this.consumeNewLines();
      const data = this.parseExpressionList();
      this.consumeNewLines();
      this.checkCloseCurly();
      const endArray = this.getToken();
      this.pos += 1;
      this.parsingArrayDimension -= 1;
      const sourceInfo = SourceInfo.createSourceInfoFromList(
        beginArray,
        endArray
      );
      if (lastSize == null) {
        lastSize = data.length;
      } else if (lastSize !== data.length) {
        const expString = this.inputStream.substring(
          beginArray.offset,
          endArray.offset + endArray.text.length
        );
        throw SyntaxErrorFactory.invalid_matrix_literal_line(
          expString,
          sourceInfo
        );
      }
      const type = new ArrayType(typeString, 1);
      const exp = new Expressions.ArrayLiteral(type, data);
      exp.sourceInfo = sourceInfo;
      list.push(exp);
      const commaToken = this.getToken();
      if (commaToken.type !== this.ruleNames.COMMA) {
        break;
      }
      this.pos += 1;
      this.consumeNewLines();
    }
    if (list.length == 1) {
      console.log("Talvez um vetor seja uma melhor opção");
    }
    return list;
  }

  /*
   * Returns an object {type: 'variable', value: value}.
   * @returns object with fields type and value
   **/
  parseVariable (token) {
    const sourceInfo = SourceInfo.createSourceInfo(token);
    const exp = new Expressions.VariableLiteral(token.text);
    exp.sourceInfo = sourceInfo;
    return exp;
  }

  /*
   * Returns an object representing a function. It has
   * four attributes: returnType, id, formalParams and block.
   * The block object has two attributes: declarations and commands
   **/
  parseFunction () {
    this.pushScope(IVProgParser.FUNCTION);
    let formalParams = [];
    const token = this.getToken();
    if (token.type !== this.ruleNames.RK_FUNCTION) {
      //throw SyntaxError.createError(this.lexer.literalNames[this.lexerClass.PR_FUNCAO], token);
      return null;
    }
    this.pos++;
    const funType = this.parseType();
    let dimensions = 0;
    if (this.checkOpenBrace(true)) {
      this.pos++;
      this.checkCloseBrace();
      this.pos++;
      dimensions++;
      if (this.checkOpenBrace(true)) {
        this.pos++;
        this.checkCloseBrace();
        this.pos++;
        dimensions++;
      }
    }
    const funcIDToken = this.getToken();
    const functionID = this.parseID();
    this.checkFunctionDuplicate(functionID, funcIDToken);
    this.checkOpenParenthesis();
    this.pos++;
    this.consumeNewLines();
    if (!this.checkCloseParenthesis(true)) {
      formalParams = this.parseFormalParameters(); // formal parameters
      this.consumeNewLines();
      this.checkCloseParenthesis();
      this.pos++;
    } else {
      this.pos++;
    }
    this.consumeNewLines();
    const commandsBlock = this.parseCommandBlock();
    let returnType = funType;
    if (dimensions > 0) {
      returnType = new ArrayType(funType, dimensions);
    }
    const func = new Commands.Function(
      functionID,
      returnType,
      formalParams,
      commandsBlock
    );
    if (functionID === null && !func.isMain) {
      throw SyntaxErrorFactory.invalid_main_return(
        LanguageDefinedFunction.getMainFunctionName(),
        this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_VOID],
        token.line
      );
    } else if (func.isMain && formalParams.length !== 0) {
      throw SyntaxErrorFactory.main_parameters();
    }
    this.popScope();
    func.sourceInfo = SourceInfo.createSourceInfo(funcIDToken);
    return func;
  }

  /*
   * Parse the formal parameters of a function.
   * @returns a list of objects with the following attributes: type, id and dimensions.
   **/
  parseFormalParameters () {
    const list = [];
    for (;;) {
      let dimensions = 0;
      let reference = false;
      const typeString = this.parseType();
      let maybeIDToken = this.getToken();
      if (maybeIDToken.type === this.ruleNames.RK_REFERENCE) {
        reference = true;
        this.pos += 1;
        maybeIDToken = this.getToken();
      }
      const idString = this.parseID();
      this.checkVariableDuplicate(idString, maybeIDToken);
      if (this.checkOpenBrace(true)) {
        this.pos += 1;
        dimensions += 1;
        this.checkCloseBrace();
        this.pos += 1;
        if (this.checkOpenBrace(true)) {
          this.pos += 1;
          dimensions += 1;
          this.checkCloseBrace();
          this.pos += 1;
        }
      }
      let type = null;
      if (dimensions > 0) {
        type = new ArrayType(typeString, dimensions);
      } else {
        type = typeString;
      }
      const parameter = new Commands.FormalParameter(type, idString, reference);
      parameter.sourceInfo = SourceInfo.createSourceInfo(maybeIDToken);
      list.push(parameter);
      const commaToken = this.getToken();
      if (commaToken.type !== this.ruleNames.COMMA) break;
      this.pos++;
      this.consumeNewLines();
    }
    return list;
  }

  parseID () {
    const token = this.getToken();
    if (token.type !== this.ruleNames.ID) {
      throw SyntaxErrorFactory.id_missing(token);
    }
    this.pos++;
    if (this.insideScope(IVProgParser.FUNCTION)) {
      if (token.text === LanguageDefinedFunction.getMainFunctionName()) {
        return null;
      }
    }
    return token.text;
  }

  /**
   * @return {string}
   **/
  parseMaybeLibID () {
    const token = this.getToken();
    if (token.type !== this.ruleNames.ID) {
      throw SyntaxErrorFactory.id_missing(token);
    }
    const maybeDOT = this.getToken(this.pos + 1);
    if (maybeDOT.type === this.ruleNames.DOT) {
      this.pos += 2;
      const anotherID = this.getToken();
      if (anotherID.type !== this.ruleNames.ID) {
        throw SyntaxErrorFactory.id_missing(anotherID);
      }
      this.pos++;
      return `${token.text}.${anotherID.text}`;
    }
    this.pos++;
    return token.text;
  }

  parseType () {
    const token = this.getToken();
    if (
      token.type === this.ruleNames.ID &&
      this.insideScope(IVProgParser.FUNCTION)
    ) {
      return Types.VOID;
    } else if (
      token.type === this.ruleNames.RK_VOID &&
      this.insideScope(IVProgParser.FUNCTION)
    ) {
      this.pos++;
      return Types.VOID;
    } else if (this.isVariableType(token)) {
      this.pos++;
      switch (token.type) {
        case this.ruleNames.RK_INTEGER:
          return Types.INTEGER;
        case this.ruleNames.RK_BOOLEAN:
          return Types.BOOLEAN;
        case this.ruleNames.RK_REAL:
          return Types.REAL;
        case this.ruleNames.RK_STRING:
          return Types.STRING;
        case this.ruleNames.RK_CHARACTER:
          return Types.CHAR;
        default:
          break;
      }
    }

    throw SyntaxErrorFactory.invalid_type(this.getTypeArray(), token);
  }

  parseCommandBlock (optionalCurly = false) {
    let variablesDecl = [];
    const commands = [];
    let hasOpen = false;
    if (this.checkOpenCurly(optionalCurly)) {
      this.pos++;
      hasOpen = true;
    }
    this.consumeNewLines();
    let parsedCommand = false;
    for (;;) {
      const cmd = this.parseCommand();
      if (cmd === null) break;
      if (cmd !== -1) {
        if (cmd instanceof Array) {
          if (parsedCommand) {
            const lastToken = this.getToken(this.pos - 1);
            throw SyntaxErrorFactory.invalid_var_declaration(lastToken);
          }
          variablesDecl = variablesDecl.concat(cmd);
        } else {
          parsedCommand = true;
          commands.push(cmd);
        }
      }
    }
    this.consumeNewLines();
    if (hasOpen) {
      this.checkCloseCurly();
      this.pos++;
      this.consumeNewLines();
    }
    return new Commands.CommandBlock(variablesDecl, commands);
  }

  parseCommand () {
    const token = this.getToken();
    if (this.isVariableType(token)) {
      if (!this.insideScope(IVProgParser.FUNCTION)) {
        throw SyntaxErrorFactory.invalid_var_declaration(token);
      }
      this.pushScope(IVProgParser.BASE);
      const varType = this.parseType();
      this.popScope();
      const cmd = this.parseDeclaration(varType);
      this.checkEOS();
      this.pos++;
      return cmd;
    } else if (token.type === this.ruleNames.ID) {
      return this.parseIDCommand();
    } else if (token.type === this.ruleNames.DOT) {
      // TODO Check if this is relevant since DOT is a replacement for antlr4 LIB_ID :=> ID . ID
      throw SyntaxErrorFactory.invalid_syntax(
        token.text,
        token.line,
        token.col
      );
    } else if (token.type === this.ruleNames.RK_RETURN) {
      return this.parseReturn();
    } else if (
      token.type === this.ruleNames.RK_WHILE ||
      token.type === this.ruleNames.RK_WHILE_ALT
    ) {
      return this.parseWhile();
    } else if (
      token.type === this.ruleNames.RK_FOR ||
      token.type === this.ruleNames.RK_FOR_ALT
    ) {
      return this.parseFor();
    } else if (token.type === this.ruleNames.RK_BREAK) {
      if (!this.insideScope(IVProgParser.BREAKABLE)) {
        throw SyntaxErrorFactory.invalid_break_command(
          this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_BREAK],
          token
        );
      }
      return this.parseBreak();
    } else if (token.type === this.ruleNames.RK_SWITCH) {
      return this.parseSwitchCase();
    } else if (token.type === this.ruleNames.RK_DO) {
      return this.parseRepeatUntil();
    } else if (token.type === this.ruleNames.RK_IF) {
      return this.parseIfThenElse();
    } else if (this.checkEOS(true)) {
      this.pos++;
      return -1;
    } else {
      return null;
    }
  }

  parseSwitchCase () {
    const token = this.getToken();
    this.pushScope(IVProgParser.BREAKABLE);
    this.pos++;
    this.checkOpenParenthesis();
    this.pos++;
    this.consumeNewLines();
    const exp = this.parseExpressionOR();
    this.consumeNewLines();
    this.checkCloseParenthesis();
    this.pos++;
    this.consumeNewLines();
    this.checkOpenCurly();
    this.pos++;
    this.consumeNewLines();
    const casesList = this.parseCases();
    this.consumeNewLines();
    this.checkCloseCurly();
    this.pos++;
    this.consumeNewLines();

    this.popScope();
    const command = new Commands.Switch(exp, casesList);
    command.sourceInfo = SourceInfo.createSourceInfo(token);
    return command;
  }

  parseRepeatUntil () {
    const token = this.getToken();
    this.pos++;
    this.consumeNewLines();
    this.pushScope(IVProgParser.BREAKABLE);
    const commandsBlock = this.parseCommandBlock();
    this.consumeNewLines(); //Maybe not...
    const whileToken = this.getToken();
    if (whileToken.type !== this.ruleNames.RK_DO_UNTIL) {
      throw SyntaxErrorFactory.token_missing_one(
        this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_DO_UNTIL],
        whileToken
      );
    }
    this.pos++;
    this.checkOpenParenthesis();
    this.pos++;
    this.consumeNewLines();
    const condition = this.parseExpressionOR();
    this.consumeNewLines();
    this.checkCloseParenthesis();
    this.pos++;
    this.checkEOS();
    this.popScope();
    const command = new Commands.RepeatUntil(condition, commandsBlock);
    command.sourceInfo = SourceInfo.createSourceInfo(token);
    return command;
  }

  parseIfThenElse () {
    if (this.insideScope(IVProgParser.BREAKABLE)) {
      this.pushScope(IVProgParser.BREAKABLE);
    } else {
      this.pushScope(IVProgParser.COMMAND);
    }
    const token = this.getToken();
    this.pos++;
    this.checkOpenParenthesis();
    this.pos++;
    this.consumeNewLines();
    const logicalExpression = this.parseExpressionOR();
    this.consumeNewLines();
    this.checkCloseParenthesis();
    this.pos++;
    this.consumeNewLines();
    const cmdBlocks = this.parseCommandBlock();

    const maybeElse = this.getToken();
    if (maybeElse.type === this.ruleNames.RK_ELSE) {
      this.pos++;
      this.consumeNewLines();
      const maybeIf = this.getToken();
      let elseBlock = null;
      if (this.checkOpenCurly(true)) {
        elseBlock = this.parseCommandBlock();
      } else if (maybeIf.type === this.ruleNames.RK_IF) {
        elseBlock = this.parseIfThenElse();
      } else {
        throw SyntaxErrorFactory.token_missing_list(
          [this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_IF], "{"],
          maybeIf
        );
      }
      this.popScope();
      const cmd = new Commands.IfThenElse(
        logicalExpression,
        cmdBlocks,
        elseBlock
      );
      cmd.sourceInfo = SourceInfo.createSourceInfo(token);
      return cmd;
    }
    this.popScope();

    const cmd = new Commands.IfThenElse(logicalExpression, cmdBlocks, null);
    cmd.sourceInfo = SourceInfo.createSourceInfo(token);
    return cmd;
  }

  parseFor () {
    this.pushScope(IVProgParser.BREAKABLE);
    const for_token = this.getToken();
    this.pos += 1;
    // parse ID
    const id_token = this.getToken();
    const id = this.parseID();
    const for_id = new Expressions.VariableLiteral(id);
    for_id.sourceInfo = SourceInfo.createSourceInfo(id_token);
    // END parse ID
    const for_from = this.parseForParameters(this.ruleNames.RK_FOR_FROM);
    const for_to = this.parseForParameters(this.ruleNames.RK_FOR_TO);
    const maybePass = this.parseForParameters(this.ruleNames.RK_FOR_PASS);
    this.consumeNewLines();
    const commandsBlock = this.parseCommandBlock();
    this.popScope();
    const cmd = new Commands.For(
      for_id,
      for_from,
      for_to,
      maybePass,
      commandsBlock
    );
    cmd.sourceInfo = SourceInfo.createSourceInfo(for_token);
    return cmd;
  }

  parseWhile () {
    this.pushScope(IVProgParser.BREAKABLE);
    const token = this.getToken();
    this.pos++;
    this.checkOpenParenthesis();
    this.pos++;
    this.consumeNewLines();
    const logicalExpression = this.parseExpressionOR();
    this.consumeNewLines();
    this.checkCloseParenthesis();
    this.pos++;
    this.consumeNewLines();
    const cmdBlocks = this.parseCommandBlock();
    this.popScope();
    const cmd = new Commands.While(logicalExpression, cmdBlocks);
    cmd.sourceInfo = SourceInfo.createSourceInfo(token);
    return cmd;
  }

  parseBreak () {
    const token = this.getToken();
    this.pos++;
    this.checkEOS();
    this.pos++;
    const command = new Commands.Break();
    command.sourceInfo = SourceInfo.createSourceInfo(token);
    return command;
  }

  parseReturn () {
    const token = this.getToken();
    this.pos++;
    let exp = null;
    if (!this.checkEOS(true)) {
      exp = this.parseExpressionOR();
      this.checkEOS();
    }
    this.pos++;
    const returnCommand = new Commands.Return(exp);
    returnCommand.sourceInfo = SourceInfo.createSourceInfo(token);
    return returnCommand;
  }

  parseIDCommand () {
    const refToken = this.getToken();
    const id = this.parseMaybeLibID();
    const isID = id.indexOf(".") === -1;
    if (this.checkOpenBrace(true)) {
      this.pos++;
      let lineExpression = null;
      let columnExpression = null;
      this.consumeNewLines();
      lineExpression = this.parseExpression();
      this.consumeNewLines();
      this.checkCloseBrace();
      this.pos++;
      if (this.checkOpenBrace(true)) {
        this.pos++;
        this.consumeNewLines();
        columnExpression = this.parseExpression();
        this.consumeNewLines();
        this.checkCloseBrace();
        this.pos++;
      }
      const assignmentToken = this.getToken();
      if (assignmentToken.type !== this.ruleNames.ASSIGNMENT) {
        // TODO BETTER MESSAGE
        throw SyntaxErrorFactory.token_missing_one("<-", assignmentToken);
      }
      this.pos++;
      const exp = this.parseExpressionOR();
      this.checkEOS();
      this.pos++;
      const cmd = new Commands.ArrayIndexAssign(
        id,
        lineExpression,
        columnExpression,
        exp
      );
      cmd.sourceInfo = SourceInfo.createSourceInfo(assignmentToken);
      return cmd;
    }
    const assignmentOrParenthesis = this.getToken();
    if (isID && assignmentOrParenthesis.type === this.ruleNames.ASSIGNMENT) {
      this.pos++;
      const exp = this.parseExpressionOR();
      this.checkEOS();
      this.pos++;
      const cmd = new Commands.Assign(id, exp);
      cmd.sourceInfo = SourceInfo.createSourceInfo(assignmentOrParenthesis);
      return cmd;
    } else if (
      assignmentOrParenthesis.type === this.ruleNames.OPEN_PARENTHESIS
    ) {
      const funcCall = this.parseFunctionCallCommand(id);
      this.checkEOS();
      this.pos++;
      return funcCall;
    } else if (isID) {
      throw SyntaxErrorFactory.token_missing_list(
        ["<-", "("],
        assignmentOrParenthesis
      );
    } else {
      throw SyntaxErrorFactory.invalid_id_format(refToken);
    }
  }

  parseForParameters (keyword_code) {
    if (keyword_code === this.ruleNames.RK_FOR_PASS) {
      if (this.checkOpenCurly(true)) {
        return null;
      }
    }
    const from_token = this.getToken();
    if (from_token.type !== keyword_code) {
      // TODO better error message
      const keyword = this.ivprogLexer.getReservedKeys()[keyword_code];
      throw new Error(
        "Error de sintaxe no comando repita_para: esperava-se " +
          keyword +
          " mas encontrou " +
          from_token.text
      );
    }
    this.pos += 1;
    let int_or_id = this.getToken();
    let is_unary_op = false;
    let op = null;
    if (int_or_id.type === this.ruleNames.SUM_OP) {
      is_unary_op = true;
      op = int_or_id.text;
      this.pos += 1;
      int_or_id = this.getToken();
    }
    let for_from = null;
    if (int_or_id.type === this.ruleNames.ID) {
      for_from = new Expressions.VariableLiteral(this.parseID());
      for_from.sourceInfo = SourceInfo.createSourceInfo(int_or_id);
    } else if (int_or_id.type === this.ruleNames.INTEGER) {
      this.pos += 1;
      for_from = this.getIntLiteral(int_or_id);
    }
    if (for_from == null) {
      // TODO better error message
      const keyword = this.ivprogLexer.getReservedKeys()[keyword_code];
      throw new Error(
        "Error de sintaxe no comando repeita_para: " +
          int_or_id.text +
          " não é compativel com o esperado para o paramentro " +
          keyword +
          ". O valor deve ser um inteiro ou variável."
      );
    }
    if (is_unary_op) {
      for_from = new Expressions.UnaryApp(convertFromString(op), for_from);
    }
    return for_from;
  }

  parseCases () {
    const token = this.getToken();
    if (token.type !== this.ruleNames.RK_CASE) {
      throw SyntaxErrorFactory.token_missing_one(
        this.ivprogLexer.getReservedKeys()[this.ruleNames.RK_CASE],
        token
      );
    }
    this.pos++;
    const nextToken = this.getToken();
    if (nextToken.type === this.ruleNames.RK_DEFAULT) {
      this.pos++;
      const colonToken = this.getToken();
      if (colonToken.type !== this.ruleNames.COLON) {
        throw SyntaxErrorFactory.token_missing_one(":", colonToken);
      }
      this.pos++;
      this.consumeNewLines();
      const block = this.parseCommandBlock(true);
      const defaultCase = new Commands.Case(null);
      defaultCase.sourceInfo = SourceInfo.createSourceInfo(token);
      defaultCase.setCommands(block.commands);
      return [defaultCase];
    } else {
      const exp = this.parseExpressionOR();
      const colonToken = this.getToken();
      if (colonToken.type !== this.ruleNames.COLON) {
        throw SyntaxErrorFactory.token_missing_one(":", colonToken);
      }
      this.pos++;
      this.consumeNewLines();
      const block = this.parseCommandBlock(true);
      const aCase = new Commands.Case(exp);
      aCase.sourceInfo = SourceInfo.createSourceInfo(token);
      aCase.setCommands(block.commands);
      const caseToken = this.getToken();
      if (caseToken.type === this.ruleNames.RK_CASE) {
        return [aCase].concat(this.parseCases());
      } else {
        return [aCase];
      }
    }
  }

  /*
   * Parses an Expression following the structure:
   *
   * EOR  => EAnd ( 'or' EOR)? #expression and
   *
   * EAnd   => ENot ('and' EAnd)? #expression or
   *
   * ENot  => 'not'? ER #expression not
   *
   * ER    => E ((>=, <=, ==, >, <) ER)? #expression relational
   *
   * E     => factor ((+, -) E)? #expression
   *
   * factor=> term ((*, /, %) factor)?
   *
   * term  => literal || arrayAccess || FuncCall || ID || '('EOR')'
   **/
  parseExpressionOR () {
    let exp1 = this.parseExpressionAND();
    while (this.getToken().type === this.ruleNames.RK_LOGICAL_OR) {
      const opToken = this.getToken();
      this.pos++;
      const or = convertFromString("or");
      this.consumeNewLines();
      const exp2 = this.parseExpressionAND();
      const finalExp = new Expressions.InfixApp(or, exp1, exp2);
      finalExp.sourceInfo = SourceInfo.createSourceInfo(opToken);
      exp1 = finalExp;
    }
    return exp1;
  }

  parseExpressionAND () {
    let exp1 = this.parseExpressionNot();
    while (this.getToken().type === this.ruleNames.RK_LOGICAL_AND) {
      const opToken = this.getToken();
      this.pos++;
      const and = convertFromString("and");
      this.consumeNewLines();
      const exp2 = this.parseExpressionNot();
      const finalExp = new Expressions.InfixApp(and, exp1, exp2);
      finalExp.sourceInfo = SourceInfo.createSourceInfo(opToken);
      exp1 = finalExp;
    }
    return exp1;
  }

  parseExpressionNot () {
    const maybeNotToken = this.getToken();
    if (maybeNotToken.type === this.ruleNames.RK_LOGICAL_NOT) {
      const opToken = this.getToken();
      this.pos++;
      const not = convertFromString("not");
      const exp1 = this.parseExpressionRel();
      const finalExp = new Expressions.UnaryApp(not, exp1);
      finalExp.sourceInfo = SourceInfo.createSourceInfo(opToken);
      return finalExp;
    } else {
      return this.parseExpressionRel();
    }
  }

  parseExpressionRel () {
    let exp1 = this.parseExpression();
    while (this.getToken().type === this.ruleNames.RELATIONAL_OPERATOR) {
      const relToken = this.getToken();
      this.pos++;
      const rel = convertFromString(relToken.text);
      const exp2 = this.parseExpression();
      const finalExp = new Expressions.InfixApp(rel, exp1, exp2);
      finalExp.sourceInfo = SourceInfo.createSourceInfo(relToken);
      exp1 = finalExp;
    }
    return exp1;
  }

  parseExpression () {
    let factor = this.parseFactor();
    while (this.getToken().type === this.ruleNames.SUM_OP) {
      const sumOpToken = this.getToken();
      this.pos++;
      const op = convertFromString(sumOpToken.text);
      const factor2 = this.parseFactor();
      const finalExp = new Expressions.InfixApp(op, factor, factor2);
      finalExp.sourceInfo = SourceInfo.createSourceInfo(sumOpToken);
      factor = finalExp;
    }
    return factor;
  }

  parseFactor () {
    let term = this.parseTerm();
    while (this.getToken().type === this.ruleNames.MULTI_OP) {
      const multOpToken = this.getToken();
      this.pos++;
      const op = convertFromString(multOpToken.text);
      const term2 = this.parseTerm();
      const finalExp = new Expressions.InfixApp(op, term, term2);
      finalExp.sourceInfo = SourceInfo.createSourceInfo(multOpToken);
      term = finalExp;
    }
    return term;
  }

  parseTerm () {
    const token = this.getToken();
    let sourceInfo = null;
    let exp = null;
    switch (token.type) {
      case this.ruleNames.SUM_OP:
        this.pos++;
        sourceInfo = SourceInfo.createSourceInfo(token);
        exp = new Expressions.UnaryApp(
          convertFromString(token.text),
          this.parseTerm()
        );
        exp.sourceInfo = sourceInfo;
        return exp;
      case this.ruleNames.INTEGER:
        this.pos++;
        return this.getIntLiteral(token);
      case this.ruleNames.REAL:
        this.pos++;
        return this.getRealLiteral(token);
      case this.ruleNames.STRING:
        this.pos++;
        return this.getStringLiteral(token);
      case this.ruleNames.CHARACTER:
        this.pos++;
        return this.getCharLiteral(token);
      case this.ruleNames.RK_TRUE:
      case this.ruleNames.RK_FALSE:
        this.pos++;
        return this.getBoolLiteral(token);
      case this.ruleNames.OPEN_CURLY:
        // No more annonymous array
        // return this.parseArrayLiteral();
        throw SyntaxErrorFactory.annonymous_array_literal(token);
      case this.ruleNames.ID:
        return this.parseIDTerm();
      case this.ruleNames.OPEN_PARENTHESIS:
        return this.parseParenthesisExp();
      default:
        throw SyntaxErrorFactory.invalid_terminal(token);
    }
  }

  parseIDTerm () {
    const tokenA = this.getToken();
    const id = this.parseMaybeLibID();
    const isID = id.indexOf(".") === -1;
    if (isID && this.checkOpenBrace(true)) {
      let tokenB = null;
      this.pos++;
      const firstIndex = this.parseExpression();
      let secondIndex = null;
      this.consumeNewLines();
      this.checkCloseBrace();
      tokenB = this.getToken();
      this.pos++;
      if (this.checkOpenBrace(true)) {
        this.pos++;
        secondIndex = this.parseExpression();
        this.consumeNewLines();
        this.checkCloseBrace();
        tokenB = this.getToken();
        this.pos++;
      }
      const sourceInfo = SourceInfo.createSourceInfoFromList(tokenA, tokenB);
      const exp = new Expressions.ArrayAccess(id, firstIndex, secondIndex);
      exp.sourceInfo = sourceInfo;
      return exp;
    } else if (this.checkOpenParenthesis(true)) {
      return this.parseFunctionCallExpression(id);
    } else if (isID) {
      const sourceInfo = SourceInfo.createSourceInfo(tokenA);
      const exp = new Expressions.VariableLiteral(id);
      exp.sourceInfo = sourceInfo;
      return exp;
    } else {
      throw SyntaxErrorFactory.invalid_id_format(tokenA);
    }
  }

  getFunctionName (id) {
    const name = LanguageDefinedFunction.getInternalName(id);
    if (name === null) {
      if (id === LanguageDefinedFunction.getMainFunctionName()) {
        return null;
      }
      return id;
    } else {
      return name;
    }
  }

  parseFunctionCallExpression (id) {
    const stepBack = id.indexOf(".") === -1 ? 1 : 3;
    const tokenA = this.getToken(this.pos - stepBack);
    const actualParameters = this.parseActualParameters();
    const tokenB = this.getToken(this.pos - 1);
    const funcName = this.getFunctionName(id);
    const sourceInfo = SourceInfo.createSourceInfoFromList(tokenA, tokenB);
    const cmd = new Expressions.FunctionCall(funcName, actualParameters);
    cmd.sourceInfo = sourceInfo;
    return cmd;
  }

  parseFunctionCallCommand (id) {
    return this.parseFunctionCallExpression(id);
  }

  parseParenthesisExp () {
    this.checkOpenParenthesis();
    const tokenA = this.getToken();
    this.pos += 1;
    this.consumeNewLines();
    const exp = this.parseExpressionOR();
    this.consumeNewLines();
    this.checkCloseParenthesis();
    const tokenB = this.getToken();
    this.pos += 1;
    exp.sourceInfo = SourceInfo.createSourceInfoFromList(tokenA, tokenB);
    exp.parenthesis = true;
    return exp;
  }

  parseActualParameters () {
    this.checkOpenParenthesis();
    this.pos++;
    if (this.checkCloseParenthesis(true)) {
      this.pos++;
      return [];
    }
    this.consumeNewLines();
    const list = this.parseExpressionList();
    this.consumeNewLines();
    this.checkCloseParenthesis();
    this.pos++;
    return list;
  }

  parseExpressionList () {
    const list = [];
    for (;;) {
      const exp = this.parseExpressionOR();
      list.push(exp);
      const maybeToken = this.getToken();
      if (maybeToken.type !== this.ruleNames.COMMA) {
        break;
      } else {
        this.pos++;
        this.consumeNewLines();
      }
    }
    return list;
  }

  getTypeArray () {
    const types = this.insideScope(IVProgParser.FUNCTION)
      ? this.functionTypes
      : this.variableTypes;
    return types.map((x) => this.lexer.literalNames[x]);
  }
}
