pub use tinymist_world::args::{ExportTarget, OutputFormat, PdfStandard, TaskWhen};

use core::fmt;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::{path::Path, str::FromStr};

use serde::{Deserialize, Serialize};
use tinymist_std::ImmutPath;
use tinymist_std::error::prelude::*;
use tinymist_std::path::{PathClean, unix_slash};
use tinymist_world::vfs::WorkspaceResolver;
use tinymist_world::{CompilerFeat, CompilerWorld, EntryReader, EntryState};
use typst::diag::EcoString;
use typst::layout::PageRanges;
use typst::syntax::FileId;
use typst_shim::syntax::{RootedPathExt, VirtualPathExt};

/// A scalar that is not NaN.
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct Scalar(f32);

impl TryFrom<f32> for Scalar {
    type Error = &'static str;

    fn try_from(value: f32) -> Result<Self, Self::Error> {
        if value.is_nan() {
            Err("NaN is not a valid scalar value")
        } else {
            Ok(Scalar(value))
        }
    }
}

impl Scalar {
    /// Converts the scalar to an f32.
    pub fn to_f32(self) -> f32 {
        self.0
    }
}

impl PartialEq for Scalar {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

impl Eq for Scalar {}

impl Hash for Scalar {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.0.to_bits().hash(state);
    }
}

impl PartialOrd for Scalar {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Scalar {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.0.partial_cmp(&other.0).unwrap()
    }
}

/// A project ID.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Id(String);

impl Id {
    /// Creates a new project Id.
    pub fn new(s: String) -> Self {
        Id(s)
    }

    /// Creates a new project Id from a world.
    pub fn from_world<F: CompilerFeat>(world: &CompilerWorld<F>, ctx: CtxPath) -> Option<Self> {
        let entry = world.entry_state();
        let id = unix_slash(entry.main()?.vpath().as_rootless_path_compat());

        // todo: entry root may not be set, so we should use the cwd
        let path = &ResourcePath::from_user_sys(Path::new(&id), ctx);
        Some(path.into())
    }
}

impl From<&ResourcePath> for Id {
    fn from(value: &ResourcePath) -> Self {
        Id::new(value.to_string())
    }
}

impl fmt::Display for Id {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

/// The path pattern that could be substituted.
///
/// # Examples
/// - `$root` is the root of the project.
/// - `$root/$dir` is the parent directory of the input (main) file.
/// - `$root/main` will help store pdf file to `$root/main.pdf` constantly.
/// - (default) `$root/$dir/$name` will help store pdf file along with the input
///   file.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PathPattern(pub EcoString);

impl fmt::Display for PathPattern {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl PathPattern {
    /// Creates a new path pattern.
    pub fn new(pattern: &str) -> Self {
        Self(pattern.into())
    }

    /// Creates a new path pattern from a string.
    pub fn relative_to(self, base: &Path) -> Self {
        if self.0.is_empty() {
            return self;
        }

        let path = Path::new(self.0.as_str());
        if path.is_absolute() {
            let rel_path = tinymist_std::path::diff(path, base);

            match rel_path {
                Some(rel) => PathPattern(unix_slash(&rel).into()),
                None => self,
            }
        } else {
            self
        }
    }

    /// Substitutes the path pattern with `$root`, and `$dir/$name`.
    pub fn substitute(&self, entry: &EntryState) -> Option<ImmutPath> {
        self.substitute_impl(entry.root(), entry.main())
    }

    #[comemo::memoize]
    fn substitute_impl(&self, root: Option<ImmutPath>, main: Option<FileId>) -> Option<ImmutPath> {
        log::debug!("Check path {main:?} and root {root:?} with output directory {self:?}");

        let (root, main) = root.zip(main)?;

        // Files in packages are not exported
        if WorkspaceResolver::is_package_file(main) {
            return None;
        }
        // Files without a path are not exported
        let path = main.vpath().realize(&root).ok()?;

        // todo: handle untitled path
        if let Ok(path) = path.strip_prefix("/untitled") {
            let tmp = std::env::temp_dir();
            let path = tmp.join("typst").join(path);
            return Some(path.as_path().into());
        }

        let path = path.strip_prefix(&root).ok()?;
        let dir = path.parent();
        let file_name = path.file_name().unwrap_or_default();

        let w = root.to_string_lossy();
        let f = file_name.to_string_lossy();
        let f = f.as_ref().strip_suffix(".typ").unwrap_or(f.as_ref());

        if self.0.is_empty() {
            let dest = dir
                .map(|d| root.join(d).join(f))
                .unwrap_or_else(|| root.join(f));
            return Some(dest.clean().into());
        }

        // replace all $root
        let mut path = self.0.replace("$root", &w);
        if let Some(dir) = dir {
            let d = dir.to_string_lossy();
            let d = if d.is_empty() { "." } else { d.as_ref() };
            path = path.replace("$dir", d);
        }
        path = path.replace("$name", f);

        Some(Path::new(path.as_str()).clean().into())
    }
}

/// Implements parsing of page ranges (`1-3`, `4`, `5-`, `-2`), used by the
/// `CompileCommand.pages` argument, through the `FromStr` trait instead of a
/// value parser, in order to generate better errors.
///
/// See also: <https://github.com/clap-rs/clap/issues/5065>
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Pages(pub RangeInclusive<Option<NonZeroUsize>>);

impl Pages {
    /// Selects the first page.
    pub const FIRST: Pages = Pages(NonZeroUsize::new(1)..=NonZeroUsize::new(1));
}

impl FromStr for Pages {
    type Err = &'static str;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        match value
            .split('-')
            .map(str::trim)
            .collect::<Vec<_>>()
            .as_slice()
        {
            [] | [""] => Err("page export range must not be empty"),
            [single_page] => {
                let page_number = parse_page_number(single_page)?;
                Ok(Pages(Some(page_number)..=Some(page_number)))
            }
            ["", ""] => Err("page export range must have start or end"),
            [start, ""] => Ok(Pages(Some(parse_page_number(start)?)..=None)),
            ["", end] => Ok(Pages(None..=Some(parse_page_number(end)?))),
            [start, end] => {
                let start = parse_page_number(start)?;
                let end = parse_page_number(end)?;
                if start > end {
                    Err("page export range must end at a page after the start")
                } else {
                    Ok(Pages(Some(start)..=Some(end)))
                }
            }
            [_, _, _, ..] => Err("page export range must have a single hyphen"),
        }
    }
}

/// The ranges of the pages to be exported as specified by the user.
pub fn exported_page_ranges(pages: &[Pages]) -> PageRanges {
    PageRanges::new(pages.iter().map(|p| p.0.clone()).collect())
}

impl fmt::Display for Pages {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let start = match self.0.start() {
            Some(start) => start.to_string(),
            None => String::from(""),
        };
        let end = match self.0.end() {
            Some(end) => end.to_string(),
            None => String::from(""),
        };
        write!(f, "{start}-{end}")
    }
}

impl serde::Serialize for Pages {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> serde::Deserialize<'de> for Pages {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        value.parse().map_err(serde::de::Error::custom)
    }
}

/// Parses a single page number.
fn parse_page_number(value: &str) -> Result<NonZeroUsize, &'static str> {
    if value == "0" {
        Err("page numbers start at one")
    } else {
        NonZeroUsize::from_str(value).map_err(|_| "not a valid page number")
    }
}

/// A resource path.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ResourcePath(EcoString, String);

impl fmt::Display for ResourcePath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}:{}", self.0, self.1)
    }
}

impl FromStr for ResourcePath {
    type Err = &'static str;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        let mut parts = value.split(':');
        let scheme = parts.next().ok_or("missing scheme")?;
        let path = parts.next().ok_or("missing path")?;
        if parts.next().is_some() {
            Err("too many colons")
        } else {
            Ok(ResourcePath(scheme.into(), path.to_string()))
        }
    }
}

impl serde::Serialize for ResourcePath {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

impl<'de> serde::Deserialize<'de> for ResourcePath {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        value.parse().map_err(serde::de::Error::custom)
    }
}

/// The path context.
// todo: The ctx path looks not quite maintainable. But we only target to make
// things correct, then back to make code good.
pub type CtxPath<'a, 'b> = (/* cwd */ &'a Path, /* lock_dir */ &'b Path);

impl ResourcePath {
    /// Creates a new resource path from a user passing system path.
    pub fn from_user_sys(inp: &Path, (cwd, lock_dir): CtxPath) -> Self {
        let abs = if inp.is_absolute() {
            inp.to_path_buf()
        } else {
            cwd.join(inp)
        };
        let resource_path = if let Some(rel) = tinymist_std::path::diff(&abs, lock_dir) {
            rel
        } else {
            abs
        };
        // todo: clean is not posix compatible,
        // for example /symlink/../file is not equivalent to /file
        let rel = unix_slash(&resource_path.clean());
        ResourcePath("file".into(), rel.to_string())
    }

    /// Creates a new resource path from a file id.
    pub fn from_file_id(id: FileId) -> Self {
        if let Some(package) = id.package_compat() {
            ResourcePath(
                "file_id".into(),
                format!(
                    "{package}{}",
                    unix_slash(id.vpath().as_rooted_path_compat())
                ),
            )
        } else {
            ResourcePath(
                "file_id".into(),
                format!("$root{}", unix_slash(id.vpath().as_rooted_path_compat())),
            )
        }
    }

    /// Converts the resource path to a path relative to the `base` (usually the
    /// directory storing the lockfile).
    pub fn relative_to(&self, base: &Path) -> Option<Self> {
        if self.0 == "file" {
            let path = Path::new(&self.1);
            if path.is_absolute() {
                let rel_path = tinymist_std::path::diff(path, base)?;
                Some(ResourcePath(self.0.clone(), unix_slash(&rel_path)))
            } else {
                Some(ResourcePath(self.0.clone(), self.1.clone()))
            }
        } else {
            Some(self.clone())
        }
    }

    /// Converts the resource path to a path relative to the `base` (usually the
    /// directory storing the lockfile).
    pub fn to_rel_path(&self, base: &Path) -> Option<PathBuf> {
        if self.0 == "file" {
            let path = Path::new(&self.1);
            if path.is_absolute() {
                Some(tinymist_std::path::diff(path, base).unwrap_or_else(|| path.to_owned()))
            } else {
                Some(path.to_owned())
            }
        } else {
            None
        }
    }

    /// Converts the resource path to an absolute file system path.
    pub fn to_abs_path(&self, base: &Path) -> Option<PathBuf> {
        if self.0 == "file" {
            let path = Path::new(&self.1);
            if path.is_absolute() {
                Some(path.to_owned())
            } else {
                Some(base.join(path))
            }
        } else {
            None
        }
    }
}

/// Utilities for output template processing.
/// Copied from typst-cli.
pub mod output_template {
    const INDEXABLE: [&str; 3] = ["{p}", "{0p}", "{n}"];

    /// Checks if the output template has indexable templates.
    pub fn has_indexable_template(output: &str) -> bool {
        INDEXABLE.iter().any(|template| output.contains(template))
    }

    /// Formats the output template with the given page number and total pages.
    /// Note: `this_page` is 1-based.
    pub fn format(output: &str, this_page: usize, total_pages: usize) -> String {
        // Find the base 10 width of number `i`
        fn width(i: usize) -> usize {
            1 + i.checked_ilog10().unwrap_or(0) as usize
        }

        let other_templates = ["{t}"];
        INDEXABLE
            .iter()
            .chain(other_templates.iter())
            .fold(output.to_string(), |out, template| {
                let replacement = match *template {
                    "{p}" => format!("{this_page}"),
                    "{0p}" | "{n}" => format!("{:01$}", this_page, width(total_pages)),
                    "{t}" => format!("{total_pages}"),
                    _ => unreachable!("unhandled template placeholder {template}"),
                };
                out.replace(template, replacement.as_str())
            })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use typst::syntax::VirtualPath;

    fn test_root() -> PathBuf {
        if cfg!(windows) {
            PathBuf::from(r"C:\dummy-root")
        } else {
            PathBuf::from("/dummy-root")
        }
    }

    fn test_entry(path: &str) -> EntryState {
        let root = test_root();
        EntryState::new_rooted(root.into(), Some(VirtualPath::new(path).unwrap()))
    }

    #[test]
    fn test_substitute_path() {
        let root = Path::new("/dummy-root");
        let entry = EntryState::new_rooted(
            root.into(),
            Some(VirtualPath::new("/dir1/dir2/file.txt").unwrap()),
        );

        assert_eq!(
            PathPattern::new("/substitute/$dir/$name").substitute(&entry),
            Some(PathBuf::from("/substitute/dir1/dir2/file.txt").into())
        );
        assert_eq!(
            PathPattern::new("/substitute/$dir/../$name").substitute(&entry),
            Some(PathBuf::from("/substitute/dir1/file.txt").into())
        );
        assert_eq!(
            PathPattern::new("/substitute/$name").substitute(&entry),
            Some(PathBuf::from("/substitute/file.txt").into())
        );
        assert_eq!(
            PathPattern::new("/substitute/target/$dir/$name").substitute(&entry),
            Some(PathBuf::from("/substitute/target/dir1/dir2/file.txt").into())
        );
    }

    #[test]
    fn test_substitute_path_keeps_workspace_root_relative() {
        let entry = test_entry("/Chapter 1.1.typ");

        assert_eq!(
            PathPattern::new("$dir/$name").substitute(&entry),
            Some(PathBuf::from("Chapter 1.1").into())
        );
    }

    #[test]
    fn test_substitute_default_path_matches_documented_behavior() {
        let root = test_root();
        let entry = test_entry("/Chapter 1.1.typ");

        assert_eq!(
            PathPattern::default().substitute(&entry),
            Some(root.join("Chapter 1.1").into())
        );
    }

    #[test]
    fn test_substitute_path_preserves_multi_dot_stem() {
        let root = test_root();
        let entry = test_entry("/chapters/Chapter 1.1.1.typ");

        assert_eq!(
            PathPattern::new("$root/out/$dir/$name").substitute(&entry),
            Some(root.join("out/chapters/Chapter 1.1.1").into())
        );
    }

    #[test]
    fn test_substitute_path_preserves_name_without_extension() {
        let root = test_root();
        let entry = test_entry("/README");

        assert_eq!(
            PathPattern::new("$root/$dir/$name").substitute(&entry),
            Some(root.join("README").into())
        );
    }
}
