//! Embedded `cph-render` package — makes an installed `cph` self-contained.
//!
//! The typst [`crate::world::LessonWorld`] reads the render package
//! (`@local/cph-render` + the vendored `@preview/numbly`) **from disk**, by
//! `render_dir.join(vpath.realize)`. In a source checkout that dir is the repo's
//! `render/`. But a `cargo install`-ed binary has no repo around it — the old
//! `env!("CARGO_MANIFEST_DIR")/../../render` path points into the build sandbox
//! and does not exist at runtime.
//!
//! So we **embed the whole `render/` tree into the binary** at compile time
//! (`include_dir!`) and, on first use, **extract it to a per-user cache dir**
//! keyed by the crate version. `default_render_dir` returns that extracted path.
//! The tree is tiny (~140 KiB, ~33 files), so embedding is cheap and extraction
//! is a one-time cost per version.
//!
//! `render/vendor/local-packages/` (a dev-only self-referential symlink,
//! gitignored) is excluded by the build script when it stages the tree for
//! embedding — see `build.rs`. The embedded copy never contains it, which is
//! correct: the World resolves `@local/cph-render` from `render_dir` directly,
//! and `@preview/*` from `vendor/typst-packages/`, neither via that symlink.

use std::path::{Path, PathBuf};

use include_dir::{include_dir, Dir};

/// The `render/` tree, embedded at compile time from the **staged** copy the
/// build script wrote to `OUT_DIR/render` (which omits the dev-only symlink that
/// would otherwise make `include_dir!` recurse forever). `CPH_STAGED_RENDER_DIR`
/// is set by `build.rs`.
static RENDER_DIR: Dir<'_> = include_dir!("$CPH_STAGED_RENDER_DIR");

/// Resolve the directory the typst World should read the render package from,
/// extracting the embedded copy to a per-user cache dir if needed.
///
/// Resolution order:
/// 1. `CPH_RENDER_DIR` env var — an explicit override (dev convenience: point
///    at the live repo `render/`).
/// 2. The extracted embedded copy under the user cache dir
///    (`<cache>/cph/render-<version>/`). Extracted once per crate version;
///    subsequent runs reuse it.
///
/// On any failure to locate a cache dir or extract, falls back to a temp-dir
/// location so the engine still works (just re-extracting per process).
pub fn resolve_render_dir() -> PathBuf {
    if let Ok(dir) = std::env::var("CPH_RENDER_DIR") {
        return PathBuf::from(dir);
    }
    ensure_extracted().unwrap_or_else(|_| {
        // Last-resort: extract under the OS temp dir. Still correct, just not
        // cached across processes.
        let fallback =
            std::env::temp_dir().join(format!("cph-render-{}", env!("CARGO_PKG_VERSION")));
        let _ = extract_to(&fallback);
        fallback
    })
}

/// The version-keyed cache location and a guarantee the embedded tree is present
/// there. Returns the directory the World should use.
fn ensure_extracted() -> std::io::Result<PathBuf> {
    let base = dirs::cache_dir()
        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "no user cache dir"))?;
    let dest = base
        .join("cph")
        .join(format!("render-{}", env!("CARGO_PKG_VERSION")));

    // A sentinel marks a complete extraction; if present, reuse as-is. (Keyed by
    // version, so a new `cph` version re-extracts into a fresh dir.)
    let sentinel = dest.join(".extracted");
    if sentinel.is_file() {
        return Ok(dest);
    }

    extract_to(&dest)?;
    std::fs::write(&sentinel, env!("CARGO_PKG_VERSION"))?;
    Ok(dest)
}

/// Write the embedded `render/` tree under `dest` (creating dirs as needed).
///
/// Each embedded entry's `.path()` is already relative to the embed root (e.g.
/// `src/elements/segment.typ`), so we join the **full** path under `dest` and
/// create parents — no per-level mirroring needed.
fn extract_to(dest: &Path) -> std::io::Result<()> {
    std::fs::create_dir_all(dest)?;
    write_dir(&RENDER_DIR, dest)
}

/// Recursively write an embedded [`Dir`]'s files under `dest`, preserving the
/// full relative path of each file.
fn write_dir(dir: &Dir<'_>, dest: &Path) -> std::io::Result<()> {
    for entry in dir.entries() {
        match entry {
            include_dir::DirEntry::Dir(sub) => write_dir(sub, dest)?,
            include_dir::DirEntry::File(file) => {
                let file_dest = dest.join(file.path());
                if let Some(parent) = file_dest.parent() {
                    std::fs::create_dir_all(parent)?;
                }
                std::fs::write(&file_dest, file.contents())?;
            }
        }
    }
    Ok(())
}
