import { chromium } from 'playwright'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { stages } from './frames.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

export const FONTS_DIR = path.join(__dirname, '../../resources/fonts')

// Fonts we can embed. Keyed by the font-family name Excalidraw writes into
// the SVG.
export const FONT_FILES = {
  Excalifont:      'Excalifont-Regular.woff2',
  'Cascadia Code': 'Cascadia.woff2',
}

// Excalidraw always emits its own @font-face { font-family: Excalifont; ... }
// block with a base64 subset (~150KB). Strip it so we don't ship it per-SVG;
// --embed-fonts re-adds our own subset only when the family is actually used.
export function stripEmbeddedFonts(svg) {
  return svg.replace(/@font-face\s*\{\s*font-family:\s*Excalifont;[^}]*\}/g, '')
}

export function injectFontDefinitions(svg, fontFaces) {
  if (!fontFaces.length) return svg
  const css = fontFaces.map(({ family, base64 }) =>
    `@font-face { font-family: '${family}'; src: url('data:font/woff2;base64,${base64}'); }`
  ).join(' ')
  return svg.replace(/<style[^>]*>/, `$&${css}`)
}

async function fontFacesForSvg(svg) {
  const out = []
  for (const [family, file] of Object.entries(FONT_FILES)) {
    if (!svg.includes(family)) continue
    try {
      const data = await fs.readFile(path.join(FONTS_DIR, file))
      out.push({ family, base64: data.toString('base64') })
    } catch { /* font file missing — skip */ }
  }
  return out
}

async function exportStep(page, source, elements) {
  return page.evaluate(
    async ({ elements, appState, files }) => {
      const { exportToSvg } = window.Excalidraw
      const svgEl = await exportToSvg({
        elements,
        appState: {
          ...appState,
          scrollX: 0,
          scrollY: 0,
          zoom: { value: 1 },
          exportBackground: false,
          exportPadding: 20,
        },
        files: files ?? {},
      })

      // Stamp data-element-id on each top-level <g> in document order.
      // Excalidraw renders one <g stroke-linecap="round"> per element, in the
      // same order as the elements array (excluding deleted/frame elements).
      // true for vendored @excalidraw/excalidraw@0.18.0
      const renderableIds = elements
        .filter(el => !el.isDeleted && el.type !== 'frame')
        .map(el => el.id)
      const topGroups = svgEl.querySelectorAll(':scope > g[stroke-linecap="round"]')
      topGroups.forEach((g, i) => {
        if (i < renderableIds.length) g.setAttribute('data-element-id', renderableIds[i])
      })

      return svgEl.outerHTML
    },
    { elements, appState: source.appState, files: source.files }
  )
}

export function toFilename(name) {
  return name.replace(/[^a-z0-9-]/gi, '-').replace(/-+/g, '-').replace(/^-/, '').replace(/-$/, '')
}

export async function render(source, outDir, { embedFonts = false } = {}) {
  const utilsBundle = path.join(__dirname, '../../resources/excalidraw.min.js')

  const browser = await chromium.launch()
  const page = await browser.newPage()
  await page.goto('about:blank')
  await page.addScriptTag({ path: utilsBundle })

  // All non-frame elements — passed to every exportToSvg call so the
  // coordinate system is identical for every stage. Elements not yet
  // visible are rendered at opacity 0.
  const allElements = source.elements.filter(el => el.type !== 'frame')

  const frameStages = stages(source)

  try {
    if (!frameStages.length) {
      const rawSvg = stripEmbeddedFonts(await exportStep(page, source, allElements))
      const fontFaces = embedFonts ? await fontFacesForSvg(rawSvg) : []
      const svg = injectFontDefinitions(rawSvg, fontFaces)
      const stem = 'image'
      await fs.writeFile(path.join(outDir, `${stem}.svg`), svg)
    } else {
      for (const { frame, elementIds } of frameStages) {
        const renderElements = allElements.map(el =>
          elementIds.has(el.id) ? el : { ...el, opacity: 0 }
        )
        const rawSvg = stripEmbeddedFonts(await exportStep(page, source, renderElements))
        const fontFaces = embedFonts ? await fontFacesForSvg(rawSvg) : []
        const marker = `<!-- excalidraw-stages: ${frame.name} -->`
        const svg = injectFontDefinitions(rawSvg, fontFaces)
          .replace(/<svg([^>]*)>/, `<svg$1>${marker}`)
        const stem = toFilename(frame.name)
        await fs.writeFile(path.join(outDir, `${stem}.svg`), svg)
      }
    }
  } finally {
    await browser.close()
  }
}
