use ciborium::de::Error;
use ecow::eco_format;
use typst_syntax::Spanned;

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

/// Reads structured data from a CBOR file.
///
/// The file must contain a valid CBOR serialization. The CBOR values will be
/// converted into corresponding Typst values as listed in the
/// @cbor:conversion[table below].
///
/// The function returns a dictionary, an array or, depending on the CBOR file,
/// another CBOR data type.
///
/// = #short-or-long[Conversion][Conversion details] <conversion>
/// #docs-table(
///   table.header[CBOR value][Converted into Typst],
///
///   [integer],
///   [@int (or @float)],
///
///   [bytes],
///   [@bytes],
///
///   [float],
///   [@float],
///
///   [text],
///   [@str],
///
///   [bool],
///   [@bool],
///
///   [null],
///   [`{none}`],
///
///   [array],
///   [@array],
///
///   [map],
///   [@dictionary],
/// )
///
/// #docs-table(
///   table.header[Typst value][Converted into CBOR],
///
///   [types that can be converted from CBOR],
///   [corresponding CBOR value],
///
///   [@symbol],
///   [text],
///
///   [@content],
///   [a map describing the content],
///
///   [other types (@length, etc.)],
///   [text via @repr],
/// )
///
/// == Notes <notes>
/// - Be aware that CBOR 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.
///
/// - CBOR tags are not supported, and an error will be thrown.
///
/// - 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 = "CBOR")]
pub fn cbor(
    engine: &mut Engine,
    /// A path to a CBOR file or raw CBOR bytes.
    source: Spanned<DataSource>,
) -> SourceResult<Value> {
    let loaded = source.load(engine.world)?;
    ciborium::from_reader(loaded.data.as_slice())
        .map_err(format_cbor_error)
        .within(&loaded)
}

/// Format a user-facing error encountered while parsing a CBOR file
/// ([`ciborium::de::Error`]'s [`Display`](std::fmt::Display) implementation
/// just forwards to [`Debug`]).
fn format_cbor_error(error: Error<std::io::Error>) -> LoadError {
    LoadError::binary(
        "failed to parse CBOR",
        typst_utils::display(|f| match &error {
            Error::Io(e) => write!(f, "IO error: {e}"),
            Error::Syntax(_) => f.write_str("syntax error"),
            Error::Semantic(_, s) => f.write_str(s),
            Error::RecursionLimitExceeded => f.write_str("recursion limit exceeded"),
        }),
    )
}

#[scope]
impl cbor {
    /// Encode structured data into CBOR bytes.
    #[func(title = "Encode CBOR")]
    pub fn encode(
        /// Value to be encoded.
        value: Spanned<Value>,
    ) -> SourceResult<Bytes> {
        let Spanned { v: value, span } = value;
        let mut res = Vec::new();
        ciborium::into_writer(&value, &mut res)
            .map(|_| Bytes::new(res))
            .map_err(|err| eco_format!("failed to encode value as CBOR ({err})"))
            .at(span)
    }
}
