use std::collections::HashMap;

use comemo::Prehashed;
use time::OffsetDateTime;
use typst::{
    diag::{FileError, FileResult},
    eval::Tracer,
    foundations::{Bytes, Datetime, IntoValue},
    syntax::{FileId, Source, VirtualPath},
    text::{Font, FontBook},
    visualize::Color,
    Library, World,
};

const EMBEDDED_FONTS: &[&[u8]] = &[
    include_bytes!("../res/ComicNeue-Bold.ttf"),
    include_bytes!("../res/NotoColorEmoji.ttf"),
];

const AUTH_SOURCE: &str = include_str!("../res/auth.typ");
const UNAUTH_SOURCE: &str = include_str!("../res/unauth.typ");

const FILE_SOURCES: &[(&str, &[u8])] = &[
    ("confetti.svg", include_bytes!("../res/confetti.svg")),
    ("party.svg", include_bytes!("../res/party.svg")),
];

fn get_fonts() -> (FontBook, Vec<Font>) {
    let mut book = FontBook::new();
    let mut fonts = vec![];
    for font in EMBEDDED_FONTS
        .iter()
        .flat_map(|data| Font::iter(Bytes::from_static(data)))
    {
        book.push(font.info().clone());
        fonts.push(font);
    }

    (book, fonts)
}

/// A minimal world that generates Authenticated Media certificates
pub struct AuthMediaWorld {
    /// The name of the server issuing the certificate
    issuer: String,
    /// File ID of the authenticated source
    auth_id: FileId,
    /// File ID of the unauthenticated source
    unauth_id: FileId,
    /// True if the authenticated source should be used
    is_authenticated: bool,
    /// Typst's standard library.
    library: Prehashed<Library>,
    /// Metadata about discovered fonts.
    book: Prehashed<FontBook>,
    /// Locations of and storage for lazily loaded fonts.
    fonts: Vec<Font>,
    /// Maps file ids to source files and buffers.
    slots: HashMap<FileId, (Bytes, Option<Source>)>,
    /// The current datetime if requested. This is stored here to ensure it is
    /// always the same within one compilation. Reset between compilations.
    now: Datetime,
}

impl AuthMediaWorld {
    #[must_use]
    pub fn new(issuer: String) -> Self {
        let auth_id = FileId::new_fake(VirtualPath::new("<auth>"));
        let unauth_id = FileId::new_fake(VirtualPath::new("<unauth>"));

        let mut slots = HashMap::new();
        for &(name, data) in FILE_SOURCES {
            let id = FileId::new(None, VirtualPath::new(format!("/{name}")));
            let bytes = data.into();
            let source = std::str::from_utf8(data)
                .ok()
                .map(|s| Source::new(id, s.to_owned()));
            slots.insert(id, (bytes, source));
        }

        let library = Library::builder().build();

        let (book, fonts) = get_fonts();
        let now = Datetime::Date(OffsetDateTime::now_utc().date());

        Self {
            issuer,
            auth_id,
            unauth_id,
            is_authenticated: false,
            library: Prehashed::new(library),
            book: Prehashed::new(book),
            fonts,
            slots,
            now,
        }
    }

    pub fn generate(&mut self, target: Option<&str>) -> Option<Vec<u8>> {
        let mut inputs =
            vec![("issuer".into(), self.issuer.as_str().into_value())];
        if let Some(target) = target {
            inputs.push(("target".into(), target.into_value()));
        }
        let library = Library::builder()
            .with_inputs(inputs.into_iter().collect())
            .build();
        self.library.update(|old| *old = library);
        self.is_authenticated = target.is_some();

        let mut tracer = Tracer::new();
        let document = typst::compile(self, &mut tracer).unwrap();
        if document.pages.len() != 1 {
            return None;
        }
        typst_render::render(&document.pages[0].frame, 2.0, Color::WHITE)
            .encode_png()
            .ok()
    }
}

impl World for AuthMediaWorld {
    fn library(&self) -> &Prehashed<Library> {
        &self.library
    }

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

    fn main(&self) -> Source {
        if self.is_authenticated {
            Source::new(self.auth_id, AUTH_SOURCE.to_owned())
        } else {
            Source::new(self.unauth_id, UNAUTH_SOURCE.to_owned())
        }
    }

    fn source(&self, id: FileId) -> FileResult<Source> {
        let (_bytes, source) = self.slots.get(&id).ok_or(
            FileError::NotFound(id.vpath().as_rooted_path().to_owned()),
        )?;
        Ok(source.as_ref().ok_or(FileError::NotSource)?.clone())
    }

    fn file(&self, id: FileId) -> FileResult<Bytes> {
        let (bytes, _source) = self.slots.get(&id).ok_or(
            FileError::NotFound(id.vpath().as_rooted_path().to_owned()),
        )?;
        Ok(bytes.clone())
    }

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

    fn today(&self, offset: Option<i64>) -> Option<Datetime> {
        assert_eq!(offset, None, "today offset not supported");
        Some(self.now)
    }
}
