import type { SemanticProgramIr } from "../grl/ir/index.js";
import type { MotionSourceMap, TrajectoryPoint } from "../kdl/types.js";
import type { IoImageRuntime } from "../runtime/ioRuntime.js";
import type { MotionQueue, MotionQueueItem, MotionPlaybackSample } from "../runtime/motionQueue.js";
import type { IrExecutionRuntime, RuntimeBreakpoint } from "../controller/runtime.js";
import { RuntimeSourceMapIndex, type RuntimeSourceLocation } from "../controller/sourceMap.js";
import type { RuntimeTraceEvent } from "../controller/trace.js";

export interface MotionBreakpoint {
  id: string;
  pathId?: string;
  pointId?: string;
  operationId?: string;
  enabled: boolean;
}

export interface VariableWatch {
  id: string;
  expression: string;
  value?: unknown;
  source?: RuntimeSourceLocation;
}

export interface DebugFacadeSnapshot {
  breakpoints: RuntimeBreakpoint[];
  motionBreakpoints: MotionBreakpoint[];
  watches: VariableWatch[];
  currentSource?: RuntimeSourceLocation;
  trace: RuntimeTraceEvent[];
  playback?: MotionPlaybackSample;
}

export interface DebugFacadeOptions {
  runtime: IrExecutionRuntime;
  motionQueue?: MotionQueue;
  io?: IoImageRuntime;
  program?: SemanticProgramIr;
}

export class DebugFacade {
  private readonly runtime: IrExecutionRuntime;
  private readonly motionQueue: MotionQueue | undefined;
  private readonly io: IoImageRuntime | undefined;
  private readonly sourceIndex: RuntimeSourceMapIndex;
  private readonly motionBreakpointsValue = new Map<string, MotionBreakpoint>();
  private readonly watchesValue = new Map<string, VariableWatch>();

  constructor(options: DebugFacadeOptions) {
    this.runtime = options.runtime;
    this.motionQueue = options.motionQueue;
    this.io = options.io;
    this.sourceIndex = options.program ? new RuntimeSourceMapIndex(options.program) : this.runtime.sourceMapIndex;
  }

  addBreakpoint(breakpoint: RuntimeBreakpoint): void {
    this.runtime.setBreakpoint(breakpoint);
  }

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

  addMotionBreakpoint(breakpoint: MotionBreakpoint): void {
    this.motionBreakpointsValue.set(breakpoint.id, { ...breakpoint });
  }

  removeMotionBreakpoint(id: string): boolean {
    return this.motionBreakpointsValue.delete(id);
  }

  checkMotionBreakpoint(item: MotionQueueItem): MotionBreakpoint | undefined {
    return [...this.motionBreakpointsValue.values()].find((breakpoint) => {
      if (!breakpoint.enabled) {
        return false;
      }
      if (breakpoint.pathId && breakpoint.pathId !== item.source.pathId) {
        return false;
      }
      if (breakpoint.pointId && breakpoint.pointId !== item.source.pointId) {
        return false;
      }
      if (breakpoint.operationId && breakpoint.operationId !== item.source.operationId) {
        return false;
      }
      return breakpoint.pathId !== undefined || breakpoint.pointId !== undefined || breakpoint.operationId !== undefined;
    });
  }

  addWatch(id: string, expression: string): VariableWatch {
    const source = this.runtime.currentSource();
    const watch: VariableWatch = {
      id,
      expression,
      value: this.evaluateWatchExpression(expression),
      ...(source ? { source } : {})
    };
    this.watchesValue.set(id, watch);
    return { ...watch };
  }

  updateWatches(): VariableWatch[] {
    const source = this.runtime.currentSource();
    for (const watch of this.watchesValue.values()) {
      watch.value = this.evaluateWatchExpression(watch.expression);
      if (source) {
        watch.source = source;
      } else {
        delete watch.source;
      }
    }
    return this.watches();
  }

  watches(): VariableWatch[] {
    return [...this.watchesValue.values()].map((watch) => ({ ...watch }));
  }

  locateSource(sourceMap: MotionSourceMap): RuntimeSourceLocation[] {
    return this.sourceIndex.locateSource(sourceMap);
  }

  locatePathPoint(pathId: string, pointId: string): RuntimeSourceLocation | undefined {
    return this.sourceIndex.locatePathPoint(pathId, pointId);
  }

  locateOperation(operationId: string): RuntimeSourceLocation[] {
    return this.sourceIndex.locateOperation(operationId);
  }

  playbackAt(time: number): MotionPlaybackSample | undefined {
    return this.motionQueue?.sampleAt(time);
  }

  replayTrace(): RuntimeTraceEvent[] {
    return this.runtime.snapshot().trace;
  }

  currentPoint(): TrajectoryPoint | undefined {
    return this.motionQueue?.sampleAt(this.motionQueue.virtualTime)?.point;
  }

  snapshot(): DebugFacadeSnapshot {
    const playback = this.motionQueue?.sampleAt(this.motionQueue.virtualTime);
    const currentSource = this.runtime.currentSource();
    return {
      breakpoints: this.runtime.listBreakpoints(),
      motionBreakpoints: [...this.motionBreakpointsValue.values()].map((breakpoint) => ({ ...breakpoint })),
      watches: this.updateWatches(),
      ...(currentSource ? { currentSource } : {}),
      trace: this.runtime.snapshot().trace,
      ...(playback ? { playback } : {})
    };
  }

  private evaluateWatchExpression(expression: string): unknown {
    if (expression.startsWith("io.")) {
      const key = expression.replace(/\s+/g, "");
      return this.io?.snapshot().image[key];
    }
    return this.runtime.getVariable(expression);
  }
}
