import type {
  GrlArrayExpression,
  GrlBinaryExpression,
  GrlBooleanLiteral,
  GrlCallExpression,
  GrlExpression,
  GrlIdentifierExpression,
  GrlNumberLiteral,
  GrlObjectExpression,
  GrlObjectProperty,
  GrlOffsetAxis,
  GrlOffsetExpression,
  GrlStringLiteral,
  GrlUnaryExpression,
  GrlUnitExpression
} from "../ast/index.js";
import type { GrlToken } from "../lexer/index.js";
import { isGrlUnitLiteral, normalizeUnitLiteral } from "../lexer/units.js";
import { GrlParseError } from "./errors.js";

export function parseGrlExpression(tokens: GrlToken[]): GrlExpression {
  const parser = new GrlExpressionParser(tokens);
  return parser.parse();
}

class GrlExpressionParser {
  private current = 0;

  constructor(private readonly tokens: GrlToken[]) {}

  parse(): GrlExpression {
    const expression = this.parseExpression();
    if (!this.isAtEnd()) {
      throw new GrlParseError("Unexpected token after expression", this.peek());
    }
    return expression;
  }

  private parseExpression(): GrlExpression {
    return this.parseOrExpression();
  }

  private parseOrExpression(): GrlExpression {
    return this.parseBinaryLeftAssoc(() => this.parseAndExpression(), ["or", "||"]);
  }

  private parseAndExpression(): GrlExpression {
    return this.parseBinaryLeftAssoc(() => this.parseEqualityExpression(), ["and", "&&"]);
  }

  private parseEqualityExpression(): GrlExpression {
    return this.parseBinaryLeftAssoc(() => this.parseComparisonExpression(), ["==", "!="]);
  }

  private parseComparisonExpression(): GrlExpression {
    return this.parseBinaryLeftAssoc(() => this.parseAdditiveExpression(), ["<", "<=", ">", ">="]);
  }

  private parseAdditiveExpression(): GrlExpression {
    return this.parseBinaryLeftAssoc(() => this.parseMultiplicativeExpression(), ["+", "-"]);
  }

  private parseMultiplicativeExpression(): GrlExpression {
    return this.parseBinaryLeftAssoc(() => this.parseUnaryExpression(), ["*", "/", "mod"]);
  }

  private parseUnaryExpression(): GrlExpression {
    const token = this.peek();
    if (this.isUnaryOperator(token)) {
      const operatorToken = this.advance();
      const argument = this.parseUnaryExpression();
      const expression: GrlUnaryExpression = {
        kind: "UnaryExpression",
        operator: operatorToken.raw as GrlUnaryExpression["operator"],
        argument,
        range: {
          start: operatorToken.range.start,
          end: argument.range.end
        }
      };
      return expression;
    }

    return this.parseOffsetExpression();
  }

  private parseBinaryLeftAssoc(parseOperand: () => GrlExpression, operators: string[]): GrlExpression {
    let expression = parseOperand();
    while (this.isBinaryOperator(this.peek(), operators)) {
      const operator = this.advance();
      const right = parseOperand();
      const binary: GrlBinaryExpression = {
        kind: "BinaryExpression",
        operator: operator.raw as GrlBinaryExpression["operator"],
        left: expression,
        right,
        range: {
          start: expression.range.start,
          end: right.range.end
        }
      };
      expression = binary;
    }
    return expression;
  }

  private parseOffsetExpression(): GrlExpression {
    const base = this.parseUnitExpression();
    if (this.matchKeyword("offset")) {
      return this.finishOffsetExpression(base, "frame");
    }
    if (this.matchKeyword("offset_in")) {
      if (this.matchKeyword("tool")) {
        return this.finishOffsetExpression(base, "tool");
      }
      this.consumeKeyword("frame", "Expected tool or frame after offset_in");
      const frameName = this.consumeIdentifierLike("Expected frame name after offset_in frame");
      return this.finishOffsetExpression(base, "frame", frameName.raw);
    }
    return base;
  }

  private finishOffsetExpression(
    base: GrlExpression,
    mode: "frame" | "tool",
    frameName?: string
  ): GrlOffsetExpression {
    const axes: GrlOffsetAxis[] = [];
    while (!this.isAtEnd()) {
      const axis = this.consumeAxis();
      const value = this.parseUnaryExpression();
      axes.push({ axis, value });
    }

    if (axes.length === 0) {
      throw new GrlParseError("Expected at least one offset axis", this.peek());
    }

    return {
      kind: "OffsetExpression",
      base,
      mode,
      ...(frameName ? { frameName } : {}),
      axes,
      range: {
        start: base.range.start,
        end: axes.at(-1)?.value.range.end ?? base.range.end
      }
    };
  }

  private parseUnitExpression(): GrlExpression {
    let expression = this.parsePrimary();
    while (this.isUnitToken(this.peek())) {
      const unitToken = this.advance();
      const unit = normalizeUnitLiteral(unitToken.raw);
      const withUnit: GrlUnitExpression = {
        kind: "UnitExpression",
        expression,
        unit: {
          raw: unit.literal,
          kind: unit.kind,
          siUnit: unit.siUnit,
          factor: unit.factor
        },
        range: {
          start: expression.range.start,
          end: unitToken.range.end
        }
      };
      expression = withUnit;
    }
    return expression;
  }

  private parsePrimary(): GrlExpression {
    const token = this.peek();

    if (token.kind === "number") {
      this.advance();
      return numberLiteralFromToken(token);
    }

    if (token.kind === "string") {
      this.advance();
      const literal: GrlStringLiteral = {
        kind: "StringLiteral",
        value: token.value,
        range: token.range
      };
      return literal;
    }

    if (token.kind === "keyword" && (token.raw === "true" || token.raw === "false")) {
      this.advance();
      const literal: GrlBooleanLiteral = {
        kind: "BooleanLiteral",
        value: token.raw === "true",
        range: token.range
      };
      return literal;
    }

    if (token.kind === "punctuation" && token.raw === "[") {
      return this.parseArrayExpression();
    }

    if (token.kind === "punctuation" && token.raw === "(") {
      const start = this.advance();
      const expression = this.parseExpression();
      const end = this.consumePunctuation(")", "Expected ) after expression");
      return {
        ...expression,
        range: {
          start: start.range.start,
          end: end.range.end
        }
      };
    }

    if (token.kind === "identifier" || token.kind === "keyword") {
      const name = this.advance();
      if (this.matchPunctuation("(")) {
        return this.finishCallExpression(name);
      }
      if (this.matchPunctuation("{")) {
        return this.finishObjectExpression(name);
      }
      const expression: GrlIdentifierExpression = {
        kind: "IdentifierExpression",
        name: name.raw,
        range: name.range
      };
      return expression;
    }

    throw new GrlParseError("Expected expression", token);
  }

  private parseArrayExpression(): GrlArrayExpression {
    const start = this.consumePunctuation("[", "Expected [");
    const elements: GrlExpression[] = [];
    while (!this.checkPunctuation("]") && !this.isAtEnd()) {
      elements.push(this.parseExpression());
      this.matchPunctuation(",");
    }
    const end = this.consumePunctuation("]", "Expected ] after array expression");

    return {
      kind: "ArrayExpression",
      elements,
      range: {
        start: start.range.start,
        end: end.range.end
      }
    };
  }

  private finishCallExpression(callee: GrlToken): GrlCallExpression {
    const args: GrlExpression[] = [];
    while (!this.checkPunctuation(")") && !this.isAtEnd()) {
      args.push(this.parseExpression());
      this.matchPunctuation(",");
    }
    const end = this.consumePunctuation(")", "Expected ) after call expression");

    return {
      kind: "CallExpression",
      callee: callee.raw,
      args,
      range: {
        start: callee.range.start,
        end: end.range.end
      }
    };
  }

  private finishObjectExpression(typeName: GrlToken): GrlObjectExpression {
    const properties: GrlObjectProperty[] = [];
    while (!this.checkPunctuation("}") && !this.isAtEnd()) {
      const key = this.consumeIdentifierLike("Expected object property name");
      this.consumePunctuation(":", "Expected : after object property name");
      const value = this.parseExpression();
      properties.push({
        key: key.raw,
        value,
        range: {
          start: key.range.start,
          end: value.range.end
        }
      });
      this.matchPunctuation(",");
    }
    const end = this.consumePunctuation("}", "Expected } after object expression");

    return {
      kind: "ObjectExpression",
      typeName: typeName.raw,
      properties,
      range: {
        start: typeName.range.start,
        end: end.range.end
      }
    };
  }

  private consumeAxis(): "x" | "y" | "z" {
    const token = this.consumeIdentifierLike("Expected offset axis");
    if (token.raw === "x" || token.raw === "y" || token.raw === "z") {
      return token.raw;
    }
    throw new GrlParseError("Expected offset axis x, y, or z", token);
  }

  private consumeIdentifierLike(message: string): GrlToken {
    const token = this.peek();
    if (token.kind === "identifier" || token.kind === "keyword") {
      return this.advance();
    }
    throw new GrlParseError(message, token);
  }

  private consumeKeyword(keyword: string, message: string): GrlToken {
    if (this.checkKeyword(keyword)) {
      return this.advance();
    }
    throw new GrlParseError(message, this.peek());
  }

  private consumePunctuation(value: string, message: string): GrlToken {
    if (this.checkPunctuation(value)) {
      return this.advance();
    }
    throw new GrlParseError(message, this.peek());
  }

  private matchKeyword(keyword: string): boolean {
    if (this.checkKeyword(keyword)) {
      this.advance();
      return true;
    }
    return false;
  }

  private matchPunctuation(value: string): boolean {
    if (this.checkPunctuation(value)) {
      this.advance();
      return true;
    }
    return false;
  }

  private checkKeyword(keyword: string): boolean {
    const token = this.peek();
    return (token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword;
  }

  private checkPunctuation(value: string): boolean {
    const token = this.peek();
    return token.kind === "punctuation" && token.raw === value;
  }

  private isUnaryOperator(token: GrlToken): boolean {
    return (
      (token.kind === "operator" && (token.raw === "+" || token.raw === "-" || token.raw === "!")) ||
      ((token.kind === "keyword" || token.kind === "identifier") && token.raw === "not")
    );
  }

  private isBinaryOperator(token: GrlToken, operators: string[]): boolean {
    return (
      (token.kind === "operator" || token.kind === "keyword" || token.kind === "identifier") &&
      operators.includes(token.raw)
    );
  }

  private isUnitToken(token: GrlToken): boolean {
    return !this.isAtEnd() && (token.kind === "identifier" || token.kind === "keyword" || token.kind === "operator") && isGrlUnitLiteral(token.raw);
  }

  private advance(): GrlToken {
    if (!this.isAtEnd()) {
      this.current += 1;
    }
    return this.previous();
  }

  private isAtEnd(): boolean {
    return this.current >= this.tokens.length;
  }

  private peek(): GrlToken {
    return this.tokens[this.current] ?? this.tokens[this.tokens.length - 1]!;
  }

  private previous(): GrlToken {
    return this.tokens[this.current - 1] ?? this.tokens[0]!;
  }
}

function numberLiteralFromToken(token: GrlToken): GrlNumberLiteral {
  if (token.kind !== "number") {
    throw new GrlParseError("Expected number literal", token);
  }

  return {
    kind: "NumberLiteral",
    value: token.value,
    raw: token.raw,
    ...(token.unit
      ? {
          unit: {
            raw: token.unit.raw,
            kind: token.unit.kind,
            siUnit: token.unit.siUnit,
            normalizedValue: token.unit.normalizedValue
          }
        }
      : {}),
    range: token.range
  };
}
