/**
 * Font loading, caching, and text-to-path conversion service.
 *
 * Uses opentype.js to parse .ttf files fetched from the public /fonts/ directory.
 * Provides text-to-SVG-path conversion for the "Convert to Paths" feature.
 */

// opentype.js ships without TS declarations — we declare the subset we use.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type OpentypeFont = any;

/** Metadata for a bundled font available to the user. */
export interface FontDescriptor {
  /** Display name shown in the font picker */
  family: string;
  /** Filename relative to /fonts/ (e.g. "Roboto-Regular.ttf") */
  file: string;
}

/** Result from text-to-path conversion. */
export interface TextPathResult {
  /** SVG path `d` attribute data */
  pathData: string;
  /** Total advance width in canvas pixels */
  width: number;
  /** Line height in canvas pixels (ascender + descender) */
  height: number;
}

/** Registry of bundled fonts. Add entries here when new fonts are added to public/fonts/. */
const BUNDLED_FONTS: FontDescriptor[] = [
  { family: 'Roboto', file: 'Roboto-Regular.ttf' },
  { family: 'Open Sans', file: 'OpenSans-Regular.ttf' },
  { family: 'Lato', file: 'Lato-Regular.ttf' },
];

/** In-memory cache of parsed Font objects keyed by font file path. */
const fontCache = new Map<string, OpentypeFont>();

/** In-flight fetch promises to deduplicate concurrent loads for the same font. */
const loadingPromises = new Map<string, Promise<OpentypeFont>>();

/**
 * Return the list of bundled fonts available for selection.
 */
export function getAvailableFonts(): FontDescriptor[] {
  return BUNDLED_FONTS;
}

/**
 * Resolve a font family display name to the bundled font URL.
 * Falls back to the first bundled font if the family isn't found.
 */
function resolveFontUrl(family: string): string {
  const desc = BUNDLED_FONTS.find(
    (f) => f.family.toLowerCase() === family.toLowerCase(),
  );
  const file = desc ? desc.file : BUNDLED_FONTS[0].file;
  return `/fonts/${file}`;
}

/**
 * Load and parse a font from a URL. Returns a cached instance if available.
 * Concurrent calls for the same URL are deduplicated.
 */
export async function loadFont(fontUrl: string): Promise<OpentypeFont> {
  // Return cached font if available
  const cached = fontCache.get(fontUrl);
  if (cached) return cached;

  // Deduplicate in-flight requests
  const inflight = loadingPromises.get(fontUrl);
  if (inflight) return inflight;

  const promise = (async () => {
    const opentype = await import('opentype.js');
    const response = await fetch(fontUrl);
    if (!response.ok) {
      throw new Error(`Failed to fetch font: ${fontUrl} (${response.status})`);
    }
    const arrayBuffer = await response.arrayBuffer();
    const font = opentype.parse(arrayBuffer);
    fontCache.set(fontUrl, font);
    return font;
  })();

  loadingPromises.set(fontUrl, promise);

  try {
    const font = await promise;
    return font;
  } finally {
    loadingPromises.delete(fontUrl);
  }
}

/**
 * Load a font by its family display name (e.g. "Roboto").
 * Resolves the family name to the bundled font URL, then loads it.
 */
export async function loadFontByFamily(family: string): Promise<OpentypeFont> {
  const url = resolveFontUrl(family);
  return loadFont(url);
}

/**
 * Convert text to SVG path data using opentype.js.
 *
 * Handles per-character glyph positioning with manual x-advance to support
 * letter spacing (which opentype's `getPath()` does not natively support).
 *
 * The returned path data uses a coordinate system suitable for SVG/Konva:
 * - origin at top-left of the text bounding box
 * - Y increases downward (font coordinates are flipped from Y-up to Y-down)
 *
 * @param text      The string to convert
 * @param family    Font family name (resolved to a bundled font)
 * @param fontSize  Font size in pixels
 * @param letterSpacing  Extra spacing between characters in pixels (default 0)
 * @returns Path data, dimensions
 */
export async function textToPathData(
  text: string,
  family: string,
  fontSize: number,
  letterSpacing = 0,
): Promise<TextPathResult> {
  const font = await loadFontByFamily(family);
  const scale = fontSize / font.unitsPerEm;

  // Collect per-character path commands with manual x-advance for letter spacing
  let cursorX = 0;
  const allCommands: Array<{ type: string; x?: number; y?: number; x1?: number; y1?: number; x2?: number; y2?: number }> = [];

  for (let i = 0; i < text.length; i++) {
    const glyph = font.charToGlyph(text[i]);
    // getPath returns path at font units, positioned at (x, y) at the given fontSize
    // y=0 is the baseline — ascenders go negative, descenders go positive in font coords
    const glyphPath = glyph.getPath(cursorX / scale, 0, font.unitsPerEm);

    for (const cmd of glyphPath.commands) {
      allCommands.push(cmd);
    }

    // Advance cursor: glyph advance width scaled + letter spacing
    const advance = (glyph.advanceWidth ?? 0) * scale;
    cursorX += advance + letterSpacing;
  }

  // Compute metrics for the ascender offset
  const ascender = font.ascender * scale;

  // Build SVG path data string, flipping Y and offsetting by ascender
  // Font coords: Y-up, baseline at 0, ascenders negative
  // Canvas coords: Y-down, top-left origin
  // Transform: canvas_y = ascender - font_y (mapped through scale)
  let pathData = '';
  for (const cmd of allCommands) {
    const yFlip = (y: number) => ascender - y * scale;
    const xScale = (x: number) => x * scale;

    switch (cmd.type) {
      case 'M':
        pathData += `M${round(xScale(cmd.x!))} ${round(yFlip(cmd.y!))}`;
        break;
      case 'L':
        pathData += `L${round(xScale(cmd.x!))} ${round(yFlip(cmd.y!))}`;
        break;
      case 'Q':
        pathData += `Q${round(xScale(cmd.x1!))} ${round(yFlip(cmd.y1!))} ${round(xScale(cmd.x!))} ${round(yFlip(cmd.y!))}`;
        break;
      case 'C':
        pathData += `C${round(xScale(cmd.x1!))} ${round(yFlip(cmd.y1!))} ${round(xScale(cmd.x2!))} ${round(yFlip(cmd.y2!))} ${round(xScale(cmd.x!))} ${round(yFlip(cmd.y!))}`;
        break;
      case 'Z':
        pathData += 'Z';
        break;
    }
  }

  // Total dimensions
  const descender = Math.abs(font.descender * scale);
  const width = cursorX > 0 ? cursorX - letterSpacing : 0; // remove trailing spacing
  const height = ascender + descender;

  return { pathData, width: round(width), height: round(height) };
}

/** Round to 2 decimal places to keep path data compact. */
function round(n: number): number {
  return Math.round(n * 100) / 100;
}

/**
 * Clear the font cache. Useful for testing.
 */
export function clearFontCache(): void {
  fontCache.clear();
  loadingPromises.clear();
}

/**
 * Check if a font is already cached by URL.
 */
export function isFontCached(fontUrl: string): boolean {
  return fontCache.has(fontUrl);
}
