export type GrlGeneratorStyle = "expanded" | "compact";

export interface GeneratedTargetSpec {
  name: string;
  kind: "joint" | "pose";
  values: number[];
}

export interface GeneratedPathPointSpec {
  id?: string;
  motion: "movej" | "movel" | "movec";
  target: string;
  via?: string;
  speed?: string;
  zone?: string;
}

export interface GeneratedPathSpec {
  name: string;
  source?: Record<string, string | number | boolean>;
  defaults: {
    speed: string;
    zone: string;
  };
  points: GeneratedPathPointSpec[];
}

export interface GeneratedOperationSpec {
  name: string;
  kind: string;
  path: string;
  startAction?: string;
  endAction?: string;
}

export interface GrlProgramGenerationSpec {
  moduleName: string;
  speeds?: Record<string, string>;
  zones?: Record<string, string>;
  targets: GeneratedTargetSpec[];
  path: GeneratedPathSpec;
  operation: GeneratedOperationSpec;
}

export interface GeneratedGrlProgram {
  text: string;
  stableIds: {
    targets: string[];
    points: string[];
    path: string;
    operation: string;
  };
}

export function generateGrlProgram(spec: GrlProgramGenerationSpec, style: GrlGeneratorStyle = "expanded"): GeneratedGrlProgram {
  const normalized = normalizeSpec(spec);
  const text = style === "compact" ? renderCompact(normalized) : renderExpanded(normalized);
  return {
    text,
    stableIds: {
      targets: normalized.targets.map((target) => target.name),
      points: normalized.path.points.map((point) => point.id!),
      path: normalized.path.name,
      operation: normalized.operation.name
    }
  };
}

function normalizeSpec(spec: GrlProgramGenerationSpec): GrlProgramGenerationSpec {
  return {
    ...spec,
    speeds: sortRecord(spec.speeds ?? {}),
    zones: sortRecord(spec.zones ?? {}),
    targets: [...spec.targets].sort((left, right) => left.name.localeCompare(right.name)),
    path: {
      ...spec.path,
      ...(spec.path.source ? { source: sortRecord(spec.path.source) } : {}),
      points: spec.path.points.map((point, index) => ({
        ...point,
        id: point.id ?? `p${String(index).padStart(2, "0")}`
      }))
    }
  };
}

function renderExpanded(spec: GrlProgramGenerationSpec): string {
  const lines: string[] = [`language grl 0.1`, `module ${spec.moduleName}`];
  for (const [name, expression] of Object.entries(spec.speeds ?? {})) {
    lines.push(`  const speed ${name} = ${expression}`);
  }
  for (const [name, expression] of Object.entries(spec.zones ?? {})) {
    lines.push(`  const zone ${name} = ${expression}`);
  }
  for (const target of spec.targets) {
    lines.push(...renderTargetExpanded(target));
  }
  lines.push(`  path ${spec.path.name} {`);
  if (spec.path.source && Object.keys(spec.path.source).length > 0) {
    lines.push(`    source {`);
    for (const [key, value] of Object.entries(spec.path.source)) {
      lines.push(`      ${key}: ${formatSourceLiteral(key, value)}`);
    }
    lines.push(`    }`);
  }
  lines.push(`    defaults {`);
  lines.push(`      speed: ${spec.path.defaults.speed}`);
  lines.push(`      zone: ${spec.path.defaults.zone}`);
  lines.push(`    }`);
  for (const point of spec.path.points) {
    lines.push(`    ${renderPoint(point)}`);
  }
  lines.push(`  }`);
  lines.push(...renderOperationExpanded(spec.operation));
  lines.push(`  proc main()`);
  lines.push(`    run_operation ${spec.operation.name}`);
  lines.push(`  end`);
  lines.push(`end`);
  return lines.join("\n");
}

function renderCompact(spec: GrlProgramGenerationSpec): string {
  const lines: string[] = [`language grl 0.1`, `module ${spec.moduleName}`];
  for (const [name, expression] of Object.entries(spec.speeds ?? {})) {
    lines.push(`  const speed ${name} = ${expression}`);
  }
  for (const [name, expression] of Object.entries(spec.zones ?? {})) {
    lines.push(`  const zone ${name} = ${expression}`);
  }
  for (const target of spec.targets) {
    lines.push(`  ${renderTargetCompact(target)}`);
  }
  const source = spec.path.source && Object.keys(spec.path.source).length > 0
    ? ` source { ${Object.entries(spec.path.source).map(([key, value]) => `${key}: ${formatSourceLiteral(key, value)}`).join(" ")} }`
    : "";
  lines.push(`  path ${spec.path.name} {${source} defaults { speed: ${spec.path.defaults.speed} zone: ${spec.path.defaults.zone} } ${spec.path.points.map(renderPoint).join(" ")} }`);
  lines.push(`  operation ${spec.operation.name} { kind: ${spec.operation.kind} path: ${spec.operation.path}${spec.operation.startAction ? ` start_action: ${spec.operation.startAction}` : ""}${spec.operation.endAction ? ` end_action: ${spec.operation.endAction}` : ""} }`);
  lines.push(`  proc main()`);
  lines.push(`    run_operation ${spec.operation.name}`);
  lines.push(`  end`);
  lines.push(`end`);
  return lines.join("\n");
}

function renderTargetExpanded(target: GeneratedTargetSpec): string[] {
  if (target.kind === "joint") {
    return [
      `  target ${target.name} = joint_target {`,
      `    joints: [${target.values.map((value) => `${value} deg`).join(", ")}]`,
      `  }`
    ];
  }
  return [
    `  target ${target.name} = pose_target {`,
    `    pose: pose(${target.values.map((value, index) => `${value} ${index < 3 ? "mm" : "deg"}`).join(", ")})`,
    `  }`
  ];
}

function renderTargetCompact(target: GeneratedTargetSpec): string {
  if (target.kind === "joint") {
    return `target ${target.name} = joint_target { joints: [${target.values.map((value) => `${value} deg`).join(", ")}] }`;
  }
  return `target ${target.name} = pose_target { pose: pose(${target.values.map((value, index) => `${value} ${index < 3 ? "mm" : "deg"}`).join(", ")}) }`;
}

function renderPoint(point: GeneratedPathPointSpec): string {
  const params = [
    `point ${point.id} ${point.motion}`,
    point.motion === "movec" ? `via ${point.via}` : undefined,
    point.motion === "movec" ? `target ${point.target}` : point.target,
    point.speed ? `speed ${point.speed}` : undefined,
    point.zone ? `zone ${point.zone}` : undefined
  ].filter(Boolean);
  return params.join(" ");
}

function renderOperationExpanded(operation: GeneratedOperationSpec): string[] {
  const lines = [`  operation ${operation.name} {`, `    kind: ${operation.kind}`, `    path: ${operation.path}`];
  if (operation.startAction) {
    lines.push(`    start_action:`);
    lines.push(`      ${operation.startAction}`);
  }
  if (operation.endAction) {
    lines.push(`    end_action:`);
    lines.push(`      ${operation.endAction}`);
  }
  lines.push(`  }`);
  return lines;
}

function sortRecord<T>(record: Record<string, T>): Record<string, T> {
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
}

function formatLiteral(value: string | number | boolean): string {
  if (typeof value === "string") {
    return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value) ? value : JSON.stringify(value);
  }
  return String(value);
}

function formatSourceLiteral(key: string, value: string | number | boolean): string {
  if (typeof value !== "string") {
    return String(value);
  }
  if (key === "type" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
    return value;
  }
  return JSON.stringify(value);
}
