import type { IoReference, PulseInstruction, WaitInstruction } from "../grl/ir/index.js";
import type { RuntimeTraceEvent } from "../controller/index.js";
import { evaluateRuntimeBoolean } from "../controller/index.js";

export type IoWriter = "program" | "user" | "script";
export type IoEdge = "rising" | "falling" | "changed";

export interface IoEvent {
  time: number;
  writer: IoWriter;
  key: string;
  previous: unknown;
  value: unknown;
  edge?: IoEdge;
}

export interface IoPermissionRule {
  key: string;
  writers: IoWriter[];
}

export class IoImage {
  private readonly values = new Map<string, unknown>();
  private readonly eventsValue: IoEvent[] = [];
  private readonly rules = new Map<string, Set<IoWriter>>();

  setPermission(key: string, writers: IoWriter[]): void {
    this.rules.set(key, new Set(writers));
  }

  read(ref: IoReference | string): unknown {
    return this.values.get(ioKey(ref));
  }

  write(ref: IoReference | string, value: unknown, writer: IoWriter, time = 0): IoEvent {
    const key = ioKey(ref);
    const allowed = this.rules.get(key);
    if (allowed && !allowed.has(writer)) {
      throw new Error(`Writer ${writer} cannot write ${key}`);
    }
    const previous = this.values.get(key);
    this.values.set(key, value);
    const edge = edgeOf(previous, value);
    const event: IoEvent = {
      time,
      writer,
      key,
      previous,
      value,
      ...(edge ? { edge } : {})
    };
    this.eventsValue.push(event);
    return event;
  }

  variables(): Record<string, unknown> {
    return Object.fromEntries([...this.values.entries()]);
  }

  events(): IoEvent[] {
    return this.eventsValue.map((event) => ({ ...event }));
  }
}

export interface WaitResult {
  status: "satisfied" | "waiting" | "timeout" | "hold" | "stop";
  condition: string;
  elapsed: number;
  trace?: RuntimeTraceEvent;
}

export class WaitRegistry {
  evaluate(wait: WaitInstruction, io: IoImage, elapsed: number): WaitResult {
    if (evaluateRuntimeBoolean(wait.condition, io.variables())) {
      return { status: "satisfied", condition: wait.condition, elapsed };
    }
    if (wait.timeout !== undefined && elapsed >= wait.timeout) {
      if (wait.onTimeout?.kind === "alarm") {
        return { status: "hold", condition: wait.condition, elapsed };
      }
      if (wait.onTimeout?.kind === "call") {
        return { status: "stop", condition: wait.condition, elapsed };
      }
      return { status: "timeout", condition: wait.condition, elapsed };
    }
    return { status: "waiting", condition: wait.condition, elapsed };
  }
}

export function applyPulse(io: IoImage, pulse: PulseInstruction, writer: IoWriter = "program", startTime = 0): IoEvent[] {
  return [
    io.write(pulse.target, true, writer, startTime),
    io.write(pulse.target, false, writer, startTime + pulse.duration)
  ];
}

export interface IoScriptStep {
  delay: number;
  target: IoReference | string;
  value: unknown;
}

export function runIoScript(io: IoImage, script: IoScriptStep[], startTime = 0): IoEvent[] {
  let time = startTime;
  return script.map((step) => {
    time += step.delay;
    return io.write(step.target, step.value, "script", time);
  });
}

function ioKey(ref: IoReference | string): string {
  if (typeof ref === "string") {
    return ref;
  }
  return ref.raw;
}

function edgeOf(previous: unknown, value: unknown): IoEdge | undefined {
  if (previous === value) {
    return undefined;
  }
  if (previous === false && value === true) {
    return "rising";
  }
  if (previous === true && value === false) {
    return "falling";
  }
  return "changed";
}
