//! Project compiler for tinymist.

use core::fmt;
use std::collections::HashSet;
use std::path::Path;
use std::sync::{Arc, OnceLock};

use ecow::{EcoString, EcoVec, eco_vec};
use tinymist_std::error::prelude::Result;
use tinymist_std::{ImmutPath, typst::TypstDocument};
use tinymist_task::ExportTarget;
use tinymist_world::vfs::notify::{
    FilesystemEvent, MemoryEvent, NotifyDeps, NotifyMessage, UpstreamUpdateEvent,
};
use tinymist_world::vfs::{FileId, FsProvider, RevisingVfs, WorkspaceResolver};
use tinymist_world::{
    CompileSignal, CompileSnapshot, CompilerFeat, CompilerUniverse, DiagnosticsTask, EntryReader,
    EntryState, FlagTask, HtmlCompilationTask, PagedCompilationTask, ProjectInsId, TaskInputs,
    WorldComputeGraph, WorldDeps,
};
use tokio::sync::mpsc;
use typst::World;
use typst::diag::{At, FileError};
use typst::syntax::Span;

/// A compiled artifact.
pub struct CompiledArtifact<F: CompilerFeat> {
    /// The used compute graph.
    pub graph: Arc<WorldComputeGraph<F>>,
    /// The diagnostics of the document.
    pub diag: Arc<DiagnosticsTask>,
    /// The compiled document.
    pub doc: Option<TypstDocument>,
    /// The depended files.
    pub deps: OnceLock<EcoVec<FileId>>,
}

impl<F: CompilerFeat> fmt::Display for CompiledArtifact<F> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let rev = self.graph.snap.world.revision();
        write!(f, "CompiledArtifact({:?}, rev={rev:?})", self.graph.snap.id)
    }
}

impl<F: CompilerFeat> std::ops::Deref for CompiledArtifact<F> {
    type Target = Arc<WorldComputeGraph<F>>;

    fn deref(&self) -> &Self::Target {
        &self.graph
    }
}

impl<F: CompilerFeat> Clone for CompiledArtifact<F> {
    fn clone(&self) -> Self {
        Self {
            graph: self.graph.clone(),
            doc: self.doc.clone(),
            diag: self.diag.clone(),
            deps: self.deps.clone(),
        }
    }
}

impl<F: CompilerFeat> CompiledArtifact<F> {
    /// Returns the project id.
    pub fn id(&self) -> &ProjectInsId {
        &self.graph.snap.id
    }

    /// Returns the last successfully compiled document.
    pub fn success_doc(&self) -> Option<TypstDocument> {
        self.doc
            .as_ref()
            .cloned()
            .or_else(|| self.snap.success_doc.clone())
    }

    /// Returns the depended files.
    pub fn depended_files(&self) -> &EcoVec<FileId> {
        self.deps.get_or_init(|| {
            let mut deps = EcoVec::default();
            self.graph.snap.world.iter_dependencies(&mut |f| {
                deps.push(f);
            });

            deps
        })
    }

    /// Runs the compiler and returns the compiled document.
    pub fn from_graph(graph: Arc<WorldComputeGraph<F>>, is_html: bool) -> CompiledArtifact<F> {
        let _ = graph.provide::<FlagTask<HtmlCompilationTask>>(Ok(FlagTask::flag(is_html)));
        let _ = graph.provide::<FlagTask<PagedCompilationTask>>(Ok(FlagTask::flag(!is_html)));
        let doc = if is_html {
            graph.shared_compile_html().expect("html").map(From::from)
        } else {
            graph.shared_compile().expect("paged").map(From::from)
        };

        CompiledArtifact {
            diag: graph.shared_diagnostics().expect("diag"),
            graph,
            doc,
            deps: OnceLock::default(),
        }
    }

    /// Returns the error count.
    pub fn error_cnt(&self) -> usize {
        self.diag.error_cnt()
    }

    /// Returns the warning count.
    pub fn warning_cnt(&self) -> usize {
        self.diag.warning_cnt()
    }

    /// Returns the diagnostics.
    pub fn diagnostics(&self) -> impl Iterator<Item = &typst::diag::SourceDiagnostic> + Clone {
        self.diag.diagnostics()
    }

    /// Returns whether there are any errors.
    pub fn has_errors(&self) -> bool {
        self.error_cnt() > 0
    }

    /// Sets the signal.
    pub fn with_signal(mut self, signal: CompileSignal) -> Self {
        let mut snap = self.snap.clone();
        snap.signal = signal;

        self.graph = self.graph.snapshot_unsafe(snap);
        self
    }
}

/// The compilation status of a project.
#[derive(Debug, Clone)]
pub struct CompileReport {
    /// The project ID.
    pub id: ProjectInsId,
    /// The file getting compiled.
    pub compiling_id: Option<FileId>,
    /// The number of pages in the compiled document, zero if failed.
    pub page_count: u32,
    /// The status of the compilation.
    pub status: CompileStatusEnum,
}

/// The compilation status of a project.
#[derive(Debug, Clone)]
pub enum CompileStatusEnum {
    /// The project is suspended.
    Suspend,
    /// The project is compiling.
    Compiling,
    /// The project compiled successfully.
    CompileSuccess(CompileStatusResult),
    /// The project failed to compile.
    CompileError(CompileStatusResult),
    /// The project failed to export.
    ExportError(CompileStatusResult),
}

/// The compilation status result of a project.
#[derive(Debug, Clone)]
pub struct CompileStatusResult {
    /// The number of errors or warnings occur.
    diag: u32,
    /// Used time
    elapsed: tinymist_std::time::Duration,
}

impl CompileReport {
    /// Gets the status message.
    pub fn message(&self) -> CompileReportMsg<'_> {
        CompileReportMsg(self)
    }
}

/// A message of the compilation status.
pub struct CompileReportMsg<'a>(&'a CompileReport);

impl fmt::Display for CompileReportMsg<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        use CompileStatusEnum::*;
        use CompileStatusResult as Res;

        let input = WorkspaceResolver::display(self.0.compiling_id);
        let (stage, Res { diag, elapsed }) = match &self.0.status {
            Suspend => return f.write_str("suspended"),
            Compiling => return f.write_str("compiling"),
            CompileSuccess(Res { diag: 0, elapsed }) => {
                return write!(f, "{input:?}: compilation succeeded in {elapsed:?}");
            }
            CompileSuccess(res) => ("compilation succeeded", res),
            CompileError(res) => ("compilation failed", res),
            ExportError(res) => ("export failed", res),
        };
        write!(
            f,
            "{input:?}: {stage} with {diag} warnings and errors in {elapsed:?}"
        )
    }
}

/// A project compiler handler.
pub trait CompileHandler<F: CompilerFeat, Ext>: Send + Sync + 'static {
    /// Called when there is any reason to compile. This doesn't mean that the
    /// project should be compiled.
    fn on_any_compile_reason(&self, state: &mut ProjectCompiler<F, Ext>);
    // todo: notify project specific compile
    /// Called when a compilation is done.
    fn notify_compile(&self, res: &CompiledArtifact<F>);
    /// Called when a project is removed.
    fn notify_removed(&self, _id: &ProjectInsId) {}
    /// Called when the compilation status is changed.
    fn status(&self, revision: usize, rep: CompileReport);
}

/// No need so no compilation.
impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> CompileHandler<F, Ext>
    for std::marker::PhantomData<fn(F, Ext)>
{
    fn on_any_compile_reason(&self, _state: &mut ProjectCompiler<F, Ext>) {
        log::info!("ProjectHandle: no need to compile");
    }
    fn notify_compile(&self, _res: &CompiledArtifact<F>) {}
    fn status(&self, _revision: usize, _rep: CompileReport) {}
}

/// An interrupt to the compiler.
pub enum Interrupt<F: CompilerFeat> {
    /// Compile anyway.
    Compile(ProjectInsId),
    /// Settle a dedicated project.
    Settle(ProjectInsId),
    /// Compiled from computing thread.
    Compiled(CompiledArtifact<F>),
    /// Change the watching entry.
    ChangeTask(ProjectInsId, TaskInputs),
    /// Font changes.
    Font(Arc<F::FontResolver>),
    /// Creation timestamp changes.
    CreationTimestamp(Option<i64>),
    /// Memory file changes.
    Memory(MemoryEvent),
    /// File system event.
    Fs(FilesystemEvent),
    /// Save a file.
    Save(ImmutPath),
}

impl<F: CompilerFeat> fmt::Debug for Interrupt<F> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Interrupt::Compile(id) => write!(f, "Compile({id:?})"),
            Interrupt::Settle(id) => write!(f, "Settle({id:?})"),
            Interrupt::Compiled(artifact) => write!(f, "Compiled({:?})", artifact.id()),
            Interrupt::ChangeTask(id, change) => {
                write!(f, "ChangeTask({id:?}, entry={:?})", change.entry.is_some())
            }
            Interrupt::Font(..) => write!(f, "Font(..)"),
            Interrupt::CreationTimestamp(ts) => write!(f, "CreationTimestamp({ts:?})"),
            Interrupt::Memory(..) => write!(f, "Memory(..)"),
            Interrupt::Fs(..) => write!(f, "Fs(..)"),
            Interrupt::Save(path) => write!(f, "Save({path:?})"),
        }
    }
}

fn no_reason() -> CompileSignal {
    CompileSignal::default()
}

fn reason_by_mem() -> CompileSignal {
    CompileSignal {
        by_mem_events: true,
        ..CompileSignal::default()
    }
}

fn reason_by_fs() -> CompileSignal {
    CompileSignal {
        by_fs_events: true,
        ..CompileSignal::default()
    }
}

fn reason_by_entry_change() -> CompileSignal {
    CompileSignal {
        by_entry_update: true,
        ..CompileSignal::default()
    }
}

/// A tagged memory event with logical tick.
struct TaggedMemoryEvent {
    /// The logical tick when the event is received.
    logical_tick: usize,
    /// The memory event happened.
    event: MemoryEvent,
}

/// The compiler server options.
pub struct CompileServerOpts<F: CompilerFeat, Ext> {
    /// The compilation handler.
    pub handler: Arc<dyn CompileHandler<F, Ext>>,
    /// Whether to ignoring the first fs sync event.
    pub ignore_first_sync: bool,
    /// Specifies the current export target.
    pub export_target: ExportTarget,
    /// Whether to run in syntax-only mode.
    pub syntax_only: bool,
}

impl<F: CompilerFeat + Send + Sync + 'static, Ext: 'static> Default for CompileServerOpts<F, Ext> {
    fn default() -> Self {
        Self {
            handler: Arc::new(std::marker::PhantomData),
            ignore_first_sync: false,
            syntax_only: false,
            export_target: ExportTarget::Paged,
        }
    }
}

const FILE_MISSING_ERROR_MSG: EcoString = EcoString::inline("t-file-missing");
/// The file missing error constant.
pub const FILE_MISSING_ERROR: FileError = FileError::Other(Some(FILE_MISSING_ERROR_MSG));

/// The synchronous compiler that runs on one project or multiple projects.
pub struct ProjectCompiler<F: CompilerFeat, Ext> {
    /// The compilation handle.
    pub handler: Arc<dyn CompileHandler<F, Ext>>,
    /// Specifies the current export target.
    export_target: ExportTarget,
    /// Whether to run in syntax-only mode.
    syntax_only: bool,
    /// Channel for sending interrupts to the compiler actor.
    dep_tx: mpsc::UnboundedSender<NotifyMessage>,
    /// Whether to ignore the first sync event.
    pub ignore_first_sync: bool,

    /// The current logical tick.
    logical_tick: usize,
    /// Last logical tick when invalidation is caused by shadow update.
    dirty_shadow_logical_tick: usize,
    /// Estimated latest set of shadow files.
    estimated_shadow_files: HashSet<Arc<Path>>,

    /// The primary state.
    pub primary: ProjectInsState<F, Ext>,
    /// The states for dedicate tasks
    pub dedicates: Vec<ProjectInsState<F, Ext>>,
    /// The project file dependencies.
    deps: ProjectDeps,
}

impl<F: CompilerFeat + Send + Sync + 'static, Ext: Default + 'static> ProjectCompiler<F, Ext> {
    /// Creates a compiler with options
    pub fn new(
        verse: CompilerUniverse<F>,
        dep_tx: mpsc::UnboundedSender<NotifyMessage>,
        CompileServerOpts {
            handler,
            ignore_first_sync,
            export_target,
            syntax_only,
        }: CompileServerOpts<F, Ext>,
    ) -> Self {
        let primary = Self::create_project(
            ProjectInsId("primary".into()),
            verse,
            export_target,
            syntax_only,
            handler.clone(),
        );
        Self {
            handler,
            dep_tx,
            export_target,
            syntax_only,

            logical_tick: 1,
            dirty_shadow_logical_tick: 0,

            estimated_shadow_files: Default::default(),
            ignore_first_sync,

            primary,
            deps: Default::default(),
            dedicates: vec![],
        }
    }

    /// Creates a snapshot of the primary project.
    pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
        self.primary.snapshot()
    }

    /// Compiles the document once.
    pub fn compile_once(&mut self) -> CompiledArtifact<F> {
        let snap = self.primary.make_snapshot();
        ProjectInsState::run_compile(
            self.handler.clone(),
            snap,
            self.export_target,
            self.syntax_only,
        )()
    }

    /// Gets the iterator of all projects.
    pub fn projects(&mut self) -> impl Iterator<Item = &mut ProjectInsState<F, Ext>> {
        std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut())
    }

    fn create_project(
        id: ProjectInsId,
        verse: CompilerUniverse<F>,
        export_target: ExportTarget,
        syntax_only: bool,
        handler: Arc<dyn CompileHandler<F, Ext>>,
    ) -> ProjectInsState<F, Ext> {
        ProjectInsState {
            id,
            ext: Default::default(),
            syntax_only,
            verse,
            reason: no_reason(),
            cached_snapshot: None,
            handler,
            export_target,
            latest_compilation: OnceLock::default(),
            latest_success_doc: None,
            deps: Default::default(),
            committed_revision: 0,
        }
    }

    /// Find a project by id, but with less borrow checker restriction.
    pub fn find_project<'a>(
        primary: &'a mut ProjectInsState<F, Ext>,
        dedicates: &'a mut [ProjectInsState<F, Ext>],
        id: &ProjectInsId,
    ) -> &'a mut ProjectInsState<F, Ext> {
        if id == &primary.id {
            return primary;
        }

        dedicates.iter_mut().find(|e| e.id == *id).unwrap()
    }

    /// Clear all dedicate projects.
    pub fn clear_dedicates(&mut self) {
        self.dedicates.clear();
    }

    /// Restart a dedicate project.
    pub fn restart_dedicate(&mut self, group: &str, entry: EntryState) -> Result<ProjectInsId> {
        let id = ProjectInsId(group.into());

        let verse = CompilerUniverse::<F>::new_raw(
            entry,
            self.primary.verse.features.clone(),
            Some(self.primary.verse.inputs().clone()),
            self.primary.verse.vfs().fork(),
            self.primary.verse.registry.clone(),
            self.primary.verse.font_resolver.clone(),
            self.primary.verse.creation_timestamp,
        );

        let mut proj = Self::create_project(
            id.clone(),
            verse,
            self.export_target,
            self.syntax_only,
            self.handler.clone(),
        );
        proj.reason.merge(reason_by_entry_change());

        self.remove_dedicates(&id);
        self.dedicates.push(proj);

        Ok(id)
    }

    fn remove_dedicates(&mut self, id: &ProjectInsId) {
        let proj = self.dedicates.iter().position(|e| e.id == *id);
        if let Some(idx) = proj {
            // Resets the handle state, e.g. notified revision
            self.handler.notify_removed(id);
            self.deps.project_deps.remove_mut(id);

            let _proj = self.dedicates.remove(idx);
            // todo: kill compilations

            let res = self
                .dep_tx
                .send(NotifyMessage::SyncDependency(Box::new(self.deps.clone())));
            log_send_error("dep_tx", res);
        } else {
            log::warn!("ProjectCompiler: settle project not found {id:?}");
        }
    }

    /// Process an interrupt.
    pub fn process(&mut self, intr: Interrupt<F>) {
        // todo: evcit cache
        self.process_inner(intr);
        // Customized Project Compilation Handler
        self.handler.clone().on_any_compile_reason(self);
    }

    fn process_inner(&mut self, intr: Interrupt<F>) {
        match intr {
            Interrupt::Compile(id) => {
                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
                // Increment the revision anyway.
                proj.verse.increment_revision(|verse| {
                    verse.flush();
                });

                proj.reason.merge(reason_by_entry_change());
            }
            Interrupt::Compiled(artifact) => {
                let proj =
                    Self::find_project(&mut self.primary, &mut self.dedicates, artifact.id());

                let processed = proj.process_compile(artifact);

                if processed {
                    self.deps
                        .project_deps
                        .insert_mut(proj.id.clone(), proj.deps.clone());

                    let event = NotifyMessage::SyncDependency(Box::new(self.deps.clone()));
                    let err = self.dep_tx.send(event);
                    log_send_error("dep_tx", err);
                }
            }
            Interrupt::Settle(id) => {
                self.remove_dedicates(&id);
            }
            Interrupt::ChangeTask(id, change) => {
                let proj = Self::find_project(&mut self.primary, &mut self.dedicates, &id);
                proj.verse.increment_revision(|verse| {
                    if let Some(inputs) = change.inputs.clone() {
                        verse.set_inputs(inputs);
                    }

                    if let Some(entry) = change.entry.clone() {
                        let res = verse.mutate_entry(entry);
                        if let Err(err) = res {
                            log::error!("ProjectCompiler: change entry error: {err:?}");
                        }
                    }
                });

                // After incrementing the revision
                if let Some(entry) = change.entry {
                    // todo: dedicate suspended
                    if entry.is_inactive() {
                        log::info!("ProjectCompiler: removing diag");
                        self.handler.status(proj.verse.revision.get(), {
                            CompileReport {
                                id: proj.id.clone(),
                                compiling_id: None,
                                page_count: 0,
                                status: CompileStatusEnum::Suspend,
                            }
                        });
                    }

                    // Forget the document state of previous entry.
                    proj.latest_success_doc = None;
                }

                proj.reason.merge(reason_by_entry_change());
            }

            Interrupt::Font(fonts) => {
                self.projects().for_each(|proj| {
                    let font_changed = proj.verse.increment_revision(|verse| {
                        verse.set_fonts(fonts.clone());
                        verse.font_changed()
                    });
                    if font_changed {
                        // todo: reason_by_font_change
                        proj.reason.merge(reason_by_entry_change());
                    }
                });
            }
            Interrupt::CreationTimestamp(creation_timestamp) => {
                self.projects().for_each(|proj| {
                    let timestamp_changed = proj.verse.increment_revision(|verse| {
                        verse.set_creation_timestamp(creation_timestamp);
                        // Creation timestamp changes affect compilation
                        verse.creation_timestamp_changed()
                    });
                    if timestamp_changed {
                        proj.reason.merge(reason_by_entry_change());
                    }
                });
            }
            Interrupt::Memory(event) => {
                log::debug!("ProjectCompiler: memory event incoming");

                // Emulate memory changes.
                let mut files = HashSet::new();
                if matches!(event, MemoryEvent::Sync(..)) {
                    std::mem::swap(&mut files, &mut self.estimated_shadow_files);
                }

                let (MemoryEvent::Sync(e) | MemoryEvent::Update(e)) = &event;
                for path in &e.removes {
                    self.estimated_shadow_files.remove(path);
                    files.insert(Arc::clone(path));
                }
                for (path, _) in &e.inserts {
                    self.estimated_shadow_files.insert(Arc::clone(path));
                    files.remove(path);
                }

                // If there is no invalidation happening, apply memory changes directly.
                if files.is_empty() && self.dirty_shadow_logical_tick == 0 {
                    let changes = std::iter::repeat_n(event, 1 + self.dedicates.len());
                    let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());
                    for (proj, event) in proj.zip(changes) {
                        log::debug!("memory update: vfs {:#?}", proj.verse.vfs().display());
                        let vfs_changed = proj.verse.increment_revision(|verse| {
                            log::debug!("memory update: {:?}", proj.id);
                            Self::apply_memory_changes(&mut verse.vfs(), event.clone());
                            log::debug!("memory update: changed {}", verse.vfs_changed());
                            verse.vfs_changed()
                        });
                        if vfs_changed {
                            proj.reason.merge(reason_by_mem());
                        }
                        log::debug!("memory update: vfs after {:#?}", proj.verse.vfs().display());
                    }
                    return;
                }

                // Otherwise, send upstream update event.
                // Also, record the logical tick when shadow is dirty.
                self.dirty_shadow_logical_tick = self.logical_tick;
                let event = NotifyMessage::UpstreamUpdate(UpstreamUpdateEvent {
                    invalidates: files.into_iter().collect(),
                    opaque: Box::new(TaggedMemoryEvent {
                        logical_tick: self.logical_tick,
                        event,
                    }),
                });
                let err = self.dep_tx.send(event);
                log_send_error("dep_tx", err);
            }
            Interrupt::Save(event) => {
                let changes = std::iter::repeat_n(&event, 1 + self.dedicates.len());
                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());

                for (proj, saved_path) in proj.zip(changes) {
                    log::debug!(
                        "ProjectCompiler({}, rev={}): save changes",
                        proj.verse.revision.get(),
                        proj.id
                    );

                    // todo: only emit if saved_path is related
                    let _ = saved_path;

                    proj.reason.merge(reason_by_fs());
                }
            }
            Interrupt::Fs(event) => {
                log::debug!("ProjectCompiler: fs event incoming {event:?}");

                // Apply file system changes.
                let dirty_tick = &mut self.dirty_shadow_logical_tick;
                let (changes, is_sync, event) = event.split_with_is_sync();
                let changes = std::iter::repeat_n(changes, 1 + self.dedicates.len());
                let proj = std::iter::once(&mut self.primary).chain(self.dedicates.iter_mut());

                for (proj, changes) in proj.zip(changes) {
                    log::debug!(
                        "ProjectCompiler({}, rev={}): fs changes applying",
                        proj.verse.revision.get(),
                        proj.id
                    );

                    proj.verse.increment_revision(|verse| {
                        let mut vfs = verse.vfs();

                        // Handle delayed upstream update event before applying file system
                        // changes
                        if Self::apply_delayed_memory_changes(&mut vfs, dirty_tick, &event)
                            .is_none()
                        {
                            log::warn!("ProjectCompiler: unknown upstream update event");

                            // Actual a delayed memory event.
                            proj.reason.merge(reason_by_mem());
                        }
                        vfs.notify_fs_changes(changes);
                    });

                    log::debug!(
                        "ProjectCompiler({},rev={}): fs changes applied, {is_sync}",
                        proj.id,
                        proj.verse.revision.get(),
                    );

                    if !self.ignore_first_sync || !is_sync {
                        proj.reason.merge(reason_by_fs());
                    }
                }
            }
        }
    }

    /// Apply delayed memory changes to underlying compiler.
    fn apply_delayed_memory_changes(
        verse: &mut RevisingVfs<'_, F::AccessModel>,
        dirty_shadow_logical_tick: &mut usize,
        event: &Option<UpstreamUpdateEvent>,
    ) -> Option<()> {
        // Handle delayed upstream update event before applying file system changes
        if let Some(event) = event {
            let TaggedMemoryEvent {
                logical_tick,
                event,
            } = event.opaque.as_ref().downcast_ref()?;

            // Recovery from dirty shadow state.
            if logical_tick == dirty_shadow_logical_tick {
                *dirty_shadow_logical_tick = 0;
            }

            Self::apply_memory_changes(verse, event.clone());
        }

        Some(())
    }

    /// Apply memory changes to underlying compiler.
    fn apply_memory_changes(vfs: &mut RevisingVfs<'_, F::AccessModel>, event: MemoryEvent) {
        if matches!(event, MemoryEvent::Sync(..)) {
            vfs.reset_shadow();
        }
        match event {
            MemoryEvent::Update(event) | MemoryEvent::Sync(event) => {
                for path in event.removes {
                    let _ = vfs.unmap_shadow(&path);
                }
                for (path, snap) in event.inserts {
                    let _ = vfs.map_shadow(&path, snap);
                }
            }
        }
    }
}

/// A project instance state.
pub struct ProjectInsState<F: CompilerFeat, Ext> {
    /// The project instance id.
    pub id: ProjectInsId,
    /// The extension
    pub ext: Ext,
    /// The underlying universe.
    pub verse: CompilerUniverse<F>,
    /// Specifies the current export target.
    pub export_target: ExportTarget,
    /// Whether to run in syntax-only mode.
    pub syntax_only: bool,
    /// The reason to compile.
    pub reason: CompileSignal,
    /// The compilation handle.
    pub handler: Arc<dyn CompileHandler<F, Ext>>,
    /// The file dependencies.
    deps: EcoVec<ImmutPath>,

    /// The latest compute graph (snapshot), derived lazily from
    /// `latest_compilation` as needed.
    pub cached_snapshot: Option<Arc<WorldComputeGraph<F>>>,
    /// The latest compilation.
    pub latest_compilation: OnceLock<CompiledArtifact<F>>,
    /// The latest successly compiled document.
    pub latest_success_doc: Option<TypstDocument>,

    committed_revision: usize,
}

impl<F: CompilerFeat, Ext: 'static> ProjectInsState<F, Ext> {
    /// Gets a snapshot of the project.
    pub fn snapshot(&mut self) -> Arc<WorldComputeGraph<F>> {
        match self.cached_snapshot.as_ref() {
            Some(snap) if snap.world().revision() == self.verse.revision => snap.clone(),
            _ => {
                let snap = self.make_snapshot();
                self.cached_snapshot = Some(snap.clone());
                snap
            }
        }
    }

    /// Creates a new snapshot of the project derived from `latest_compilation`.
    fn make_snapshot(&self) -> Arc<WorldComputeGraph<F>> {
        let world = self.verse.snapshot();
        let snap = CompileSnapshot {
            id: self.id.clone(),
            world,
            signal: self.reason,
            success_doc: self.latest_success_doc.clone(),
        };
        WorldComputeGraph::new(snap)
    }

    /// Compiles the document once if there is any reason and the entry is
    /// active. (this is used for experimenting typst.node compilations)
    #[must_use]
    pub fn may_compile2<'a>(
        &mut self,
        compute: impl FnOnce(&Arc<WorldComputeGraph<F>>) + 'a,
    ) -> Option<impl FnOnce() -> Arc<WorldComputeGraph<F>> + 'a> {
        if !self.reason.any() || self.verse.entry_state().is_inactive() {
            return None;
        }

        let snap = self.snapshot();
        self.reason = Default::default();
        Some(move || {
            compute(&snap);
            snap
        })
    }

    /// Compiles the document once if there is any reason and the entry is
    /// active.
    #[must_use]
    pub fn may_compile(
        &mut self,
        handler: &Arc<dyn CompileHandler<F, Ext>>,
    ) -> Option<impl FnOnce() -> CompiledArtifact<F> + 'static> {
        if !self.reason.any() || self.verse.entry_state().is_inactive() {
            return None;
        }

        let snap = self.snapshot();
        self.reason = Default::default();

        Some(Self::run_compile(
            handler.clone(),
            snap,
            self.export_target,
            self.syntax_only,
        ))
    }

    /// Compile the document once.
    fn run_compile(
        h: Arc<dyn CompileHandler<F, Ext>>,
        graph: Arc<WorldComputeGraph<F>>,
        export_target: ExportTarget,
        syntax_only: bool,
    ) -> impl FnOnce() -> CompiledArtifact<F> {
        let start = tinymist_std::time::Instant::now();

        // todo unwrap main id
        let id = graph.world().main_id().unwrap();
        let revision = graph.world().revision().get();

        h.status(revision, {
            CompileReport {
                id: graph.snap.id.clone(),
                compiling_id: Some(id),
                page_count: 0,
                status: CompileStatusEnum::Compiling,
            }
        });

        move || {
            let compiled = if syntax_only {
                let main = graph.snap.world.main();
                let source_res = graph.world().source(main).at(Span::from_range(main, 0..0));
                let syntax_res = source_res.and_then(|source| {
                    let errors = source.root().errors();
                    if errors.is_empty() {
                        Ok(())
                    } else {
                        Err(errors.into_iter().map(|s| s.into()).collect())
                    }
                });
                let diag = Arc::new(DiagnosticsTask::from_errors(syntax_res.err()));

                CompiledArtifact {
                    diag,
                    graph,
                    doc: None,
                    deps: OnceLock::default(),
                }
            } else {
                CompiledArtifact::from_graph(graph, matches!(export_target, ExportTarget::Html))
            };

            let res = CompileStatusResult {
                diag: (compiled.warning_cnt() + compiled.error_cnt()) as u32,
                elapsed: start.elapsed(),
            };
            let rep = CompileReport {
                id: compiled.id().clone(),
                compiling_id: Some(id),
                page_count: compiled.doc.as_ref().map_or(0, |doc| doc.num_of_pages()),
                status: match &compiled.doc {
                    Some(..) => CompileStatusEnum::CompileSuccess(res),
                    None if res.diag == 0 => CompileStatusEnum::CompileSuccess(res),
                    None => CompileStatusEnum::CompileError(res),
                },
            };

            // todo: we need to check revision for really concurrent compilation
            log_compile_report(&rep);

            if compiled
                .diagnostics()
                .any(|d| d.message == FILE_MISSING_ERROR_MSG)
            {
                return compiled;
            }

            h.status(revision, rep);
            h.notify_compile(&compiled);
            compiled
        }
    }

    fn process_compile(&mut self, artifact: CompiledArtifact<F>) -> bool {
        let world = &artifact.snap.world;
        let compiled_revision = world.revision().get();
        if self.committed_revision >= compiled_revision {
            return false;
        }

        // Updates state.
        let doc = artifact.doc.clone();
        self.committed_revision = compiled_revision;
        if doc.is_some() {
            self.latest_success_doc = doc;
        }
        self.cached_snapshot = None; // invalidate; will be recomputed on demand

        // Notifies the new file dependencies.
        let mut deps = eco_vec![];
        world.iter_dependencies(&mut |dep| {
            if let Ok(x) = world.file_path(dep).and_then(|e| e.to_err()) {
                deps.push(x.into())
            }
        });

        self.deps = deps.clone();

        let mut world = world.clone();

        let is_primary = self.id == ProjectInsId("primary".into());

        // Trigger an evict task.
        spawn_cpu(move || {
            let evict_start = tinymist_std::time::Instant::now();
            if is_primary {
                comemo::evict(10);

                // Since all the projects share the same cache, we need to evict the cache
                // on the primary instance for all the projects.
                world.evict_source_cache(30);
            }
            world.evict_vfs(60);
            let elapsed = evict_start.elapsed();
            log::debug!("ProjectCompiler: evict cache in {elapsed:?}");
        });

        true
    }
}

fn log_compile_report(rep: &CompileReport) {
    log::info!("{}", rep.message());
}

#[inline]
fn log_send_error<T>(chan: &'static str, res: Result<(), mpsc::error::SendError<T>>) -> bool {
    res.map_err(|err| log::warn!("ProjectCompiler: send to {chan} error: {err}"))
        .is_ok()
}

#[derive(Debug, Clone, Default)]
struct ProjectDeps {
    project_deps: rpds::RedBlackTreeMapSync<ProjectInsId, EcoVec<ImmutPath>>,
}

impl NotifyDeps for ProjectDeps {
    fn dependencies(&self, f: &mut dyn FnMut(&ImmutPath)) {
        for deps in self.project_deps.values().flat_map(|e| e.iter()) {
            f(deps);
        }
    }
}

// todo: move me to tinymist-std
#[cfg(not(target_arch = "wasm32"))]
/// Spawns a CPU thread to run a computing-heavy task.
pub fn spawn_cpu<F>(func: F)
where
    F: FnOnce() + Send + 'static,
{
    rayon::spawn(func);
}

#[cfg(target_arch = "wasm32")]
/// Spawns a CPU thread to run a computing-heavy task.
pub fn spawn_cpu<F>(func: F)
where
    F: FnOnce() + Send + 'static,
{
    func();
}
