import type {
  GrlArrayExpression,
  GrlBooleanLiteral,
  GrlCallExpression,
  GrlExpression,
  GrlIdentifierExpression,
  GrlNumberLiteral,
  GrlObjectExpression,
  GrlObjectProperty,
  GrlOffsetAxis,
  GrlOffsetExpression,
  GrlStringLiteral
} from "../ast/index.js";
import type { GrlToken } from "../lexer/index.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.parseOffsetExpression();
    if (!this.isAtEnd()) {
      throw new GrlParseError("Unexpected token after expression", this.peek());
    }
    return expression;
  }

  private parseOffsetExpression(): GrlExpression {
    const base = this.parsePrimary();
    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 valueExpression = this.parsePrimary();
      if (valueExpression.kind !== "NumberLiteral") {
        throw new GrlParseError(`Expected length value after offset ${axis}`, valueExpression.range ? this.previous() : this.peek());
      }
      const value = valueExpression;
      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 parsePrimary(): GrlExpression {
    const token = this.peek();

    if (token.kind === "operator" && token.raw === "-") {
      return this.parseNegativeNumber();
    }

    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 === "(") {
      this.advance();
      const expression = this.parseOffsetExpression();
      this.consumePunctuation(")", "Expected ) after expression");
      return expression;
    }

    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.parseOffsetExpression());
      this.matchPunctuation(",");
    }
    const end = this.consumePunctuation("]", "Expected ] after array expression");

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

  private parseNegativeNumber(): GrlNumberLiteral {
    const minus = this.advance();
    const number = this.consumeNumberLiteral("Expected number after -");
    return {
      ...number,
      value: -number.value,
      raw: `${minus.raw}${number.raw}`,
      ...(number.unit
        ? {
            unit: {
              ...number.unit,
              normalizedValue: -number.unit.normalizedValue
            }
          }
        : {}),
      range: {
        start: minus.range.start,
        end: number.range.end
      }
    };
  }

  private finishCallExpression(callee: GrlToken): GrlCallExpression {
    const args: GrlExpression[] = [];
    while (!this.checkPunctuation(")") && !this.isAtEnd()) {
      args.push(this.parseOffsetExpression());
      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.parseOffsetExpression();
      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 consumeNumberLiteral(message: string): GrlNumberLiteral {
    const token = this.consume("number", message);
    if (token.kind !== "number") {
      throw new GrlParseError(message, token);
    }
    return numberLiteralFromToken(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 consume(kind: GrlToken["kind"], message: string): GrlToken {
    if (this.check(kind)) {
      return this.advance();
    }
    throw new GrlParseError(message, this.peek());
  }

  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 check(kind: GrlToken["kind"]): boolean {
    return !this.isAtEnd() && this.peek().kind === kind;
  }

  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 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
  };
}
