import { spawnSync } from "child_process";

import { clipboard } from "./clipboard-native.js";
import { loadPhoton } from "./photon.js";

export type ClipboardImage = {
  bytes: Uint8Array;
  mimeType: string;
};

const SUPPORTED_IMAGE_MIME_TYPES = [
  "image/png",
  "image/jpeg",
  "image/webp",
  "image/gif",
] as const;

const DEFAULT_LIST_TIMEOUT_MS = 1000;
const DEFAULT_READ_TIMEOUT_MS = 3000;
const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;

export function isWaylandSession(
  env: NodeJS.ProcessEnv = process.env,
): boolean {
  return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland";
}

function baseMimeType(mimeType: string): string {
  return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();
}

export function extensionForImageMimeType(mimeType: string): string | null {
  switch (baseMimeType(mimeType)) {
    case "image/png":
      return "png";
    case "image/jpeg":
      return "jpg";
    case "image/webp":
      return "webp";
    case "image/gif":
      return "gif";
    default:
      return null;
  }
}

function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
  const normalized = mimeTypes
    .map((t) => t.trim())
    .filter(Boolean)
    .map((t) => ({ raw: t, base: baseMimeType(t) }));

  for (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {
    const match = normalized.find((t) => t.base === preferred);
    if (match) {
      return match.raw;
    }
  }

  const anyImage = normalized.find((t) => t.base.startsWith("image/"));
  return anyImage?.raw ?? null;
}

function isSupportedImageMimeType(mimeType: string): boolean {
  const base = baseMimeType(mimeType);
  return SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);
}

/**
 * Convert unsupported image formats to PNG using Photon.
 * Returns null if conversion is unavailable or fails.
 */
async function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {
  const photon = await loadPhoton();
  if (!photon) {
    return null;
  }

  try {
    const image = photon.PhotonImage.new_from_byteslice(bytes);
    try {
      return image.get_bytes();
    } finally {
      image.free();
    }
  } catch {
    return null;
  }
}

function runCommand(
  command: string,
  args: string[],
  options?: { timeoutMs?: number; maxBufferBytes?: number },
): { stdout: Buffer; ok: boolean } {
  const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;
  const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;

  const result = spawnSync(command, args, {
    timeout: timeoutMs,
    maxBuffer: maxBufferBytes,
  });

  if (result.error) {
    return { ok: false, stdout: Buffer.alloc(0) };
  }

  if (result.status !== 0) {
    return { ok: false, stdout: Buffer.alloc(0) };
  }

  const stdout = Buffer.isBuffer(result.stdout)
    ? result.stdout
    : Buffer.from(
        result.stdout ?? "",
        typeof result.stdout === "string" ? "utf-8" : undefined,
      );

  return { ok: true, stdout };
}

function readClipboardImageViaWlPaste(): ClipboardImage | null {
  const list = runCommand("wl-paste", ["--list-types"], {
    timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
  });
  if (!list.ok) {
    return null;
  }

  const types = list.stdout
    .toString("utf-8")
    .split(/\r?\n/)
    .map((t) => t.trim())
    .filter(Boolean);

  const selectedType = selectPreferredImageMimeType(types);
  if (!selectedType) {
    return null;
  }

  const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
  if (!data.ok || data.stdout.length === 0) {
    return null;
  }

  return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
}

function readClipboardImageViaXclip(): ClipboardImage | null {
  const targets = runCommand(
    "xclip",
    ["-selection", "clipboard", "-t", "TARGETS", "-o"],
    {
      timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
    },
  );

  let candidateTypes: string[] = [];
  if (targets.ok) {
    candidateTypes = targets.stdout
      .toString("utf-8")
      .split(/\r?\n/)
      .map((t) => t.trim())
      .filter(Boolean);
  }

  const preferred =
    candidateTypes.length > 0
      ? selectPreferredImageMimeType(candidateTypes)
      : null;
  const tryTypes = preferred
    ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES]
    : [...SUPPORTED_IMAGE_MIME_TYPES];

  for (const mimeType of tryTypes) {
    const data = runCommand("xclip", [
      "-selection",
      "clipboard",
      "-t",
      mimeType,
      "-o",
    ]);
    if (data.ok && data.stdout.length > 0) {
      return { bytes: data.stdout, mimeType: baseMimeType(mimeType) };
    }
  }

  return null;
}

export async function readClipboardImage(options?: {
  env?: NodeJS.ProcessEnv;
  platform?: NodeJS.Platform;
}): Promise<ClipboardImage | null> {
  const env = options?.env ?? process.env;
  const platform = options?.platform ?? process.platform;

  if (env.TERMUX_VERSION) {
    return null;
  }

  let image: ClipboardImage | null = null;

  if (platform === "linux" && isWaylandSession(env)) {
    image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();
  } else {
    if (!clipboard || !clipboard.hasImage()) {
      return null;
    }

    const imageData = await clipboard.getImageBinary();
    if (!imageData || imageData.length === 0) {
      return null;
    }

    const bytes =
      imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);
    image = { bytes, mimeType: "image/png" };
  }

  if (!image) {
    return null;
  }

  // Convert unsupported formats (e.g., BMP from WSLg) to PNG
  if (!isSupportedImageMimeType(image.mimeType)) {
    const pngBytes = await convertToPng(image.bytes);
    if (!pngBytes) {
      return null;
    }
    return { bytes: pngBytes, mimeType: "image/png" };
  }

  return image;
}
