import { AsText, ErrorResult, ProcResult, TextPiece } from "../words";

export function Expr({}, argv: TextPiece[]): ProcResult {
  const name = argv[0];
  if ("bare" in name && name.bare == "expr") {
    // being called as [expr ...], not fallback for math
    argv.splice(0, 1);
  }

  const expression = argv.map(AsText).join(" ").trim();

  const parser = new ExpressionParser(expression);
  const result = parser.parseSubExpression(0);

  if (parser.pos != expression.length) {
    return { error: "Couldn't parse full expression" };
  }

  if ("value" in result) {
    return { text: String(result.value) };
  } else {
    return result;
  }
}

// ---------------------------

// Pratt parser for evaluating arithmatic expressions

const PLUS_TOKEN = /\s*(\+)/y;
const MINUS_TOKEN = /\s*(\-)/y;
const TIMES_TOKEN = /\s*(\*)/y;
const FLOOR_TOKEN = /\s*(\/\/)/y;
const DIV_TOKEN = /\s*(\/)/y;
const MOD_TOKEN = /\s*(\%)/y;

const NUMBER_TOKEN = /\s*(\d+)/y;

type Value = { value: number };

type TokenHandler = {
  leftBindingPower: number;
  token: RegExp;
  parse: (
    left: Value,
    matched: string,
    parser: ExpressionParser
  ) => Value | ErrorResult;
};

function map(
  value: Value | ErrorResult,
  op: (value: number) => Value | ErrorResult
): Value | ErrorResult {
  if ("error" in value) {
    return value;
  } else {
    return op(value.value);
  }
}

function makeInfixOp(
  token: RegExp,
  leftBindingPower: number,
  rightBindingPower: number,
  op: (left: number, right: number) => Value | ErrorResult
): TokenHandler {
  return {
    leftBindingPower,
    token,
    parse: ({ value: left }, matched, parser) =>
      map(parser.parseSubExpression(rightBindingPower), (right) =>
        op(left, right)
      ),
  };
}

const Operators: TokenHandler[] = [
  {
    leftBindingPower: -1,
    token: NUMBER_TOKEN,
    parse: (left, matched) => ({ value: Number(matched) }),
  },
  {
    leftBindingPower: -1,
    token: MINUS_TOKEN,
    parse: (left, matched, parser) =>
      map(parser.parseSubExpression(99), (right) => ({ value: -right })),
  },
  makeInfixOp(PLUS_TOKEN, 10, 11, (left, right) => ({ value: left + right })),
  makeInfixOp(MINUS_TOKEN, 10, 11, (left, right) => ({ value: left - right })),
  makeInfixOp(TIMES_TOKEN, 20, 21, (left, right) => ({ value: left * right })),
  makeInfixOp(FLOOR_TOKEN, 20, 21, (left, right) => ({
    value: Math.floor(left / right),
  })),
  makeInfixOp(DIV_TOKEN, 20, 21, (left, right) => ({ value: left / right })),
  makeInfixOp(MOD_TOKEN, 20, 21, (left, right) => ({
    value: ((left % right) + right) % right,
  })),
];

const ZERO = { value: 0 };

class ExpressionParser {
  pos: number = 0;

  constructor(public text: string) {}

  next(token: RegExp): string | null {
    token.lastIndex = this.pos;
    const matches = token.exec(this.text);
    if (matches) {
      this.pos = token.lastIndex;
      return matches[1];
    } else {
      return null;
    }
  }

  /** Tries to match one of the given tokens & returns the value of the handler. If none match, returns null. */
  tryAll(left: Value, options: TokenHandler[]): Value | ErrorResult | null {
    for (const option of options) {
      const match = this.next(option.token);
      if (match) {
        return option.parse(left, match, this);
      }
    }
    return null;
  }

  parsePrefixOp(): Value | ErrorResult {
    return (
      this.tryAll(
        ZERO,
        Operators.filter((operator) => operator.leftBindingPower == -1)
      ) ?? { error: "Couldn't parse number" }
    );
  }

  parseSubExpression(rightBindingPower: number): Value | ErrorResult {
    let value = this.parsePrefixOp();
    if ("error" in value) {
      return value;
    }

    do {
      const newValue = this.tryAll(
        value,
        Operators.filter(
          (operator) => operator.leftBindingPower > rightBindingPower
        )
      );

      if (newValue == null) {
        break;
      }
      value = newValue;
    } while (!("error" in value));

    return value;
  }
}

// parse expression:
// must parse sub-expression with right binding power 0
// must be at EOF

// parse sub-expression with right binding power :
// must parse a prefix op
// parse as many postfix ops w/ left binding power > right binding power

// parse op:
// must parse associated token (if success- failing the rest of the op fails *everything*)
// depending on op:
//   maybe parse sub-expressions with op's right-binding power
//   maybe consume other tokens