import type {
  AlarmInstruction,
  CallInstruction,
  ExecutableBranch,
  ExecutableInstruction,
  ExecutableProcedure,
  ReturnInstruction,
  SemanticProgramIr
} from "../grl/ir/index.js";
import type { MotionDiagnostic } from "../kdl/types.js";
import { evaluateRuntimeBoolean, evaluateRuntimeExpression, assignRuntimeExpression } from "./expression.js";
import { RuntimeSourceMapIndex, type RuntimeSourceLocation } from "./sourceMap.js";
import { TraceBuffer, type RuntimeTraceEvent } from "./trace.js";

export interface RuntimeBreakpoint {
  id: string;
  procedure?: string;
  pc?: number;
  source?: {
    file?: string;
    line?: number;
    column?: number;
  };
  enabled: boolean;
}

export interface RuntimeExecutionFrame {
  procedure: string;
  pc: number;
  returnTo?: {
    procedure: string;
    pc: number;
  };
}

export interface RuntimeScopeFrame {
  id: string;
  variables: Record<string, unknown>;
}

export interface RuntimeAlarm {
  id: string;
  message: string;
  severity: "info" | "warning" | "error";
  source?: RuntimeSourceLocation;
  time: number;
}

export type RuntimeStepStatus = "executed" | "blocked" | "completed" | "breakpoint";

export interface RuntimeStepResult {
  status: RuntimeStepStatus;
  instruction?: ExecutableInstruction;
  trace?: RuntimeTraceEvent;
  diagnostic?: MotionDiagnostic;
}

export interface IrExecutionRuntimeSnapshot {
  loaded: boolean;
  procedure?: string;
  pc: number;
  callStack: RuntimeExecutionFrame[];
  scopeStack: RuntimeScopeFrame[];
  alarmQueue: RuntimeAlarm[];
  trace: RuntimeTraceEvent[];
  currentSource?: RuntimeSourceLocation;
  completed: boolean;
}

export interface RuntimeInstructionHooks {
  onInstruction?: (instruction: ExecutableInstruction, runtime: IrExecutionRuntime) => RuntimeStepResult | void;
  onWait?: (instruction: Extract<ExecutableInstruction, { kind: "WAIT" }>, runtime: IrExecutionRuntime) => RuntimeStepResult | void;
  onMotion?: (instruction: Extract<ExecutableInstruction, { kind: "MOVEJ" | "MOVEL" | "MOVEC" | "RUN_PATH" | "RUN_OPERATION" }>, runtime: IrExecutionRuntime) => RuntimeStepResult | void;
  onIo?: (instruction: Extract<ExecutableInstruction, { kind: "IO_WRITE" | "PULSE" }>, runtime: IrExecutionRuntime) => RuntimeStepResult | void;
}

export interface IrExecutionRuntimeOptions {
  entryProcedure?: string;
  traceCapacity?: number;
  hooks?: RuntimeInstructionHooks;
}

export class IrExecutionRuntime {
  private programValue: SemanticProgramIr | undefined;
  private readonly procedures = new Map<string, ExecutableProcedure>();
  private readonly traceBuffer: TraceBuffer;
  private readonly scopes: RuntimeScopeFrame[] = [];
  private readonly alarms: RuntimeAlarm[] = [];
  private readonly breakpoints = new Map<string, RuntimeBreakpoint>();
  private frame: RuntimeExecutionFrame | undefined;
  private completedValue = false;
  private sourceIndex = new RuntimeSourceMapIndex();
  private virtualTimeValue = 0;

  constructor(private readonly options: IrExecutionRuntimeOptions = {}) {
    this.traceBuffer = new TraceBuffer(options.traceCapacity ?? 1000);
  }

  get program(): SemanticProgramIr | undefined {
    return this.programValue;
  }

  get virtualTime(): number {
    return this.virtualTimeValue;
  }

  set virtualTime(value: number) {
    this.virtualTimeValue = value;
  }

  get loaded(): boolean {
    return this.programValue !== undefined;
  }

  get completed(): boolean {
    return this.completedValue;
  }

  get currentFrame(): RuntimeExecutionFrame | undefined {
    return this.frame ? { ...this.frame, ...(this.frame.returnTo ? { returnTo: { ...this.frame.returnTo } } : {}) } : undefined;
  }

  get currentProcedure(): ExecutableProcedure | undefined {
    return this.frame ? this.procedures.get(this.frame.procedure) : undefined;
  }

  get currentInstruction(): ExecutableInstruction | undefined {
    const procedure = this.currentProcedure;
    if (!procedure || !this.frame) {
      return undefined;
    }
    return procedure.instructions[this.frame.pc];
  }

  get sourceMapIndex(): RuntimeSourceMapIndex {
    return this.sourceIndex;
  }

  load(program: SemanticProgramIr, entryProcedure = this.options.entryProcedure ?? "main"): void {
    this.programValue = program;
    this.procedures.clear();
    for (const procedure of program.procedures) {
      this.procedures.set(procedure.name, procedure);
    }
    if (!this.procedures.has(entryProcedure)) {
      throw new Error(`Entry procedure ${entryProcedure} was not found`);
    }
    this.sourceIndex = new RuntimeSourceMapIndex(program);
    this.reset(entryProcedure);
    this.trace("load", { message: `Loaded ${program.moduleName}`, data: { moduleName: program.moduleName } });
  }

  reset(entryProcedure = this.options.entryProcedure ?? "main"): void {
    if (!this.procedures.has(entryProcedure)) {
      throw new Error(`Entry procedure ${entryProcedure} was not found`);
    }
    this.frame = { procedure: entryProcedure, pc: 0 };
    this.scopes.length = 0;
    this.scopes.push({ id: "global", variables: {} });
    this.scopes.push({ id: entryProcedure, variables: {} });
    this.alarms.length = 0;
    this.completedValue = false;
    this.virtualTimeValue = 0;
    this.traceBuffer.clear();
  }

  setVariable(name: string, value: unknown, scopeId?: string): void {
    const scope = scopeId ? this.scopes.find((candidate) => candidate.id === scopeId) : this.scopes.at(-1);
    if (!scope) {
      throw new Error(`Scope ${scopeId ?? "<current>"} was not found`);
    }
    scope.variables[name] = value;
  }

  getVariable(name: string): unknown {
    for (let index = this.scopes.length - 1; index >= 0; index -= 1) {
      const scope = this.scopes[index]!;
      if (Object.prototype.hasOwnProperty.call(scope.variables, name)) {
        return scope.variables[name];
      }
    }
    return undefined;
  }

  variables(): Record<string, unknown> {
    return Object.assign({}, ...this.scopes.map((scope) => scope.variables));
  }

  setBreakpoint(breakpoint: RuntimeBreakpoint): void {
    this.breakpoints.set(breakpoint.id, breakpoint);
  }

  removeBreakpoint(id: string): boolean {
    return this.breakpoints.delete(id);
  }

  listBreakpoints(): RuntimeBreakpoint[] {
    return [...this.breakpoints.values()].map((breakpoint) => ({ ...breakpoint }));
  }

  step(): RuntimeStepResult {
    if (!this.frame || this.completedValue) {
      this.completedValue = true;
      return { status: "completed" };
    }

    const procedure = this.procedures.get(this.frame.procedure);
    if (!procedure) {
      return this.fail(`Procedure ${this.frame.procedure} was not found`, "VC_PROCEDURE_NOT_FOUND");
    }

    if (this.frame.pc >= procedure.instructions.length) {
      return this.returnFromProcedure();
    }

    const instruction = procedure.instructions[this.frame.pc]!;
    const breakpoint = this.matchBreakpoint(instruction);
    if (breakpoint) {
      const trace = this.trace("breakpoint", {
        instruction,
        message: `Breakpoint ${breakpoint.id} hit`,
        data: { breakpointId: breakpoint.id }
      });
      return { status: "breakpoint", instruction, trace };
    }

    const hookResult = this.runHook(instruction);
    if (hookResult) {
      if (hookResult.status === "blocked" || hookResult.status === "breakpoint") {
        return hookResult;
      }
      if (hookResult.status === "completed") {
        this.completedValue = true;
        return hookResult;
      }
    }

    const trace = this.execute(instruction);
    return {
      status: this.completedValue ? "completed" : "executed",
      instruction,
      trace
    };
  }

  run(maxSteps = 10_000): RuntimeStepResult {
    let result: RuntimeStepResult = { status: "completed" };
    for (let index = 0; index < maxSteps; index += 1) {
      result = this.step();
      if (result.status !== "executed") {
        return result;
      }
    }
    return this.fail(`Execution exceeded ${maxSteps} steps`, "VC_STEP_LIMIT_EXCEEDED");
  }

  pushAlarm(alarmId: string, message = alarmId, severity: RuntimeAlarm["severity"] = "error", source?: RuntimeSourceLocation): RuntimeAlarm {
    const alarm: RuntimeAlarm = {
      id: alarmId,
      message,
      severity,
      ...(source ? { source } : {}),
      time: this.virtualTimeValue
    };
    this.alarms.push(alarm);
    this.trace("alarm", {
      message,
      data: { alarmId, severity },
      ...(source ? { source } : {})
    });
    return alarm;
  }

  trace(kind: RuntimeTraceEvent["kind"], input: {
    instruction?: ExecutableInstruction;
    message?: string;
    data?: Record<string, unknown>;
    source?: RuntimeSourceLocation;
    diagnostic?: MotionDiagnostic;
  } = {}): RuntimeTraceEvent {
    const source = input.source ?? (input.instruction ? this.locateInstruction(input.instruction) : this.currentSource());
    return this.traceBuffer.push({
      time: this.virtualTimeValue,
      kind,
      ...(this.frame ? { procedure: this.frame.procedure, pc: this.frame.pc } : {}),
      ...(input.instruction ? { instructionKind: input.instruction.kind } : {}),
      ...(input.message ? { message: input.message } : {}),
      ...(source ? { source } : {}),
      ...(input.diagnostic ? { diagnostic: input.diagnostic } : {}),
      ...(input.data ? { data: input.data } : {})
    });
  }

  currentSource(): RuntimeSourceLocation | undefined {
    const instruction = this.currentInstruction;
    return instruction ? this.locateInstruction(instruction) : undefined;
  }

  locateInstruction(instruction: ExecutableInstruction): RuntimeSourceLocation | undefined {
    return this.sourceIndex.locateByInstruction({
      kind: instruction.kind,
      ...(instruction.sourceMap ? { sourceMap: instruction.sourceMap } : {}),
      ...(this.frame?.procedure ? { procedureId: this.frame.procedure } : {})
    });
  }

  snapshot(): IrExecutionRuntimeSnapshot {
    const currentSource = this.currentSource();
    return {
      loaded: this.loaded,
      ...(this.frame ? { procedure: this.frame.procedure } : {}),
      pc: this.frame?.pc ?? 0,
      callStack: this.frame ? [this.frame] : [],
      scopeStack: this.scopes.map((scope) => ({ id: scope.id, variables: { ...scope.variables } })),
      alarmQueue: this.alarms.map((alarm) => ({ ...alarm })),
      trace: this.traceBuffer.all(),
      ...(currentSource ? { currentSource } : {}),
      completed: this.completedValue
    };
  }

  private execute(instruction: ExecutableInstruction): RuntimeTraceEvent {
    switch (instruction.kind) {
      case "CALL":
        return this.executeCall(instruction);
      case "RETURN":
        return this.executeReturn(instruction);
      case "ALARM":
        this.pushAlarm(
          instruction.alarmId,
          instruction.message ?? instruction.alarmId,
          normalizeAlarmSeverity(instruction.severity),
          this.locateInstruction(instruction)
        );
        this.advance();
        return this.trace("instruction", { instruction });
      case "RAISE":
        this.pushAlarm(instruction.alarmId, instruction.alarmId, "error", this.locateInstruction(instruction));
        this.advance();
        return this.trace("instruction", { instruction });
      case "EXEC_IF":
        return this.executeIf(instruction.branches, instruction);
      case "EXEC_WHILE":
        if (evaluateRuntimeBoolean(instruction.condition.text, this.variables())) {
          this.insertInlineBlock(instruction.body, "while");
        }
        this.advance();
        return this.trace("instruction", { instruction });
      case "EXEC_FOR":
        this.setVariable(instruction.iterator, evaluateRuntimeExpression(instruction.from.text, this.variables()));
        this.insertInlineBlock(instruction.body, "for");
        this.advance();
        return this.trace("instruction", { instruction });
      case "EXEC_SWITCH":
        this.insertInlineBlock(instruction.cases[0]?.body ?? [], "switch");
        this.advance();
        return this.trace("instruction", { instruction });
      case "RAW_STATEMENT":
        assignRuntimeExpression(instruction.text, this.scopes.at(-1)!.variables);
        this.advance();
        return this.trace("instruction", { instruction });
      case "BREAK":
      case "CONTINUE":
      case "UNSUPPORTED_RUNTIME":
        this.advance();
        return this.trace("diagnostic", {
          instruction,
          diagnostic: {
            severity: instruction.kind === "UNSUPPORTED_RUNTIME" ? "warning" : "info",
            code: `VC_${instruction.kind}`,
            message: instruction.kind === "UNSUPPORTED_RUNTIME" ? instruction.message : `${instruction.kind} reached`,
            ...(instruction.sourceMap ? { sourceMap: instruction.sourceMap } : {})
          }
        });
      default:
        this.advance();
        return this.trace(instruction.kind === "IO_WRITE" || instruction.kind === "PULSE" ? "io" : "instruction", { instruction });
    }
  }

  private executeCall(instruction: CallInstruction): RuntimeTraceEvent {
    const target = this.procedures.get(instruction.target);
    if (!target) {
      const result = this.fail(`Procedure ${instruction.target} was not found`, "VC_CALL_TARGET_NOT_FOUND");
      return this.trace("diagnostic", {
        instruction,
        ...(result.diagnostic ? { diagnostic: result.diagnostic } : {})
      });
    }
    const previous = this.frame!;
    this.frame = {
      procedure: target.name,
      pc: 0,
      returnTo: {
        procedure: previous.procedure,
        pc: previous.pc + 1
      }
    };
    this.scopes.push({ id: target.name, variables: {} });
    return this.trace("instruction", { instruction, message: `Call ${target.name}` });
  }

  private executeReturn(instruction: ReturnInstruction): RuntimeTraceEvent {
    if (instruction.value) {
      this.setVariable("$return", evaluateRuntimeExpression(instruction.value.text, this.variables()));
    }
    const trace = this.trace("instruction", { instruction });
    this.returnFromProcedure();
    return trace;
  }

  private executeIf(branches: ExecutableBranch[], instruction: ExecutableInstruction): RuntimeTraceEvent {
    const branch = branches.find((candidate) => {
      if (candidate.branchKind === "else") {
        return true;
      }
      return candidate.condition ? evaluateRuntimeBoolean(candidate.condition.text, this.variables()) : false;
    });
    if (branch) {
      this.insertInlineBlock(branch.body, branch.branchKind);
    }
    this.advance();
    return this.trace("instruction", { instruction });
  }

  private insertInlineBlock(body: ExecutableInstruction[], scopeId: string): void {
    const procedure = this.currentProcedure;
    if (!procedure || !this.frame || body.length === 0) {
      return;
    }
    procedure.instructions.splice(this.frame.pc + 1, 0, ...body);
    this.scopes.push({ id: `${scopeId}:${this.frame.pc}`, variables: {} });
  }

  private runHook(instruction: ExecutableInstruction): RuntimeStepResult | undefined {
    const hook = this.options.hooks;
    const genericResult = hook?.onInstruction?.(instruction, this);
    if (genericResult) {
      return genericResult;
    }
    if (instruction.kind === "WAIT") {
      const result = hook?.onWait?.(instruction, this);
      return result ?? undefined;
    }
    if (instruction.kind === "IO_WRITE" || instruction.kind === "PULSE") {
      const result = hook?.onIo?.(instruction, this);
      return result ?? undefined;
    }
    if (
      instruction.kind === "MOVEJ" ||
      instruction.kind === "MOVEL" ||
      instruction.kind === "MOVEC" ||
      instruction.kind === "RUN_PATH" ||
      instruction.kind === "RUN_OPERATION"
    ) {
      const result = hook?.onMotion?.(instruction, this);
      return result ?? undefined;
    }
    return undefined;
  }

  private matchBreakpoint(instruction: ExecutableInstruction): RuntimeBreakpoint | undefined {
    const source = instruction.sourceMap;
    return [...this.breakpoints.values()].find((breakpoint) => {
      if (!breakpoint.enabled) {
        return false;
      }
      if (breakpoint.procedure && breakpoint.procedure !== this.frame?.procedure) {
        return false;
      }
      if (breakpoint.pc !== undefined && breakpoint.pc !== this.frame?.pc) {
        return false;
      }
      if (breakpoint.source?.line !== undefined && breakpoint.source.line !== source?.line) {
        return false;
      }
      if (breakpoint.source?.file !== undefined && breakpoint.source.file !== source?.file) {
        return false;
      }
      return breakpoint.pc !== undefined || breakpoint.source !== undefined;
    });
  }

  private advance(): void {
    if (this.frame) {
      this.frame.pc += 1;
    }
  }

  private returnFromProcedure(): RuntimeStepResult {
    if (!this.frame?.returnTo) {
      this.completedValue = true;
      return { status: "completed" };
    }
    const returnTo = this.frame.returnTo;
    this.scopes.pop();
    this.frame = {
      procedure: returnTo.procedure,
      pc: returnTo.pc
    };
    return { status: "executed" };
  }

  private fail(message: string, code: string): RuntimeStepResult {
    const diagnostic: MotionDiagnostic = {
      severity: "error",
      code,
      message
    };
    this.trace("diagnostic", { diagnostic, message });
    return {
      status: "blocked",
      diagnostic
    };
  }
}

function normalizeAlarmSeverity(severity?: string): RuntimeAlarm["severity"] {
  return severity === "info" || severity === "warning" || severity === "error" ? severity : "error";
}
