use std::{
    borrow::Cow,
    collections::{HashMap, HashSet},
    fmt,
};

use super::{Entry, Yarn1Lockfile};

const INDENT: &str = "  ";

impl Yarn1Lockfile {
    fn reverse_lookup(&self) -> HashMap<&Entry, HashSet<&str>> {
        let mut reverse_lookup = HashMap::new();
        for (key, value) in self.inner.iter() {
            let keys: &mut HashSet<&str> = reverse_lookup.entry(value).or_default();
            keys.insert(key);
        }
        reverse_lookup
    }
}

impl fmt::Display for Yarn1Lockfile {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(
            "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n# yarn lockfile \
             v1\n\n",
        )?;
        let reverse_lookup = self.reverse_lookup();
        let mut added_keys: HashSet<&str> = HashSet::with_capacity(self.inner.len());
        for (key, entry) in self.inner.iter() {
            if added_keys.contains(key.as_str()) {
                continue;
            }

            let all_keys = reverse_lookup
                .get(entry)
                .expect("entry in lockfile should appear as a key in reverse lookup");
            added_keys.extend(all_keys);
            let mut keys = all_keys.iter().copied().collect::<Vec<_>>();
            // Keys must be sorted before they get wrapped
            keys.sort();

            let wrapped_keys = keys.into_iter().map(maybe_wrap).collect::<Vec<_>>();
            let key_line = wrapped_keys.join(", ");

            f.write_fmt(format_args!("\n{}:\n{}\n", key_line, entry))?;
        }
        Ok(())
    }
}

impl fmt::Display for Entry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut leading = LeadingNewline::new();
        if let Some(name) = &self.name {
            f.write_fmt(format_args!(
                "{}{INDENT}name {}",
                leading.leading(),
                maybe_wrap(name)
            ))?;
        }
        f.write_fmt(format_args!(
            "{}{INDENT}version {}",
            leading.leading(),
            maybe_wrap(&self.version)
        ))?;
        if let Some(uid) = &self.uid {
            f.write_fmt(format_args!(
                "{}{INDENT}uid {}",
                leading.leading(),
                maybe_wrap(uid)
            ))?;
        }
        if let Some(resolved) = &self.resolved {
            f.write_fmt(format_args!(
                "{}{INDENT}resolved {}",
                leading.leading(),
                maybe_wrap(resolved)
            ))?;
        }
        if let Some(integrity) = &self.integrity {
            f.write_fmt(format_args!(
                "{}{INDENT}integrity {}",
                leading.leading(),
                maybe_wrap(integrity)
            ))?;
        }
        if let Some(registry) = &self.registry {
            f.write_fmt(format_args!(
                "{}{INDENT}integrity {}",
                leading.leading(),
                maybe_wrap(registry)
            ))?;
        }
        // encode deps and optional deps
        if let Some(deps) = &self.dependencies {
            f.write_fmt(format_args!("{}{INDENT}dependencies:", leading.leading()))?;
            encode_map(deps.iter().map(|(k, v)| (k.as_ref(), v.as_ref())), f)?;
        }
        if let Some(optional_deps) = &self.optional_dependencies {
            f.write_fmt(format_args!(
                "{}{INDENT}optionalDependencies:",
                leading.leading()
            ))?;
            encode_map(
                optional_deps.iter().map(|(k, v)| (k.as_ref(), v.as_ref())),
                f,
            )?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Copy)]
enum LeadingNewline {
    First,
    Rest,
}

impl LeadingNewline {
    fn new() -> Self {
        Self::First
    }

    fn leading(&mut self) -> &'static str {
        let res = match self {
            LeadingNewline::First => "",
            LeadingNewline::Rest => "\n",
        };
        *self = Self::Rest;
        res
    }
}

fn encode_map<'a, I: Iterator<Item = (&'a str, &'a str)>>(
    entries: I,
    f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
    let mut wrapped_entries = entries
        .map(|(k, v)| (maybe_wrap(k), maybe_wrap(v)))
        .collect::<Vec<_>>();
    wrapped_entries.sort_unstable_by(|(k1, _), (k2, _)| k1.cmp(k2));
    // we sort the via wrapped keys
    // then we write each line with the value wrapped as well
    for (key, value) in wrapped_entries {
        f.write_fmt(format_args!("\n{INDENT}{INDENT}{key} {value}"))?;
    }

    Ok(())
}

fn maybe_wrap(s: &str) -> Cow<str> {
    match should_wrap_key(s) {
        // yarn uses JSON.stringify to escape strings
        // we approximate this behavior using serde_json
        true => serde_json::to_string(s)
            .expect("failed at encoding string as json")
            .into(),
        false => s.into(),
    }
}

// Determines if we need to wrap a key
fn should_wrap_key(s: &str) -> bool {
    // Wrap if it starts with a syml keyword
    s.starts_with("true") ||
    s.starts_with("false") ||
    // Wrap if it doesn't start with a-zA-Z
    s.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) ||
    // Wrap if it contains any unwanted chars
    s.chars().any(|c| matches!(
        c,
        ' ' | ':' | '\t' | '\r' | '\u{000B}' | '\u{000C}' | '\n' | '\\' | '"' | ',' | '[' | ']'
    ))
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_should_wrap() {
        assert!(should_wrap_key("jsx-ast-utils@^2.4.1 || ^3.0.0"))
    }

    #[test]
    fn test_basic_serialization() {
        let entry = Entry {
            version: "12.2.5".into(),
            resolved: Some("https://registry.yarnpkg.com/next/-/next-12.2.5.tgz#14fb5975e8841fad09553b8ef41fe1393602b717".into()),
            integrity: Some("sha512-tBdjqX5XC/oFs/6gxrZhjmiq90YWizUYU6qOWAfat7zJwrwapJ+BYgX2PmiacunXMaRpeVT4vz5MSPSLgNkrpA==".into()),
            dependencies: Some(vec![
                ("@next/env".into(), "12.2.5".into()),
                ("caniuse-lite".into(), "^1.0.30001332".into()),
                ("postcss".into(), "8.4.14".into()),
            ].into_iter().collect()),
            optional_dependencies: Some(vec![("@next/swc-win32-x64-msvc".into(), "12.2.5".into())].into_iter().collect()),
            ..Default::default()
        };
        assert_eq!(
            entry.to_string(),
            r#"  version "12.2.5"
  resolved "https://registry.yarnpkg.com/next/-/next-12.2.5.tgz#14fb5975e8841fad09553b8ef41fe1393602b717"
  integrity sha512-tBdjqX5XC/oFs/6gxrZhjmiq90YWizUYU6qOWAfat7zJwrwapJ+BYgX2PmiacunXMaRpeVT4vz5MSPSLgNkrpA==
  dependencies:
    "@next/env" "12.2.5"
    caniuse-lite "^1.0.30001332"
    postcss "8.4.14"
  optionalDependencies:
    "@next/swc-win32-x64-msvc" "12.2.5""#
        );
    }
}
