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";
import { Literal } from "../ast/expressions/literal";

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.Case} switchCase
 * */
function switchCaseWalker (switchCase) {
  const commands = switchCase.commands.map(commandWalker);
  const expression = switchCase.isDefault
    ? null
    : expressionWalker(switchCase.expression);
  return {
    type: "switchcase",
    line: switchCase.sourceInfo.line,
    expression,
    commands,
  };
}

/**
 * @param {Commands.Switch} switchCommand
 * */
function switchWalker (switchCommand) {
  const expression = expressionWalker(switchCommand.expression);
  const cases = switchCommand.cases.map(switchCaseWalker);
  return {
    type: "switch",
    expression,
    cases,
  };
}

/**
 * @param {Commands.Return} returnCommand
 * */
function returnWalker (returnCommand) {
  const expression = expressionWalker(returnCommand.expression);
  return {
    type: "return",
    expression,
  };
}

function breakWalker (_) {
  return { type: "break" };
}

/**
 * @param {Commands.For} forLoop
 * */
function forWalker (forLoop) {
  const var_attribution = expressionWalker(forLoop.for_id);
  const var_initial = expressionWalker(forLoop.for_from);
  const condition = expressionWalker(forLoop.for_to);
  const step_expression = forLoop.for_pass
    ? expressionWalker(forLoop.for_pass)
    : [];
  const commands = forLoop.commands.map(commandWalker);
  return {
    type: "repeatNtimes",
    var_attribution,
    var_initial,
    condition,
    step_expression,
    commands,
  };
}

/**
 * @param {Commands.While} whileLoop
 * */
function whileWalker (whileLoop) {
  const expression = expressionWalker(whileLoop.expression);
  const commands = whileLoop.commands.map(commandWalker);
  let type = whileLoop.testFirst ? "whiletrue" : "dowhiletrue";
  return {
    type,
    expression,
    commands,
  };
}

/**
 * @param {Commands.IfThenElse} ifthenelse
 * */
function ifThenElseWalker (ifthenelse) {
  //ifthenelse.
  const expression = expressionWalker(ifthenelse.condition);
  const ifTrue = ifthenelse.ifTrue.commands.map(commandWalker);
  let ifFalse = [];
  if (ifthenelse.ifFalse) {
    if (ifthenelse.ifFalse instanceof Commands.CommandBlock) {
      ifFalse = ifthenelse.ifFalse.commands.map(commandWalker);
    } else {
      ifFalse = [ifThenElseWalker(ifthenelse.ifFalse)];
    }
  }
  return {
    type: "iftrue",
    expression,
    ifTrue,
    ifFalse,
  };
}

/**
 * @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) {
  let parsedCommand = null;
  if (command instanceof Commands.FunctionCall) {
    parsedCommand = functionCallWalker(command);
  } else if (command instanceof Commands.Assign) {
    parsedCommand = assignmentWalker(command);
  } else if (command instanceof Commands.IfThenElse) {
    parsedCommand = ifThenElseWalker(command);
  } else if (command instanceof Commands.While) {
    parsedCommand = whileWalker(command);
  } else if (command instanceof Commands.Break) {
    parsedCommand = breakWalker(command);
  } else if (command instanceof Commands.Return) {
    parsedCommand = returnWalker(command);
  } else if (command instanceof Commands.Switch) {
    parsedCommand = switchWalker(command);
  } else if (command instanceof Commands.For) {
    parsedCommand = forWalker(command);
  } else {
    throw new Error("not implemented");
  }
  parsedCommand.line = command.sourceInfo.line;
  return parsedCommand;
}

/**
 * @param {Commands.FunctionCall} functionCall
 * */
function functionCallWalker (functionCall) {
  let name = functionCall.id;
  if (name.indexOf(".") !== -1) {
    name = name.split(".")[1];
  }
  const parameters = functionCall.actualParameters.map(expressionWalker);
  if (name === "$write") {
    const lastInput = parameters[parameters.length - 1][0];
    // if lastInput is an object with value === '\n', newLine is true
    const newLine = lastInput.value && lastInput.value.match(/^\n$/) !== null;
    const content = newLine
      ? parameters.slice(0, parameters.length - 1)
      : parameters;
    return {
      type: "writer",
      newLine,
      content,
    };
  }
  if (name === "$read") {
    return {
      type: "reader",
      variable: parameters[0],
    };
  }
  return {
    type: "functioncall",
    parameters_list: parameters,
    name: functionCall.id,
  };
}
/**
 * @param {Commands.Function} func
 * */
function functionWalker (func) {
  const funcDeclaration = {
    name: func.name,
    line: func.sourceInfo.line,
    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,
    line: formalParameter.sourceInfo.line,
    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,
    line: command.sourceInfo.line,
    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.columns = lines.value;
      variable.dimension = 1;
      const values = command.initial.value.map((exp) =>
        variableInitialWalker(exp)
      );
      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.value.map((rows) =>
        rows.value.map((exp) => variableInitialWalker(exp))
      );
      variable.value = values;
    }
  } else {
    // atomic
    variable.type = command.type.value;
    variable.value = variableInitialWalker(command.initial);
  }
  return variable;
}

/**
 * @param {any} expression
 * */
function variableInitialWalker (expression) {
  if (expression instanceof Expressions.UnaryApp) {
    const left = variableInitialWalker(expression.left);
    const opType = getOpType(expression.op);
    if (opType !== TYPES.ARITHMETIC) {
      throw new Error(
        "invalid variable initial value: " + expression.toString()
      );
    }
    return `${expression.op.value}${left}`;
  } else if (expression instanceof Expressions.BoolLiteral) {
    const value = expression.value;
    return convertBoolToString(value);
  } else if (expression instanceof Literal) {
    let value = expression.value;
    if (expression.value.toNumber) {
      if (
        Types.REAL.isCompatible(expression.type) &&
        expression.value.decimalPlaces() == 0
      ) {
        value = Number(expression.value.toFixed(2));
      } else {
        value = expression.value.toNumber();
      }
    }
    return value;
  }
  throw new Error("invalid variable initial value: " + expression.toString());
}

/**
 *
 * @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 = Number(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);
  try {
    const program = parser.parseTree();
    const globals = program.global.map((decl) =>
      variableDeclarationWalker(decl, true)
    );
    const functions = program.functions.map(functionWalker);
    return { globals, functions };
  } catch (e) {
    console.error(e);
    return null;
  }
}