import type { MotionDiagnostic, SpeedSpec, ZoneSpec } from "../../kdl/types.js";
import type {
  ExecutableInstruction,
  MotionInstruction,
  SemanticProgramIr
} from "../ir/index.js";

export type PostBrand = "abb" | "fanuc" | "kuka";

export interface PostIssue {
  severity: MotionDiagnostic["severity"];
  code: string;
  message: string;
  brand?: PostBrand;
}

export interface PostResult {
  brand: PostBrand;
  filename: string;
  text: string;
  report: PostIssue[];
}

export interface MultiBrandPostResult {
  outputs: Record<PostBrand, PostResult>;
  report: PostIssue[];
}

export function postProcessAllBrands(ir: SemanticProgramIr): MultiBrandPostResult {
  const abb = postProcessBrand(ir, "abb");
  const fanuc = postProcessBrand(ir, "fanuc");
  const kuka = postProcessBrand(ir, "kuka");
  return {
    outputs: { abb, fanuc, kuka },
    report: [...abb.report, ...fanuc.report, ...kuka.report]
  };
}

export function postProcessBrand(ir: SemanticProgramIr, brand: PostBrand): PostResult {
  const report: PostIssue[] = collectBrandHintIssues(ir, brand);
  const text = renderBrandProgram(ir, brand, report);
  return {
    brand,
    filename: filenameFor(ir.moduleName, brand),
    text,
    report
  };
}

function renderBrandProgram(ir: SemanticProgramIr, brand: PostBrand, report: PostIssue[]): string {
  switch (brand) {
    case "abb":
      return renderAbb(ir, report);
    case "fanuc":
      return renderFanuc(ir, report);
    case "kuka":
      return renderKuka(ir, report);
  }
}

function renderAbb(ir: SemanticProgramIr, report: PostIssue[]): string {
  const lines = [`MODULE ${ir.moduleName}`, "  PROC main()"];
  for (const instruction of mainInstructions(ir)) {
    lines.push(`    ${renderAbbInstruction(instruction, report)}`);
  }
  lines.push("  ENDPROC", "ENDMODULE");
  return lines.join("\n");
}

function renderFanuc(ir: SemanticProgramIr, report: PostIssue[]): string {
  const lines = ["/PROG MAIN", "/MN"];
  mainInstructions(ir).forEach((instruction, index) => {
    lines.push(`  ${index + 1}: ${renderFanucInstruction(instruction, report)} ;`);
  });
  lines.push("/END");
  return lines.join("\n");
}

function renderKuka(ir: SemanticProgramIr, report: PostIssue[]): string {
  const lines = ["DEF Main()"];
  for (const instruction of mainInstructions(ir)) {
    lines.push(`  ${renderKukaInstruction(instruction, report)}`);
  }
  lines.push("END");
  return lines.join("\n");
}

function renderAbbInstruction(instruction: ExecutableInstruction, report: PostIssue[]): string {
  if (isMotion(instruction)) {
    const target = motionTargetName(instruction);
    const zone = abbZone(instruction.zone);
    const speed = abbSpeed(instruction.speed);
    if (instruction.kind === "MOVEJ") return `MoveJ ${target},${speed},${zone},tool0;`;
    if (instruction.kind === "MOVEL") return `MoveL ${target},${speed},${zone},tool0;`;
    return `MoveC ${motionViaName(instruction)},${target},${speed},${zone},tool0;`;
  }
  if (instruction.kind === "IO_WRITE") return `SetDO ${instruction.target.raw},${formatValue(instruction.value)};`;
  if (instruction.kind === "WAIT") return `WaitUntil ${instruction.condition};`;
  if (instruction.kind === "PULSE") return `PulseDO ${instruction.target.raw},${instruction.duration.toFixed(3)};`;
  return unsupportedLine("abb", instruction.kind, report);
}

function renderFanucInstruction(instruction: ExecutableInstruction, report: PostIssue[]): string {
  if (isMotion(instruction)) {
    const target = motionTargetName(instruction);
    const speed = fanucSpeed(instruction.speed);
    const zone = fanucZone(instruction.zone);
    if (instruction.kind === "MOVEJ") return `J ${target} ${speed} ${zone}`;
    if (instruction.kind === "MOVEL") return `L ${target} ${speed} ${zone}`;
    return `C ${motionViaName(instruction)} ${target} ${speed} ${zone}`;
  }
  if (instruction.kind === "IO_WRITE") return `${fanucIo(instruction.target.raw)}=${formatValue(instruction.value)}`;
  if (instruction.kind === "WAIT") return `WAIT (${instruction.condition})`;
  if (instruction.kind === "PULSE") return `PULSE ${fanucIo(instruction.target.raw)} ${Math.round(instruction.duration * 1000)}ms`;
  return unsupportedLine("fanuc", instruction.kind, report);
}

function renderKukaInstruction(instruction: ExecutableInstruction, report: PostIssue[]): string {
  if (isMotion(instruction)) {
    const target = motionTargetName(instruction);
    const speed = kukaSpeed(instruction.speed);
    const zone = kukaZone(instruction.zone);
    if (instruction.kind === "MOVEJ") return `PTP ${target} ${speed}${zone}`;
    if (instruction.kind === "MOVEL") return `LIN ${target} ${speed}${zone}`;
    return `CIRC ${motionViaName(instruction)}, ${target} ${speed}${zone}`;
  }
  if (instruction.kind === "IO_WRITE") return `${kukaIo(instruction.target.raw)} = ${formatValue(instruction.value)}`;
  if (instruction.kind === "WAIT") return `WAIT FOR ${instruction.condition}`;
  if (instruction.kind === "PULSE") return `PULSE ${kukaIo(instruction.target.raw)} ${instruction.duration.toFixed(3)}`;
  return unsupportedLine("kuka", instruction.kind, report);
}

function mainInstructions(ir: SemanticProgramIr): ExecutableInstruction[] {
  return ir.procedures.find((procedure) => procedure.name === "main")?.instructions ?? [];
}

function isMotion(instruction: ExecutableInstruction): instruction is MotionInstruction {
  return instruction.kind === "MOVEJ" || instruction.kind === "MOVEL" || instruction.kind === "MOVEC";
}

function motionTargetName(instruction: MotionInstruction): string {
  const target = instruction.target;
  if (target && "id" in target && target.id) {
    return target.id;
  }
  return instruction.pointId ?? instruction.id ?? "p_auto";
}

function motionViaName(instruction: MotionInstruction): string {
  const via = instruction.via;
  if (via && "id" in via && via.id) {
    return via.id;
  }
  return "via_auto";
}

function abbSpeed(speed: SpeedSpec): string {
  if (speed.kind === "joint_percent") return `v${Math.round(speed.value * 100)}`;
  if (speed.kind === "linear") return `v${Math.round(speed.velocity * 1000)}`;
  return "v100";
}

function fanucSpeed(speed: SpeedSpec): string {
  if (speed.kind === "joint_percent") return `${Math.round(speed.value * 100)}%`;
  if (speed.kind === "linear") return `${Math.round(speed.velocity * 1000)}mm/sec`;
  return "100mm/sec";
}

function kukaSpeed(speed: SpeedSpec): string {
  if (speed.kind === "joint_percent") return `Vel=${Math.round(speed.value * 100)}%`;
  if (speed.kind === "linear") return `Vel=${speed.velocity.toFixed(3)}m/s`;
  return "Vel=0.100m/s";
}

function abbZone(zone: ZoneSpec): string {
  if (zone.kind === "fine") return "fine";
  if (zone.kind === "distance") return `z${Math.round(zone.value * 1000)}`;
  if (zone.kind === "cnt") return `z${Math.round(zone.value * 100)}`;
  return "z10";
}

function fanucZone(zone: ZoneSpec): string {
  if (zone.kind === "fine") return "FINE";
  if (zone.kind === "cnt") return `CNT${Math.round(zone.value * 100)}`;
  if (zone.kind === "distance") return `CNT${Math.max(1, Math.round(zone.value * 1000))}`;
  return "CNT10";
}

function kukaZone(zone: ZoneSpec): string {
  return zone.kind === "fine" ? "" : " C_DIS";
}

function fanucIo(raw: string): string {
  return raw.replace("io.do", "DO").replace("io.di", "DI").replace("[", "[").replace("]", "]");
}

function kukaIo(raw: string): string {
  return raw.replace("io.do", "$OUT").replace("io.di", "$IN");
}

function formatValue(value: boolean | number | string): string {
  if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
  return String(value);
}

function unsupportedLine(brand: PostBrand, kind: string, report: PostIssue[]): string {
  report.push({
    severity: "warning",
    code: "GRL_POST_UNSUPPORTED",
    message: `${kind} is not supported by ${brand} prototype postprocessor`,
    brand
  });
  return `! unsupported ${kind}`;
}

function collectBrandHintIssues(ir: SemanticProgramIr, brand: PostBrand): PostIssue[] {
  return ir.symbols
    .filter((symbol) => symbol.kind === "raw" && symbol.typeName === "post_hint")
    .filter((symbol) => !symbol.name.includes(brand))
    .map((symbol) => ({
      severity: "info" as const,
      code: "GRL_POST_HINT_IGNORED",
      message: `post_hint ${symbol.name} ignored for ${brand}`,
      brand
    }));
}

function filenameFor(moduleName: string, brand: PostBrand): string {
  if (brand === "abb") return `${moduleName}.mod`;
  if (brand === "fanuc") return `${moduleName}.ls`;
  return `${moduleName}.src`;
}
