use ecow::eco_format;
use typst_syntax::Spanned;

use crate::diag::{At, LineCol, LoadError, LoadedWithin, ReportTextPos, SourceResult};
use crate::engine::Engine;
use crate::foundations::{Str, Value, func, scope};
use crate::loading::{DataSource, Load};

/// Reads structured data from a YAML file.
///
/// The file must contain a valid YAML object or array. The YAML values will be
/// converted into corresponding Typst values as listed in the
/// @yaml:conversion[table below].
///
/// The function returns a dictionary, an array or, depending on the YAML file,
/// another YAML data type.
///
/// The YAML files in the example contain objects with authors as keys, each
/// with a sequence of their own submapping with the keys "title" and
/// "published".
///
/// = Example <example>
/// ```example
/// #let bookshelf(contents) = {
///   for (author, works) in contents {
///     author
///     for work in works [
///       - #work.title (#work.published)
///     ]
///   }
/// }
///
/// #bookshelf(
///   yaml("scifi-authors.yaml")
/// )
/// ```
///
/// = #short-or-long[Conversion][Conversion details] <conversion>
/// #docs-table(
///   table.header[YAML value][Converted into Typst],
///
///   [null-values (`null`, `~` or empty ` `)],
///   [`{none}`],
///
///   [boolean],
///   [@bool],
///
///   [number],
///   [@float or @int],
///
///   [string],
///   [@str],
///
///   [sequence],
///   [@array],
///
///   [mapping],
///   [@dictionary],
/// )
///
/// #docs-table(
///   table.header[Typst value][Converted into YAML],
///
///   [types that can be converted from YAML],
///   [corresponding YAML value],
///
///   [@bytes],
///   [string via @repr],
///
///   [@symbol],
///   [string],
///
///   [@content],
///   [a mapping describing the content],
///
///   [other types (@length, etc.)],
///   [string via @repr],
/// )
///
/// == Notes <notes>
/// - In most cases, YAML numbers will be converted to floats or integers
///   depending on whether they are whole numbers. However, be aware that
///   integers larger than 2#super[63]-1 or smaller than -2#super[63] will be
///   converted to floating-point numbers, which may result in an approximative
///   value.
///
/// - Custom YAML tags are ignored, though the loaded value will still be
///   present.
///
/// - Bytes are not encoded as YAML sequences for performance and readability
///   reasons. Consider using @cbor.encode for binary data.
///
/// - The `repr` function is @repr:debugging-only[for debugging purposes only],
///   and its output is not guaranteed to be stable across Typst versions.
#[func(scope, title = "YAML")]
pub fn yaml(
    engine: &mut Engine,
    /// A path to a YAML file or raw YAML bytes.
    source: Spanned<DataSource>,
) -> SourceResult<Value> {
    let loaded = source.load(engine.world)?;
    serde_yaml::from_slice(loaded.data.as_slice())
        .map_err(format_yaml_error)
        .within(&loaded)
}

#[scope]
impl yaml {
    /// Encode structured data into a YAML string.
    #[func(title = "Encode YAML")]
    pub fn encode(
        /// Value to be encoded.
        value: Spanned<Value>,
    ) -> SourceResult<Str> {
        let Spanned { v: value, span } = value;
        serde_yaml::to_string(&value)
            .map(|v| v.into())
            .map_err(|err| eco_format!("failed to encode value as YAML ({err})"))
            .at(span)
    }
}

/// Format the user-facing YAML error message.
pub fn format_yaml_error(error: serde_yaml::Error) -> LoadError {
    let pos = error
        .location()
        .map(|loc| {
            let line_col = LineCol::one_based(loc.line(), loc.column());
            let range = loc.index()..loc.index();
            ReportTextPos::full(range, line_col)
        })
        .unwrap_or_default();
    LoadError::text(pos, "failed to parse YAML", error)
}
