//! Project task models.

use std::{hash::Hash, path::PathBuf};

use serde::{Deserialize, Serialize};

use super::{Id, Pages, PathPattern, PdfStandard, Scalar, TaskWhen};

/// A project task application specifier. This is used for specifying tasks to
/// run in a project. When the language service notifies an update event of the
/// project, it will check whether any associated tasks need to be run.
///
/// Each task can have different timing and conditions for running. See
/// [`TaskWhen`] for more information.
///
/// The available task types listed in the [`ProjectTask`] only represent the
/// direct formats supported by the typst compiler. More task types can be
/// customized by the [`ExportTransform`].
///
/// ## Examples
///
/// Export a JSON file with the pdfpc notes of the document:
///
/// ```bash
/// tinymist project query main.typ --format json --selector "<pdfpc-notes>" --field value --one
/// ```
///
/// Export a PDF file and then runs a ghostscript command to compress it:
///
/// ```bash
/// tinymist project compile main.typ --pipe 'import "@local/postprocess:0.0.1": ghostscript; ghostscript(output.path)'
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "type")]
pub struct ApplyProjectTask {
    /// The task's ID.
    pub id: Id,
    /// The document's ID.
    pub document: Id,
    /// The task to run.
    #[serde(flatten)]
    pub task: ProjectTask,
}

impl ApplyProjectTask {
    /// Returns the document's ID.
    pub fn doc_id(&self) -> &Id {
        &self.document
    }

    /// Returns the task's ID.
    pub fn id(&self) -> &Id {
        &self.id
    }
}

/// A project task specifier. This structure specifies the arguments for a task.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "type")]
pub enum ProjectTask {
    /// A preview task.
    Preview(PreviewTask),
    /// An export PDF task.
    ExportPdf(ExportPdfTask),
    /// An export PNG task.
    ExportPng(ExportPngTask),
    /// An export SVG task.
    ExportSvg(ExportSvgTask),
    /// An export HTML task.
    ExportHtml(ExportHtmlTask),
    /// An export bundle task.
    ExportBundle(ExportBundleTask),
    /// An export HTML task.
    ExportSvgHtml(ExportHtmlTask),
    /// An export Markdown task.
    ExportMd(ExportMarkdownTask),
    /// An export TeX task.
    ExportTeX(ExportTeXTask),
    /// An export Text task.
    ExportText(ExportTextTask),
    /// An query task.
    Query(QueryTask),
    // todo: compatibility
    // An export task of another type.
    // Other(serde_json::Value),
}

impl ProjectTask {
    /// Returns the timing of executing the task.
    pub fn when(&self) -> Option<&TaskWhen> {
        Some(match self {
            Self::Preview(task) => &task.when,
            Self::ExportPdf(..)
            | Self::ExportPng(..)
            | Self::ExportSvg(..)
            | Self::ExportHtml(..)
            | Self::ExportBundle(..)
            | Self::ExportSvgHtml(..)
            | Self::ExportMd(..)
            | Self::ExportTeX(..)
            | Self::ExportText(..)
            | Self::Query(..) => &self.as_export()?.when,
        })
    }

    /// Returns the export configuration of a task.
    pub fn as_export(&self) -> Option<&ExportTask> {
        Some(match self {
            Self::Preview(..) => return None,
            Self::ExportPdf(task) => &task.export,
            Self::ExportPng(task) => &task.export,
            Self::ExportSvg(task) => &task.export,
            Self::ExportHtml(task) => &task.export,
            Self::ExportBundle(task) => &task.export,
            Self::ExportSvgHtml(task) => &task.export,
            Self::ExportTeX(task) => &task.export,
            Self::ExportMd(task) => &task.export,
            Self::ExportText(task) => &task.export,
            Self::Query(task) => &task.export,
        })
    }

    /// Returns the export configuration of a task.
    pub fn as_export_mut(&mut self) -> Option<&mut ExportTask> {
        Some(match self {
            Self::Preview(..) => return None,
            Self::ExportPdf(task) => &mut task.export,
            Self::ExportPng(task) => &mut task.export,
            Self::ExportSvg(task) => &mut task.export,
            Self::ExportHtml(task) => &mut task.export,
            Self::ExportBundle(task) => &mut task.export,
            Self::ExportSvgHtml(task) => &mut task.export,
            Self::ExportTeX(task) => &mut task.export,
            Self::ExportMd(task) => &mut task.export,
            Self::ExportText(task) => &mut task.export,
            Self::Query(task) => &mut task.export,
        })
    }

    /// Returns extension of the artifact.
    pub fn extension(&self) -> &str {
        match self {
            Self::ExportPdf { .. } => "pdf",
            Self::Preview(..) | Self::ExportSvgHtml { .. } | Self::ExportHtml { .. } => "html",
            Self::ExportBundle { .. } => "",
            Self::ExportMd { .. } => "md",
            Self::ExportTeX { .. } => "tex",
            Self::ExportText { .. } => "txt",
            Self::ExportSvg { .. } => "svg",
            Self::ExportPng { .. } => "png",
            Self::Query(QueryTask {
                format,
                output_extension,
                ..
            }) => output_extension.as_deref().unwrap_or(format),
        }
    }
}

/// A preview task specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct PreviewTask {
    /// When to run the task. See [`TaskWhen`] for more
    /// information.
    pub when: TaskWhen,
}

/// An export task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportTask {
    /// When to run the task
    pub when: TaskWhen,
    /// The output path pattern.
    pub output: Option<PathPattern>,
    /// The task's transforms.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub transform: Vec<ExportTransform>,
}

impl ExportTask {
    /// Creates a new unmounted export task.
    pub fn new(when: TaskWhen) -> Self {
        Self {
            when,
            output: None,
            transform: Vec::new(),
        }
    }

    /// Pretty prints the output whenever possible.
    pub fn apply_pretty(&mut self) {
        self.transform
            .push(ExportTransform::Pretty { script: None });
    }
}

/// A page merge specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(default)]
pub struct PageMerge {
    /// The gap between pages (in pt).
    pub gap: Option<String>,
}

/// A project export transform specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ExportTransform {
    /// Only pick a subset of pages.
    Pages {
        /// The page ranges to export.
        ranges: Vec<Pages>,
    },
    /// Merge pages into a single page.
    Merge {
        /// The gap between pages (typst code expression, e.g. `1pt`).
        gap: Option<String>,
    },
    /// Execute a transform script.
    Script {
        /// The postprocess script (typst script) to run.
        #[serde(skip_serializing_if = "Option::is_none", default)]
        script: Option<String>,
    },
    /// Uses a pretty printer to format the output.
    Pretty {
        /// The pretty command (typst script) to run.
        ///
        /// If not provided, the default pretty printer will be used.
        /// Note: the builtin one may be only effective for json outputs.
        #[serde(skip_serializing_if = "Option::is_none", default)]
        script: Option<String>,
    },
}

/// An export pdf task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportPdfTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
    /// Which pages to export. When unspecified, all pages are exported.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub pages: Option<Vec<Pages>>,
    /// One (or multiple comma-separated) PDF standards that Typst will enforce
    /// conformance with.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub pdf_standards: Vec<PdfStandard>,
    /// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
    /// document is written to provide a baseline of accessibility. In some
    /// circumstances (for example when trying to reduce the size of a document)
    /// it can be desirable to disable tagged PDF.
    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
    pub no_pdf_tags: bool,
    /// The document's creation date formatted as a UNIX timestamp (in seconds).
    ///
    /// For more information, see <https://reproducible-builds.org/specs/source-date-epoch/>.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub creation_timestamp: Option<i64>,
}

/// An export png task specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportPngTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
    /// Which pages to export. When unspecified, all pages are exported.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub pages: Option<Vec<Pages>>,
    /// The page template to use for multiple pages.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub page_number_template: Option<String>,
    /// The page merge specifier.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub merge: Option<PageMerge>,
    /// The PPI (pixels per inch) to use for PNG export.
    pub ppi: Scalar,
    /// The expression constructing background fill color (in typst script).
    /// e.g. `#ffffff`, `#000000`, `rgba(255, 255, 255, 0.5)`.
    ///
    /// If not provided, the default background color specified in the document
    /// will be used.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub fill: Option<String>,
}

/// An export svg task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportSvgTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
    /// The page template to use for multiple pages.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub page_number_template: Option<String>,
    /// Which pages to export. When unspecified, all pages are exported.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub pages: Option<Vec<Pages>>,
    /// The page merge specifier.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub merge: Option<PageMerge>,
}

/// An export html task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportHtmlTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
}

/// An export bundle task specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportBundleTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
    /// Which pages to export in PDF documents. When unspecified, all pages are
    /// exported.
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub pages: Option<Vec<Pages>>,
    /// One (or multiple comma-separated) PDF standards that Typst will enforce
    /// conformance with for PDF documents.
    #[serde(skip_serializing_if = "Vec::is_empty", default)]
    pub pdf_standards: Vec<PdfStandard>,
    /// Disable tagged PDF output for PDF documents.
    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
    pub no_pdf_tags: bool,
    /// The document's creation date formatted as a UNIX timestamp (in seconds).
    #[serde(skip_serializing_if = "Option::is_none", default)]
    pub creation_timestamp: Option<i64>,
    /// The PPI (pixels per inch) to use for PNG documents.
    pub ppi: Scalar,
}

/// An export markdown task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportMarkdownTask {
    /// The processor to use for the markdown export.
    pub processor: Option<String>,
    /// The path of external assets directory.
    pub assets_path: Option<PathBuf>,
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
}

/// An export TeX task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportTeXTask {
    /// The processor to use for the TeX export.
    pub processor: Option<String>,
    /// The path of external assets directory.
    pub assets_path: Option<PathBuf>,
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
}

/// An export text task specifier.
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExportTextTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
}

/// An export query task specifier.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct QueryTask {
    /// The shared export arguments.
    #[serde(flatten)]
    pub export: ExportTask,
    /// The format to serialize in. Can be `json`, `yaml`, or `txt`,
    pub format: String,
    /// Uses a different output extension from the one inferring from the
    /// [`Self::format`].
    pub output_extension: Option<String>,
    /// Defines which elements to retrieve.
    pub selector: String,
    /// Extracts just one field from all retrieved elements.
    pub field: Option<String>,
    /// Expects and retrieves exactly one element.
    pub one: bool,
}
