import "./scanner";
import {
  scan,
  error,
  BANG,
  BANG_EQUAL,
  EOF,
  EQUAL,
  EQUAL_EQUAL,
  FALSE,
  GREATER,
  GREATER_EQUAL,
  IDENTIFIER,
  LEFT_BRACE,
  LEFT_PAREN,
  LESS,
  LESS_EQUAL,
  MINUS,
  NUMBER,
  PLUS,
  PRINT,
  RIGHT_BRACE,
  RIGHT_PAREN,
  SEMICOLON,
  SLASH,
  STAR,
  STRING,
  TRUE,
  VAR,
  START,
  COMMA,
  GO,
  STAIRCASE,
  PILLARS,
  MOVING_PILLARS,
  LEFT,
  RIGHT,
  TURN,
  REPEAT
} from "./scanner";

export function parse(code) {
  const tokens = scan(code);
  // console.log(tokens);
  let current = 0;

  const parse = () => {
    let statements = [];

    while (!is_at_end()) {
      try {
        statements.push(declaration());
      } catch (e) {
        console.log(e);
        current = tokens.length - 1; // stop compiling
      }
    }
    return statements;
  }

  const declaration = () => {
    if (match(VAR)) {
      return varDeclaration();
    }
    return statement();
  }

  const varDeclaration = () => {
    const name = consume(IDENTIFIER, "Expected a variable name");
    let initializer;
    if (match(EQUAL)) {
      initializer = expression();
    }
    consume(SEMICOLON, "Expected semicolon");
    return {
      class: "var",
      name: name,
      initializer: initializer,
      accept: (visitor) => visitor.visitVariableStatement(name, initializer)
    };
  }

  const statement = () => {
    if (match(PRINT)) {
      return printStatement();
    }
    if (match(START)) {
      return callStatement("start");
    }
    if (match(GO)) {
      return callStatement("go");
    }
    if (match(LEFT)) {
      return callStatement("left");
    }
    if (match(RIGHT)) {
      return callStatement("right");
    }
    if (match(TURN)) {
      return callStatement("turn");
    }
    if (match(STAIRCASE)) {
      return callStatement("staircase");
    }
    if (match(PILLARS)) {
      return callStatement("pillars");
    }
    if (match(MOVING_PILLARS)) {
      return callStatement("moving_pillars");
    }
    if (match(REPEAT)) {
      return call_block();
    }
    if (match(LEFT_BRACE)) {
      const blockStatement = block();
      return {class: "block", accept: (visitor) => visitor.visitBlockStatement(blockStatement)};
    }

    return expressionStatement();
  }

  const expression = () => {
    return assignment();
  }

  const arg_expressions = () => {
    consume(LEFT_PAREN, "Expect '('");
    let exprs = [expression()];
    while (match(COMMA)) {
      exprs.push(expression());
    }
    consume(RIGHT_PAREN, "Expect ')'");
    return exprs;
  }

  const assignment = () => {
    let expr = equality();
    if (match(EQUAL)) {
      let equals = previous();
      let value = assignment();

      if (expr.class === 'Variable') {
        let name = expr.name;
        return {
          class: "assign",
          var_name: name,
          init_expr: value,
          accept: (visitor) => visitor.visitAssignExpr(name, value)
        };
      }

      throw error(equals, "Invalid assignment target.");
    }
    return expr;
  }

  const printStatement = () => {
    const value = expression();
    consume(SEMICOLON, "Expected semicolon");
    return {class: "print", value: value, accept: (visitor) => visitor.visitPrintStatement(value)};
  }

  const callStatement = (name) => {
    const args = arg_expressions();
    consume(SEMICOLON, "Expected semicolon");
    return {class: "call", accept: (visitor) => visitor.visitCallStatement(name, args)};
  }

  const call_block = () => {
    const args = arg_expressions();
    consume(LEFT_BRACE, "Expect block");
    const action = block();
    return {class: "block", accept: (visitor) => visitor.visitParametrizedBlock(args, action)};
  }

  const expressionStatement = () => {
    const value = expression();
    consume(SEMICOLON, "Expected semicolon");
    return {
      class: "expressionStatement",
      expression: value,
      accept: (visitor) => visitor.visitExpressionStatement(value)
    };
  }

  const block = () => {
    let statements = [];
    while (!check(RIGHT_BRACE) && !is_at_end()) {
      statements.push(declaration());
    }
    consume(RIGHT_BRACE, "Expect '}' after block.");
    return statements;
  }

  const equality = () => {
    let expr = comparison();
    while (match(BANG_EQUAL, EQUAL_EQUAL)) {
      let operator = previous();
      let right = comparison();
      const left = expr;
      expr = {
        class: "binaryExpr",
        operator: operator,
        left: expr,
        right: right,
        accept: (visitor) => visitor.visitBinaryExpr(operator, left, right)
      };
    }
    return expr;
  }

  const comparison = () => {
    let expr = term();
    while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) {
      let operator = previous();
      let right = term();
      const left = expr;
      expr = {
        class: "binaryExpr",
        operator: operator,
        left: expr,
        right: right,
        accept: (visitor) => visitor.visitBinaryExpr(operator, left, right)
      };
    }
    return expr;
  }

  const term = () => {
    let expr = factor();
    while (match(MINUS, PLUS)) {
      let operator = previous();
      let right = factor();
      const left = expr;
      expr = {
        class: "binaryExpr",
        operator: operator,
        left: expr,
        right: right,
        accept: (visitor) => visitor.visitBinaryExpr(operator, left, right)
      };
    }
    return expr;
  }

  const factor = () => {
    let expr = unary();
    while (match(SLASH, STAR)) {
      let operator = previous();
      let right = unary();
      const left = expr;
      expr = {
        class: "binaryExpr",
        operator: operator,
        left: expr,
        right: right,
        accept: (visitor) => visitor.visitBinaryExpr(operator, left, right)
      };
    }
    return expr;
  }

  const unary = () => {
    if (match(BANG, MINUS)) {
      let operator = previous();
      let right = unary();
      return {
        class: "unaryExpr",
        operator: operator,
        right: right,
        accept: (visitor) => visitor.visitUnaryExpr(operator, right)
      };
    }
    return primary();
  }

  const primary = () => {
    if (match(FALSE)) {
      return {class: "boolean", value: false, accept: (visitor) => visitor.visitLiteralExpr(false)};
    }
    if (match(TRUE)) {
      return {class: "boolean", value: true, accept: (visitor) => visitor.visitLiteralExpr(true)};
    }
    if (check(NUMBER)) {
      advance();
      let number = parseFloat(previous().literal);
      return {class: "number", value: number, accept: (visitor) => visitor.visitLiteralExpr(number)};
    }
    if (check(STRING)) {
      advance();
      let string = previous().literal;
      return {class: "string", value: string, accept: (visitor) => visitor.visitLiteralExpr(string)};
    }
    if (match(IDENTIFIER)) {
      let identifier = previous();
      return {class: "Variable", identifier: identifier, accept: (visitor) => visitor.visitVariableExpr(identifier)};
    }
    if (match(LEFT_PAREN)) {
      let expr = expression();
      consume(RIGHT_PAREN, "Expect ')' after expression.");
      return {class: "groupExpr", expression: expr, accept: (visitor) => visitor.visitGroupingExpr(expr)};
    }
    throw error(peek(), "Expect expression.");
  }

  const consume = (type, message) => {
    if (check(type)) {
      return advance();
    }
    throw error(peek(), message);
  }

  const match = (...types) => {
    for (let i = 0; i < types.length; i++) {
      if (check(types[i])) {
        advance();
        return true;
      }
    }

    return false;
  }

  const check = (type) => {
    // console.log(peek().type+"==="+type);
    if (is_at_end()) {
      return false;
    }
    return peek().type === type;
  }

  const advance = () => {
    if (!is_at_end()) {
      current++;
    }
    return previous();
  }

  const is_at_end = () => {
    return peek().type === EOF;
  }

  const peek = () => {
    return tokens[current];
  }

  const previous = () => {
    return tokens[current - 1];
  }

  return parse();
}
