import type {
  CompiledOperation,
  CompiledPath,
  ExecutableInstruction,
  OperationActionInstruction,
  RunOperationInstruction,
  RunPathInstruction,
  SemanticProgramIr
} from "../grl/ir/index.js";
import { expandRunOperation } from "../grl/semantic/index.js";
import type { MotionDiagnostic } from "../kdl/types.js";
import { MotionQueue, type MotionQueueItem } from "./motionQueue.js";

export interface KdlBridgeSnapshot {
  pathIds: string[];
  operationIds: string[];
  diagnostics: MotionDiagnostic[];
}

export class KdlRuntimeBridge {
  private readonly paths = new Map<string, CompiledPath>();
  private readonly operations = new Map<string, CompiledOperation>();
  private readonly diagnosticsValue: MotionDiagnostic[] = [];

  constructor(
    private readonly program: SemanticProgramIr,
    private readonly queue: MotionQueue
  ) {
    for (const path of program.paths) {
      this.paths.set(path.pathId, path);
    }
    for (const operation of program.operations) {
      this.operations.set(operation.operationId, operation);
    }
  }

  get diagnostics(): MotionDiagnostic[] {
    return [...this.diagnosticsValue];
  }

  enqueueInstruction(instruction: ExecutableInstruction): MotionQueueItem[] {
    if (instruction.kind === "RUN_PATH") {
      return this.enqueueRunPath(instruction);
    }
    if (instruction.kind === "RUN_OPERATION") {
      return this.enqueueRunOperation(instruction);
    }
    if (instruction.kind === "MOVEJ" || instruction.kind === "MOVEL" || instruction.kind === "MOVEC") {
      return [this.queue.enqueueMotion(instruction)];
    }
    return [];
  }

  enqueueRunPath(instruction: RunPathInstruction): MotionQueueItem[] {
    const path = this.paths.get(instruction.pathId);
    if (!path) {
      this.addDiagnostic("VC_PATH_NOT_FOUND", `Path ${instruction.pathId} was not found`);
      return [];
    }
    return [this.queue.enqueueRunPath(instruction, path)];
  }

  enqueueRunOperation(instruction: RunOperationInstruction): MotionQueueItem[] {
    const operation = this.operations.get(instruction.operationId);
    if (!operation) {
      this.addDiagnostic("VC_OPERATION_NOT_FOUND", `Operation ${instruction.operationId} was not found`);
      return [];
    }
    const items: MotionQueueItem[] = [];
    for (const step of expandRunOperation(instruction, this.operations)) {
      if (step.kind === "ACTION") {
        items.push(this.queue.enqueueOperationAction(step));
      } else {
        const path = this.paths.get(step.pathId);
        if (!path) {
          this.addDiagnostic("VC_PATH_NOT_FOUND", `Path ${step.pathId} was not found for operation ${operation.operationId}`);
          continue;
        }
        items.push(this.queue.enqueueRunPath(withOperationSource(step, operation), path));
      }
    }
    return items;
  }

  snapshot(): KdlBridgeSnapshot {
    return {
      pathIds: [...this.paths.keys()],
      operationIds: [...this.operations.keys()],
      diagnostics: this.diagnostics
    };
  }

  private addDiagnostic(code: string, message: string): void {
    this.diagnosticsValue.push({
      severity: "error",
      code,
      message
    });
  }
}

function withOperationSource(step: RunPathInstruction, operation: CompiledOperation): RunPathInstruction {
  const instruction: RunPathInstruction = {
    ...step,
    ...(step.sourceMap ? { sourceMap: step.sourceMap } : {})
  };
  Object.defineProperty(instruction, "__operationId", {
    value: operation.operationId,
    enumerable: false
  });
  return instruction;
}

export function operationIdFromAction(action: OperationActionInstruction): string {
  return action.operationId;
}
