use std::fmt::Write;

use crate::cargo;

pub(crate) fn mangled(defmt_tag: &str, data: &str) -> String {
    Symbol::new(defmt_tag, data).mangle()
}

struct Symbol<'a> {
    /// Name of the Cargo package in which the symbol is being instantiated. Used for avoiding
    /// symbol name collisions.
    package: String,

    /// Unique identifier that disambiguates otherwise equivalent invocations in the same crate.
    disambiguator: u64,

    /// Symbol categorization. Known values:
    /// * `defmt_prim` for primitive formatting strings that are placed at the start of the `.defmt`
    ///   section.
    /// * `defmt_fmt`, `defmt_str` for interned format strings and string literals.
    /// * `defmt_println` for logging messages that are always displayed.
    /// * `defmt_trace`, `defmt_debug`, `defmt_info`, `defmt_warn`, `defmt_error` for logging
    ///   messages used at the different log levels.
    /// * `defmt_bitflags` indicates that a format string was generated by a `defmt::bitflags!`
    ///   invocation, and that the decoder should look up possible flags in the binary.
    ///   The data string is of the format `NAME@REPR#NUM`, where `NAME` is the name of the bitflags
    ///   struct, `REPR` is the raw integer type representing the bitflags value (and also the
    ///   wire format), and `NUM` is the number of defined bitflag values.
    /// * `defmt_bitflags_value` marks a `static` that holds the value of a bitflags `const`, its
    ///   data field is `STRUCT_NAME::FLAG_NAME`.
    /// * Anything starting with `defmt_` is reserved for use by defmt, other prefixes are free for
    ///   use by third-party apps (but they all should use a prefix!).
    tag: String,

    /// Symbol data for use by the host tooling. Interpretation depends on `tag`.
    data: &'a str,

    /// Crate name obtained via CARGO_CRATE_NAME (added since a Cargo package can contain many crates).
    crate_name: String,
}

impl<'a> Symbol<'a> {
    fn new(tag: &'a str, data: &'a str) -> Self {
        Self {
            // `CARGO_PKG_NAME` is set to the invoking package's name.
            package: cargo::package_name(),
            disambiguator: super::crate_local_disambiguator(),
            tag: format!("defmt_{tag}"),
            data,
            crate_name: cargo::crate_name(),
        }
    }

    fn mangle(&self) -> String {
        format!(
            r#"{{"package":"{}","tag":"{}","data":"{}","disambiguator":"{}","crate_name":"{}"}}"#,
            json_escape(&self.package),
            json_escape(&self.tag),
            json_escape(self.data),
            self.disambiguator,
            json_escape(&self.crate_name),
        )
    }
}

fn json_escape(string: &str) -> String {
    let mut escaped = String::new();
    for c in string.chars() {
        match c {
            '\\' => escaped.push_str("\\\\"),
            '\"' => escaped.push_str("\\\""),
            '\n' => escaped.push_str("\\n"),
            c if c.is_control() || c == '@' => write!(escaped, "\\u{:04x}", c as u32).unwrap(),
            c => escaped.push(c),
        }
    }
    escaped
}
