import { KdlStructuredError } from "../../kdl/rpc.js";
import type { MotionSourceMap } from "../../kdl/types.js";
import type {
  IoFlowInstruction,
  IoReference,
  IoWriteInstruction,
  OperationActionInstruction,
  PathEventInstruction,
  PulseInstruction,
  WaitInstruction
} from "../ir/index.js";
import { lexGrl, type GrlToken } from "../lexer/index.js";

export interface IoMap {
  aliases?: Record<string, IoReference>;
  allowedRanges?: Partial<Record<IoReference["domain"], { min: number; max: number }>>;
}

export function parseIoFlowStatements(tokens: GrlToken[], ioMap: IoMap = {}): IoFlowInstruction[] {
  const parser = new IoStatementParser(tokens, ioMap);
  return parser.parseAll();
}

export function compilePathEventIo(event: PathEventInstruction, ioMap: IoMap = {}): IoFlowInstruction[] {
  const tokens = event.data?.tokens;
  if (Array.isArray(tokens)) {
    return parseIoFlowStatements(tokens as GrlToken[], ioMap);
  }
  const statement = event.data?.statement;
  if (typeof statement !== "string") {
    return [];
  }
  return compileStatementString(statement, event.sourceMap, ioMap);
}

export function compileOperationActionIo(
  action: OperationActionInstruction,
  ioMap: IoMap = {}
): IoFlowInstruction[] {
  if (Array.isArray(action.tokens)) {
    return parseIoFlowStatements(action.tokens as GrlToken[], ioMap);
  }
  return compileStatementString(action.statement, action.sourceMap, ioMap);
}

class IoStatementParser {
  private current = 0;

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

  parseAll(): IoFlowInstruction[] {
    const instructions: IoFlowInstruction[] = [];
    while (!this.isAtEnd()) {
      if (this.checkKeyword("wait")) {
        instructions.push(this.finishWait(this.advance()));
        continue;
      }
      if (this.checkKeyword("pulse")) {
        instructions.push(this.finishPulse(this.advance()));
        continue;
      }
      if (this.checkIoStart()) {
        instructions.push(this.finishIoWrite(this.peek()));
        continue;
      }
      this.advance();
    }
    return instructions;
  }

  private finishIoWrite(start: GrlToken): IoWriteInstruction {
    const target = this.parseIoReference();
    this.consumeOperator("=", "Expected = in IO assignment");
    const value = this.parseValue();
    return {
      kind: "IO_WRITE",
      target,
      value,
      sourceMap: tokenSourceMap(start)
    };
  }

  private finishWait(start: GrlToken): WaitInstruction {
    const conditionTokens = this.collectUntilKeyword(["timeout", "on_timeout"]);
    if (conditionTokens.length === 0) {
      throw ioError("GRL_WAIT_CONDITION_MISSING", "wait requires a condition");
    }

    let timeout: number | undefined;
    let onTimeout: WaitInstruction["onTimeout"];
    if (this.matchKeyword("timeout")) {
      timeout = this.parseDuration();
    }
    if (this.matchKeyword("on_timeout")) {
      const kind = this.consumeIdentifier("Expected on_timeout action").raw;
      if (kind === "alarm") {
        const message = this.consumeString("Expected alarm message");
        onTimeout = { kind: "alarm", value: message.value };
      } else if (kind === "call") {
        onTimeout = { kind: "call", value: this.collectRest().map((token) => token.raw).join(" ") };
      } else {
        throw ioError("GRL_WAIT_TIMEOUT_ACTION_INVALID", `Unsupported on_timeout action ${kind}`);
      }
    }

    return {
      kind: "WAIT",
      condition: conditionTokens.map((token) => token.raw).join(" "),
      ...(timeout !== undefined ? { timeout } : {}),
      ...(onTimeout ? { onTimeout } : {}),
      sourceMap: tokenSourceMap(start)
    };
  }

  private finishPulse(start: GrlToken): PulseInstruction {
    const target = this.parseIoReference();
    this.consumeKeyword("duration", "Expected duration in pulse");
    const duration = this.parseDuration();
    return {
      kind: "PULSE",
      target,
      duration,
      trace: [
        { time: 0, action: "set", target, value: true },
        { time: duration, action: "reset", target, value: false }
      ],
      sourceMap: tokenSourceMap(start)
    };
  }

  private parseIoReference(): IoReference {
    const io = this.consumeKeyword("io", "Expected io reference");
    this.consumePunctuation(".", "Expected . after io");
    const domain = this.consumeIdentifier("Expected IO domain").raw as IoReference["domain"];
    if (!["di", "do", "ai", "ao", "gi", "go", "ri", "ro", "alias"].includes(domain)) {
      throw ioError("GRL_IO_DOMAIN_INVALID", `Unsupported IO domain ${domain}`);
    }

    if (domain === "alias") {
      this.consumePunctuation(".", "Expected . after io.alias");
      const alias = this.consumeIdentifier("Expected IO alias").raw;
      const mapped = this.ioMap.aliases?.[alias];
      return mapped ?? { domain, alias, raw: `io.alias.${alias}` };
    }

    this.consumePunctuation("[", "Expected [ after IO domain");
    const indexToken = this.consumeNumber("Expected IO index");
    this.consumePunctuation("]", "Expected ] after IO index");
    const index = indexToken.value;
    this.validateIoIndex(domain, index, io);
    return {
      domain,
      index,
      raw: `io.${domain}[${index}]`
    };
  }

  private validateIoIndex(domain: IoReference["domain"], index: number, token: GrlToken): void {
    if (!Number.isInteger(index) || index < 0) {
      throw ioError("GRL_IO_INDEX_INVALID", `Invalid IO index ${index}`);
    }
    const range = this.ioMap.allowedRanges?.[domain];
    if (range && (index < range.min || index > range.max)) {
      throw new KdlStructuredError(
        "GRL_IO_ADDRESS_NOT_FOUND",
        `IO address io.${domain}[${index}] is outside [${range.min}, ${range.max}]`,
        [
          {
            severity: "error",
            code: "GRL_IO_ADDRESS_NOT_FOUND",
            message: `IO address io.${domain}[${index}] is outside [${range.min}, ${range.max}]`,
            sourceMap: tokenSourceMap(token)
          }
        ]
      );
    }
  }

  private parseValue(): boolean | number | string {
    const token = this.advance();
    if (token.kind === "keyword" && (token.raw === "true" || token.raw === "false")) {
      return token.raw === "true";
    }
    if (token.kind === "number") {
      return normalizedNumber(token);
    }
    if (token.kind === "string") {
      return token.value;
    }
    if (token.kind === "identifier" || token.kind === "keyword") {
      return token.raw;
    }
    throw ioError("GRL_IO_VALUE_INVALID", "Unsupported IO assignment value");
  }

  private parseDuration(): number {
    const token = this.consumeNumber("Expected duration");
    return normalizedNumber(token);
  }

  private collectUntilKeyword(keywords: string[]): GrlToken[] {
    const tokens: GrlToken[] = [];
    let parenDepth = 0;
    while (!this.isAtEnd()) {
      const token = this.peek();
      if (parenDepth === 0) {
        if ((token.kind === "keyword" || token.kind === "identifier") && keywords.includes(token.raw)) {
          break;
        }
        if (tokens.length > 0 && this.isCurrentStatementStartAfter(tokens)) {
          break;
        }
      }
      const consumed = this.advance();
      tokens.push(consumed);
      if (consumed.kind === "punctuation" && consumed.raw === "(") {
        parenDepth += 1;
      } else if (consumed.kind === "punctuation" && consumed.raw === ")") {
        parenDepth = Math.max(0, parenDepth - 1);
      }
    }
    return tokens;
  }

  private collectRest(): GrlToken[] {
    const tokens: GrlToken[] = [];
    while (!this.isAtEnd()) {
      if (tokens.length > 0 && this.isCurrentStatementStartAfter(tokens)) {
        break;
      }
      tokens.push(this.advance());
    }
    return tokens;
  }

  private checkIoStart(): boolean {
    return this.isIoStartAtCurrent();
  }

  private isCurrentStatementStartAfter(tokens: GrlToken[]): boolean {
    const token = this.peek();
    if (this.isKeywordLike(token, "wait") || this.isKeywordLike(token, "pulse")) {
      return true;
    }
    if (!this.isIoStartAtCurrent()) {
      return false;
    }
    const previous = tokens.at(-1);
    return previous ? token.range.start.line > previous.range.end.line : true;
  }

  private isIoStartAtCurrent(): boolean {
    return this.peek().raw === "io" && this.maybePeek(1)?.raw === ".";
  }

  private isKeywordLike(token: GrlToken, keyword: string): boolean {
    return (token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword;
  }

  private matchKeyword(keyword: string): boolean {
    const token = this.peek();
    if ((token.kind === "keyword" || token.kind === "identifier") && token.raw === keyword) {
      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 consumeKeyword(keyword: string, message: string): GrlToken {
    if (this.checkKeyword(keyword)) {
      return this.advance();
    }
    throw ioError("GRL_KEYWORD_EXPECTED", message);
  }

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

  private consumePunctuation(value: string, message: string): GrlToken {
    const token = this.peek();
    if (token.kind === "punctuation" && token.raw === value) {
      return this.advance();
    }
    throw ioError("GRL_PUNCTUATION_EXPECTED", message);
  }

  private consumeOperator(value: string, message: string): GrlToken {
    const token = this.peek();
    if (token.kind === "operator" && token.raw === value) {
      return this.advance();
    }
    throw ioError("GRL_OPERATOR_EXPECTED", message);
  }

  private consume(kind: GrlToken["kind"], message: string): GrlToken {
    if (this.peek().kind === kind) {
      return this.advance();
    }
    throw ioError("GRL_TOKEN_EXPECTED", message);
  }

  private consumeNumber(message: string): Extract<GrlToken, { kind: "number" }> {
    const token = this.peek();
    if (token.kind === "number") {
      return this.advance() as Extract<GrlToken, { kind: "number" }>;
    }
    throw ioError("GRL_TOKEN_EXPECTED", message);
  }

  private consumeString(message: string): Extract<GrlToken, { kind: "string" }> {
    const token = this.peek();
    if (token.kind === "string") {
      return this.advance() as Extract<GrlToken, { kind: "string" }>;
    }
    throw ioError("GRL_TOKEN_EXPECTED", message);
  }

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

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

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

  private maybePeek(distance = 0): GrlToken | undefined {
    return this.tokens[this.current + distance];
  }

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

function compileStatementString(
  statement: string,
  _sourceMap: MotionSourceMap | undefined,
  ioMap: IoMap
): IoFlowInstruction[] {
  const tokens = lexGrl(statement, { preserveComments: false }).filter(
    (token) => token.kind !== "eof" && token.kind !== "comment"
  );
  return parseIoFlowStatements(tokens, ioMap);
}

function normalizedNumber(token: GrlToken): number {
  if (token.kind !== "number") {
    throw ioError("GRL_NUMBER_EXPECTED", "Expected number");
  }
  return token.unit?.normalizedValue ?? token.value;
}

function tokenSourceMap(token: GrlToken): MotionSourceMap {
  return {
    line: token.range.start.line,
    column: token.range.start.column
  };
}

function ioError(code: string, message: string): KdlStructuredError {
  return new KdlStructuredError(code, message);
}
