use std::time::Instant;

use log::info;
use typst::diag::FileResult;
use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, LibraryExt, World};
use typst_kit::fonts::{FontSearcher, FontSlot, Fonts};

/// Cached font search results, shared across multiple `MluxWorld` instances.
///
/// `FontSearcher::search()` scans the filesystem (~13ms even in release builds).
/// By caching the results, we avoid repeated scans when creating multiple worlds
/// (e.g., content + sidebar in the viewer, or across resize rebuilds).
pub struct FontCache {
    book: FontBook,
    fonts: Vec<FontSlot>,
}

impl Default for FontCache {
    fn default() -> Self {
        Self::new()
    }
}

impl FontCache {
    /// Perform a one-time font search and cache the results.
    pub fn new() -> Self {
        let start = Instant::now();
        let Fonts { book, fonts } = FontSearcher::new().include_system_fonts(true).search();
        info!(
            "world: font search completed in {:.1}ms",
            start.elapsed().as_secs_f64() * 1000.0
        );

        // CJK font availability check (once per process)
        let has_cjk = book.contains_family("ipagothic")
            || book.contains_family("noto sans cjk jp")
            || book.contains_family("noto serif cjk jp")
            || book.contains_family("ipamincho");
        if !has_cjk {
            eprintln!("warning: no CJK font found. Japanese text may not render correctly.");
            eprintln!("  Install IPAGothic or Noto Sans CJK JP for proper rendering.");
        }

        Self { book, fonts }
    }
}

/// The Typst world for mlux.
///
/// Provides a single virtual file (`/main.typ`) containing the theme
/// set-rules followed by the converted Markdown content.
///
/// Borrows font data from a [`FontCache`] to avoid repeated filesystem scans.
pub struct MluxWorld<'f> {
    library: LazyHash<Library>,
    book: LazyHash<FontBook>,
    fonts: &'f [FontSlot],
    main_id: FileId,
    main_source: Source,
    /// Byte offset where content_text begins within main.typ.
    content_offset: usize,
}

impl<'f> MluxWorld<'f> {
    /// Create a new MluxWorld.
    ///
    /// - `theme_text`: contents of the theme.typ file
    /// - `content_text`: Typst markup converted from Markdown
    /// - `width`: page width in pt
    /// - `fonts`: cached font search results
    pub fn new(theme_text: &str, content_text: &str, width: f64, fonts: &'f FontCache) -> Self {
        let start = Instant::now();
        // Inline theme + width override + content into a single source
        let prefix = format!("{theme_text}\n#set page(width: {width}pt)\n");
        let content_offset = prefix.len();
        let main_text = format!("{prefix}{content_text}\n");

        let mut world = Self::from_source(&main_text, fonts);
        world.content_offset = content_offset;
        info!(
            "world: new() completed in {:.1}ms",
            start.elapsed().as_secs_f64() * 1000.0
        );
        world
    }

    /// Create a MluxWorld from raw Typst source (no theme injection or width override).
    pub fn new_raw(source: &str, fonts: &'f FontCache) -> Self {
        Self::from_source(source, fonts)
    }

    /// Get a reference to the main Source (for Span resolution).
    pub fn main_source(&self) -> &Source {
        &self.main_source
    }

    /// Byte offset where content_text begins within main.typ.
    pub fn content_offset(&self) -> usize {
        self.content_offset
    }

    fn from_source(main_text: &str, fonts: &'f FontCache) -> Self {
        let vpath = VirtualPath::new("main.typ");
        let main_id = FileId::new(None, vpath);
        let main_source = Source::new(main_id, main_text.to_string());

        Self {
            library: LazyHash::new(Library::default()),
            book: LazyHash::new(fonts.book.clone()),
            fonts: &fonts.fonts,
            main_id,
            main_source,
            content_offset: 0,
        }
    }
}

impl World for MluxWorld<'_> {
    fn library(&self) -> &LazyHash<Library> {
        &self.library
    }

    fn book(&self) -> &LazyHash<FontBook> {
        &self.book
    }

    fn main(&self) -> FileId {
        self.main_id
    }

    fn source(&self, id: FileId) -> FileResult<Source> {
        if id == self.main_id {
            Ok(self.main_source.clone())
        } else {
            Err(typst::diag::FileError::NotFound(
                id.vpath().as_rootless_path().into(),
            ))
        }
    }

    fn file(&self, id: FileId) -> FileResult<Bytes> {
        if id == self.main_id {
            Ok(Bytes::from_string(self.main_source.clone()))
        } else {
            Err(typst::diag::FileError::NotFound(
                id.vpath().as_rootless_path().into(),
            ))
        }
    }

    fn font(&self, index: usize) -> Option<Font> {
        self.fonts.get(index)?.get()
    }

    fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
        None
    }
}
