// Turns a project's files into the esbuild input map + entrypoint + metadata header, then bundles.
// The one canonical "user project → deployable bundle" path: the backend and CLI both call this
// (the backend resolves asset URLs from the DB, the CLI from local metadata — same function, the
// URLs are passed in as `publicUrl` + per-entry `fileSrc`/`shaders`).

import { bundleProjectWithMap } from "./bundler"
import { buildHeader } from "./header"
import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"

export type CompileFileType = "text" | "resource" | "shader"

export type CompileEntry = {
  path: string
  type: CompileFileType
  /** Source text for code / .svg files. */
  text?: string
  /** Server src for a binary resource (e.g. "/uploads/x.png"); prefixed with publicUrl. */
  fileSrc?: string
  /** Compiled-shader artifacts (platform + src); the first drives the per-backend URL. */
  shaders?: { platform: string, src: string }[]
}

export type CompileOptions = {
  entries: CompileEntry[]
  name: string
  publicUrl: string
  entryOverride?: string
  /** Prepend the metadata header (default true). */
  header?: boolean
  /** Generated-icon URL for the header (backend only). */
  icon?: string
  /** "esm" (default, shipped) or "iife" (self-running, for the local renderer). */
  format?: "esm" | "iife"
  /** Resolve `asset()` resources to a LOCAL host asset at runtime instead of a `publicUrl` URL: each
   *  resource compiles to `"id:" + _creatorUtils.fetchLocal(<name>)` (the host reads the file by name
   *  and returns its systemId). For hosts with no asset server (the desktop native host), where a URL
   *  can't be fetched. Off by default — the shipped/web build keeps the URL form. */
  localAssets?: boolean
}

/** The default entrypoint: the largest top-level .ts/.js file with no `export` (a script entry). */
const detectEntry = (entries: CompileEntry[]): string => {
  let entry = ""
  let max = -1
  for (const e of entries) {
    const ext = e.path.split(".").at(-1)?.toLowerCase()
    if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext === "ts" || ext === "js")) {
      if (e.text.length > max) { entry = e.path; max = e.text.length }
    }
  }
  return entry
}

export const compileProject = async (opts: CompileOptions): Promise<string> => {
  const entry = opts.entryOverride ?? detectEntry(opts.entries)
  if (!entry) {
    throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.")
  }

  const files: Record<string, string> = {}
  for (const e of opts.entries) {
    if (e.type === "resource") {
      if (opts.localAssets) {
        // The host resolves the asset by name at runtime (reads the local file → systemId); the
        // "id:" prefix is the host's local-resource URL scheme (parsed by the UI + 2D texture paths).
        const name = e.path.split(/[\\/]/).pop() ?? e.path
        files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`
      } else {
        files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`
      }
    } else if (e.type === "shader") {
      const shaders = e.shaders ?? []
      if (shaders.length > 2) {
        files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``
      } else if (shaders.length > 0) {
        files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``
      } else {
        files[e.path] = `export default ""` // local-only shader with no compiled artifact yet
      }
    } else if (e.path.toLowerCase().endsWith(".svg")) {
      files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`
    } else {
      files[e.path] = e.text ?? ""
    }
  }

  const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm")
  if (opts.header === false) {
    return map ? `${code}\n${inlineSourceMapComment(map)}` : code
  }
  const header = buildHeader(opts.name, code, { icon: opts.icon })
  if (!map) return header + code
  // Shift the map down past the header (prepended before the code); the inline comment lands last.
  const headerLines = (header.match(/\n/g) ?? []).length
  return `${header}${code}\n${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`
}
