import { IVProgParser } from "../ast/ivprogParser";

import * as Expressions from "../ast/expressions";
import { Types } from "../typeSystem/types";
import { convertBoolToString } from "../typeSystem/parsers";
import * as Commands from "../ast/commands";
import { ArrayType } from "../typeSystem/array_type";

const TYPES = {
  VARIABLE: "var",
  CONST: "const",
  FUNCTION: "function",
  RELATIONAL: "relational",
  LOGIC: "logic",
  ARITHMETIC: "arithmetic",
};

function translateOp (type, op) {
  switch (type) {
    case TYPES.ARITHMETIC:
      return op.value;
    case TYPES.RELATIONAL:
      return op.value;
    case TYPES.LOGIC: {
      if (op.ord === 11) {
        return "and";
      } else if (op.ord === 12) {
        return "or";
      } else {
        return "not";
      }
    }
  }
}

function getOpType (op) {
  switch (op.ord) {
    case 0:
    case 1:
    case 2:
    case 3:
    case 4:
      return TYPES.ARITHMETIC;
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
    case 10:
      return TYPES.RELATIONAL;
    default:
      return TYPES.LOGIC;
  }
}
/**
  * @param {Commands.Assign} assingment
  * */
function assignmentWalker (assingment) {
  let variable = null
  if (assingment instanceof Commands.ArrayIndexAssign) {
    const line = expressionWalker(assingment.line);
    let arrayClass = "vector";
    let column = null;
    if (assingment.column) {
      arrayClass = "matrix";
      column = expressionWalker(assingment.column);
    }
    variable = [
      {
        instance: "expression",
        type: TYPES.VARIABLE,
        class: arrayClass,
        column: column,
        line: line,
        value: assingment.id,
      },
    ];
  } else {
    variable = [
      { instance: "expression", type: TYPES.VARIABLE, value: assingment.id },
    ]
  }
  const expression = expressionWalker(assingment.expression)
  return {
    type: "attribution",
    variable,
    expression
  }
}

/**
 * @param {Command} command
  * */
function commandWalker (command) {
  if (command instanceof Commands.FunctionCall) {
    return functionCallWalker(command)
  } else if (command instanceof Commands.Assign) {
    return assignmentWalker(command)
  }
  throw new Error("not implemented")
}

/**
 * @param {Commands.FunctionCall} functionCall
  * */
function functionCallWalker (functionCall) {
  let name = functionCall.id;
  if (name.indexOf(".") !== -1) {
    name = name.split(".")[1]
  }
  if (name === "$read") {
    const variable = functionCall.actualParameters.map(expressionWalker)[0]
    return {
      type:"reader",
      variable
    }
  }
  if (name === "$write") {
    const content = functionCall.actualParameters.map(expressionWalker)
    const lastInput = content[content.length - 1][0];
    // if lastInput is an object with value === '\n', newLine is true
    const newLine = lastInput.value && lastInput.value.match(/\n/) !== null
    return {
      type:"writer",
      newLine,
      content
    }
  }
  throw new Error("not implemented")
}
/**
 * @param {Commands.Function} func
 * */
function functionWalker (func) {
  const funcDeclaration = {
    name: func.name,
    return_type: "",
    return_dimensions: 0,
    parameters_list: [],
    variables_list: [],
    commands: [],
  };
  if (func.returnType instanceof ArrayType) {
    funcDeclaration.return_type = func.returnType.innerType.value;
    funcDeclaration.return_dimensions = func.returnType.dimensions;
  } else {
    funcDeclaration.return_type = func.returnType.value;
  }
  funcDeclaration.parameters_list = func.formalParameters.map(
    functionParameterWalker
  );
  funcDeclaration.variables_list = func.variablesDeclarations.map(
    variableDeclarationWalker
  );
  funcDeclaration.commands = func.commands.map(commandWalker)
  return funcDeclaration;
}

/**
 * @param {Commands.FormalParameter} formalParameter
 * */
function functionParameterWalker (formalParameter) {
  const variable = {
    name: formalParameter.id,
    type: "",
    rows: 0,
    columns: 0,
    dimension: 0,
    value: 0,
    is_const: false,
    reference: formalParameter.byRef,
  };
  if (formalParameter.type instanceof ArrayType) {
    variable.type = formalParameter.type.innerType.value;
    variable.dimension = formalParameter.type.dimensions;
  } else {
    variable.type = formalParameter.type.value;
  }
  return variable;
}

/**
 * @param {Commands.Declaration} command
 * @param {boolean} global
 * */
function variableDeclarationWalker (command, global = false) {
  const variable = {
    name: command.id,
    type: "",
    rows: 0,
    columns: 0,
    dimension: 0,
    value: 0,
    is_const: false,
  };
  variable.is_const = global && command.isConst;
  if (command instanceof Commands.ArrayDeclaration) {
    // array
    const lines = expressionWalker(command.lines).pop();
    variable.type = command.type.innerType.value;
    if (command.isVector) {
      variable.rows = 1;
      variable.columns = lines.value;
      variable.dimension = 1;
      const values = command.initial.map(
        (exp) => expressionWalker(exp).pop().value
      );
      variable.value = values;
    } else {
      const columns = expressionWalker(command.columns).pop();
      variable.dimension = 2;
      variable.rows = lines.value;
      variable.columns = columns.value;
      const values = command.initial.map((rows) =>
        rows.map((exp) => expressionWalker(exp).pop().value)
      );
      variable.value = values;
    }
  } else {
    // atomic
    variable.type = command.type.value;
    variable.value = expressionWalker(command.initial).pop().value;
  }
  return variable;
}

/**
 *
 * @return {[]}
 **/
function expressionWalker (expression) {
  let result;
  if (expression instanceof Expressions.VariableLiteral) {
    result = [
      { instance: "expression", type: TYPES.VARIABLE, value: expression.id },
    ];
  } else if (expression instanceof Expressions.FunctionCall) {
    const funcObj = {
      instance: "expression",
      type: TYPES.FUNCTION,
      value: expression.id,
    };
    const paramsList = expression.actualParameters.map((e) =>
      expressionWalker(e)
    );
    //const params = Array.prototype.concat.apply([], paramsList);
    funcObj.params = paramsList;
    result = [funcObj];
  } else if (expression instanceof Expressions.UnaryApp) {
    const left = expressionWalker(expression.left);
    const opType = getOpType(expression.op);
    const opValue = translateOp(opType, expression.op);
    result = [{ instance: "operator", type: opType, value: opValue }, ...left];
  } else if (expression instanceof Expressions.InfixApp) {
    const left = expressionWalker(expression.left);
    const right = expressionWalker(expression.right);
    const opType = getOpType(expression.op);
    const opValue = translateOp(opType, expression.op);
    result = [
      ...left,
      { instance: "operator", type: opType, value: opValue },
      ...right,
    ];
  } else if (expression instanceof Expressions.ArrayAccess) {
    const line = expressionWalker(expression.line);
    let arrayClass = "vector";
    let column = null;
    if (expression.column) {
      arrayClass = "matrix";
      column = expressionWalker(expression.column);
    }
    result = [
      {
        instance: "expression",
        type: TYPES.VARIABLE,
        class: arrayClass,
        column: column,
        line: line,
        value: expression.id,
      },
    ];
  } else if (expression instanceof Expressions.BoolLiteral) {
    const value = expression.value;
    result = [
      {
        instance: "expression",
        class: "simple",
        type: TYPES.CONST,
        value: convertBoolToString(value),
      },
    ];
  } else {
    let value = expression.value;
    if (expression.value.toNumber) {
      if (
        Types.REAL.isCompatible(expression.type) &&
        expression.value.decimalPlaces() == 0
      ) {
        value = expression.value.toFixed(2);
      } else {
        value = expression.value.toNumber();
      }
    }
    result = [
      {
        instance: "expression",
        class: "simple",
        type: TYPES.CONST,
        value: value,
      },
    ];
  }
  if (expression.parenthesis) return ["(", ...result, ")"];
  else return result;
}

export function parseExpression (text) {
  const parser = IVProgParser.createParser(text);
  const expressionAST = parser.parseExpressionOR();
  return expressionWalker(expressionAST);
}

/**
 * @param {string} text
 * */
export function parseCode (text) {
  const parser = IVProgParser.createParser(text, false);
  const codeLinesMap = new Map();
  const tokens = Array.from(parser.lexer.reset(text));
  const tokenStream = [];
  for (const token of tokens) {
    if (token.type === parser.ruleNames.ERROR) {
      return null;
    }
    if (token.type === parser.ruleNames.COMMENTS) {
      for (let i = 0; i <= token.lineBreaks; i++) {
        if (codeLinesMap.has(i + token.line))
          codeLinesMap.get(i + token.line).push(token);
        else codeLinesMap.set(i + token.line, [token]);
      }
      continue;
    }
    if (token.type !== parser.ruleNames.WHITESPACE) {
      tokenStream.push(token);
    }
  }
  parser.fill(tokenStream);
  const program = parser.parseTree();
  const globals = program.global.map((decl) =>
    variableDeclarationWalker(decl, true)
  );
  const functions = program.functions.map(functionWalker);
  return { globals, functions };
}