use std::fmt::{Display, Write};

use crate::content::Content;

/// The maximum number of characters to print in a single line
/// when [`to_string_pretty`] is used.
const COMPACT_MAX_CHARS: usize = 120;

#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub enum Format {
    Condensed,
    SingleLine,
    Pretty,
}

/// Serializes a serializable to JSON.
pub struct Serializer {
    out: String,
    format: Format,
    indentation: usize,
}

impl Serializer {
    /// Creates a new [`Serializer`] that writes into the given writer.
    pub fn new() -> Serializer {
        Serializer {
            out: String::new(),
            format: Format::Condensed,
            indentation: 0,
        }
    }

    pub fn into_result(self) -> String {
        self.out
    }

    fn write_indentation(&mut self) {
        if self.format == Format::Pretty {
            write!(self.out, "{: ^1$}", "", self.indentation * 2).unwrap();
        }
    }

    fn start_container(&mut self, c: char) {
        self.write_char(c);
        self.indentation += 1;
    }

    fn end_container(&mut self, c: char, empty: bool) {
        self.indentation -= 1;
        if self.format == Format::Pretty && !empty {
            self.write_char('\n');
            self.write_indentation();
        }
        self.write_char(c);
    }

    fn write_comma(&mut self, first: bool) {
        match self.format {
            Format::Pretty => {
                if first {
                    self.write_char('\n');
                } else {
                    self.write_str(",\n");
                }
                self.write_indentation();
            }
            Format::Condensed => {
                if !first {
                    self.write_char(',');
                }
            }
            Format::SingleLine => {
                if !first {
                    self.write_str(", ");
                }
            }
        }
    }

    fn write_colon(&mut self) {
        match self.format {
            Format::Pretty | Format::SingleLine => self.write_str(": "),
            Format::Condensed => self.write_char(':'),
        }
    }

    fn serialize_array(&mut self, items: &[Content]) {
        self.start_container('[');
        for (idx, item) in items.iter().enumerate() {
            self.write_comma(idx == 0);
            self.serialize(item);
        }
        self.end_container(']', items.is_empty());
    }

    fn serialize_object(&mut self, fields: &[(&str, Content)]) {
        self.start_container('{');
        for (idx, (key, value)) in fields.iter().enumerate() {
            self.write_comma(idx == 0);
            self.write_escaped_str(key);
            self.write_colon();
            self.serialize(value);
        }
        self.end_container('}', fields.is_empty());
    }

    pub fn serialize(&mut self, value: &Content) {
        match value {
            Content::Bool(true) => self.write_str("true"),
            Content::Bool(false) => self.write_str("false"),
            Content::U8(n) => write!(self.out, "{n}").unwrap(),
            Content::U16(n) => write!(self.out, "{n}").unwrap(),
            Content::U32(n) => write!(self.out, "{n}").unwrap(),
            Content::U64(n) => write!(self.out, "{n}").unwrap(),
            Content::U128(n) => write!(self.out, "{n}").unwrap(),
            Content::I8(n) => write!(self.out, "{n}").unwrap(),
            Content::I16(n) => write!(self.out, "{n}").unwrap(),
            Content::I32(n) => write!(self.out, "{n}").unwrap(),
            Content::I64(n) => write!(self.out, "{n}").unwrap(),
            Content::I128(n) => write!(self.out, "{n}").unwrap(),
            Content::F32(f) => self.write_float(f, f.is_finite()),
            Content::F64(f) => self.write_float(f, f.is_finite()),
            Content::Char(c) => self.write_escaped_str(&(*c).to_string()),
            Content::String(s) => self.write_escaped_str(s),
            Content::Bytes(bytes) => {
                self.start_container('[');
                for (idx, byte) in bytes.iter().enumerate() {
                    self.write_comma(idx == 0);
                    self.write_str(&byte.to_string());
                }
                self.end_container(']', bytes.is_empty());
            }
            Content::None | Content::Unit | Content::UnitStruct(_) => self.write_str("null"),
            Content::Some(content) => self.serialize(content),
            Content::UnitVariant(_, _, variant) => self.write_escaped_str(variant),
            Content::NewtypeStruct(_, content) => self.serialize(content),
            Content::NewtypeVariant(_, _, variant, content) => {
                self.start_container('{');
                self.write_comma(true);
                self.write_escaped_str(variant);
                self.write_colon();
                self.serialize(content);
                self.end_container('}', false);
            }
            Content::Seq(seq) | Content::Tuple(seq) | Content::TupleStruct(_, seq) => {
                self.serialize_array(seq);
            }
            Content::TupleVariant(_, _, variant, seq) => {
                self.start_container('{');
                self.write_comma(true);
                self.write_escaped_str(variant);
                self.write_colon();
                self.serialize_array(seq);
                self.end_container('}', false);
            }
            Content::Map(map) => {
                self.start_container('{');
                for (idx, (key, value)) in map.iter().enumerate() {
                    self.write_comma(idx == 0);
                    let real_key = key.resolve_inner();
                    if let Content::String(ref s) = real_key {
                        self.write_escaped_str(s);
                    } else if let Some(num) = real_key.as_i64() {
                        self.write_escaped_str(&num.to_string());
                    } else if let Some(num) = real_key.as_i128() {
                        self.write_escaped_str(&num.to_string());
                    } else {
                        panic!("cannot serialize maps without string keys to JSON");
                    }
                    self.write_colon();
                    self.serialize(value);
                }
                self.end_container('}', map.is_empty());
            }
            Content::Struct(_, fields) => {
                self.serialize_object(fields);
            }
            Content::StructVariant(_, _, variant, fields) => {
                self.start_container('{');
                self.write_comma(true);
                self.write_escaped_str(variant);
                self.write_colon();
                self.serialize_object(fields);
                self.end_container('}', false);
            }
        }
    }

    fn write_float(&mut self, n: impl Display, is_finite: bool) {
        if is_finite {
            let start = self.out.len();
            write!(self.out, "{n}").unwrap();
            // ensure the result has .0 for whole numbers to be round-trip safe
            if !self.out[start..].contains('.') {
                self.out.push_str(".0");
            }
        } else {
            self.write_str("null");
        }
    }

    fn write_str(&mut self, s: &str) {
        self.out.push_str(s);
    }

    fn write_char(&mut self, c: char) {
        self.out.push(c);
    }

    fn write_escaped_str(&mut self, value: &str) {
        self.write_char('"');

        let bytes = value.as_bytes();
        let mut start = 0;

        for (i, &byte) in bytes.iter().enumerate() {
            let escape = ESCAPE[byte as usize];
            if escape == 0 {
                continue;
            }

            if start < i {
                self.write_str(&value[start..i]);
            }

            match escape {
                self::BB => self.write_str("\\b"),
                self::TT => self.write_str("\\t"),
                self::NN => self.write_str("\\n"),
                self::FF => self.write_str("\\f"),
                self::RR => self.write_str("\\r"),
                self::QU => self.write_str("\\\""),
                self::BS => self.write_str("\\\\"),
                self::U => {
                    static HEX_DIGITS: [u8; 16] = *b"0123456789abcdef";
                    self.write_str("\\u00");
                    self.write_char(HEX_DIGITS[(byte >> 4) as usize] as char);
                    self.write_char(HEX_DIGITS[(byte & 0xF) as usize] as char);
                }
                _ => unreachable!(),
            }

            start = i + 1;
        }

        if start != bytes.len() {
            self.write_str(&value[start..]);
        }

        self.write_char('"');
    }
}

const BB: u8 = b'b'; // \x08
const TT: u8 = b't'; // \x09
const NN: u8 = b'n'; // \x0A
const FF: u8 = b'f'; // \x0C
const RR: u8 = b'r'; // \x0D
const QU: u8 = b'"'; // \x22
const BS: u8 = b'\\'; // \x5C
const U: u8 = b'u'; // \x00...\x1F except the ones above

// Lookup table of escape sequences. A value of b'x' at index i means that byte
// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.
#[rustfmt::skip]
static ESCAPE: [u8; 256] = [
    //  1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
    U,  U,  U,  U,  U,  U,  U,  U, BB, TT, NN,  U, FF, RR,  U,  U, // 0
    U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U,  U, // 1
    0,  0, QU,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 2
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 3
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 4
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, BS,  0,  0,  0, // 5
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 6
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 7
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 8
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // 9
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // A
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // B
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // C
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // D
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // E
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, // F
];

/// Serializes a value to JSON.
pub fn to_string(value: &Content) -> String {
    let mut ser = Serializer::new();
    ser.serialize(value);
    ser.into_result()
}

/// Serializes a value to JSON in single-line format.
#[allow(unused)]
pub fn to_string_compact(value: &Content) -> String {
    let mut ser = Serializer::new();
    ser.format = Format::SingleLine;
    ser.serialize(value);
    let rv = ser.into_result();
    // this is pretty wasteful as we just format twice
    // but it's acceptable for the way this is used in
    // insta.
    if rv.chars().count() > COMPACT_MAX_CHARS {
        to_string_pretty(value)
    } else {
        rv
    }
}

/// Serializes a value to JSON pretty
#[allow(unused)]
pub fn to_string_pretty(value: &Content) -> String {
    let mut ser = Serializer::new();
    ser.format = Format::Pretty;
    ser.serialize(value);
    ser.into_result()
}

#[test]
fn test_to_string() {
    let json = to_string(&Content::Map(vec![
        (
            Content::from("environments"),
            Content::Seq(vec![
                Content::from("development"),
                Content::from("production"),
            ]),
        ),
        (Content::from("cmdline"), Content::Seq(vec![])),
        (Content::from("extra"), Content::Map(vec![])),
    ]));
    crate::assert_snapshot!(&json, @r#"{"environments":["development","production"],"cmdline":[],"extra":{}}"#);
}

#[test]
fn test_to_string_pretty() {
    let json = to_string_pretty(&Content::Map(vec![
        (
            Content::from("environments"),
            Content::Seq(vec![
                Content::from("development"),
                Content::from("production"),
            ]),
        ),
        (Content::from("cmdline"), Content::Seq(vec![])),
        (Content::from("extra"), Content::Map(vec![])),
    ]));
    crate::assert_snapshot!(&json, @r#"
    {
      "environments": [
        "development",
        "production"
      ],
      "cmdline": [],
      "extra": {}
    }
    "#);
}

#[test]
fn test_to_string_num_keys() {
    let content = Content::Map(vec![
        (Content::from(42u32), Content::from(true)),
        (Content::from(-23i32), Content::from(false)),
    ]);
    let json = to_string_pretty(&content);
    crate::assert_snapshot!(&json, @r#"
    {
      "42": true,
      "-23": false
    }
    "#);
}

#[test]
fn test_to_string_pretty_complex() {
    let content = Content::Map(vec![
        (
            Content::from("is_alive"),
            Content::NewtypeStruct("Some", Content::from(true).into()),
        ),
        (
            Content::from("newtype_variant"),
            Content::NewtypeVariant(
                "Foo",
                0,
                "variant_a",
                Box::new(Content::Struct(
                    "VariantA",
                    vec![
                        ("field_a", Content::String("value_a".into())),
                        ("field_b", 42u32.into()),
                    ],
                )),
            ),
        ),
        (
            Content::from("struct_variant"),
            Content::StructVariant(
                "Foo",
                0,
                "variant_b",
                vec![
                    ("field_a", Content::String("value_a".into())),
                    ("field_b", 42u32.into()),
                ],
            ),
        ),
        (
            Content::from("tuple_variant"),
            Content::TupleVariant(
                "Foo",
                0,
                "variant_c",
                vec![(Content::String("value_a".into())), (42u32.into())],
            ),
        ),
        (Content::from("empty_array"), Content::Seq(vec![])),
        (Content::from("empty_object"), Content::Map(vec![])),
        (Content::from("array"), Content::Seq(vec![true.into()])),
        (
            Content::from("object"),
            Content::Map(vec![("foo".into(), true.into())]),
        ),
        (
            Content::from("array_of_objects"),
            Content::Seq(vec![Content::Struct(
                "MyType",
                vec![
                    ("foo", Content::from("bar".to_string())),
                    ("bar", Content::from("xxx".to_string())),
                ],
            )]),
        ),
        (
            Content::from("unit_variant"),
            Content::UnitVariant("Stuff", 0, "value"),
        ),
        (Content::from("u8"), Content::U8(8)),
        (Content::from("u16"), Content::U16(16)),
        (Content::from("u32"), Content::U32(32)),
        (Content::from("u64"), Content::U64(64)),
        (Content::from("u128"), Content::U128(128)),
        (Content::from("i8"), Content::I8(8)),
        (Content::from("i16"), Content::I16(16)),
        (Content::from("i32"), Content::I32(32)),
        (Content::from("i64"), Content::I64(64)),
        (Content::from("i128"), Content::I128(128)),
        (Content::from("f32"), Content::F32(32.0)),
        (Content::from("f64"), Content::F64(64.0)),
        (Content::from("char"), Content::Char('A')),
        (Content::from("bytes"), Content::Bytes(b"hehe".to_vec())),
        (Content::from("null"), Content::None),
        (Content::from("unit"), Content::Unit),
        (
            Content::from("crazy_string"),
            Content::String((0u8..=126).map(|x| x as char).collect()),
        ),
    ]);
    let json = to_string_pretty(&content);

    crate::assert_snapshot!(&json, @r##"
    {
      "is_alive": true,
      "newtype_variant": {
        "variant_a": {
          "field_a": "value_a",
          "field_b": 42
        }
      },
      "struct_variant": {
        "variant_b": {
          "field_a": "value_a",
          "field_b": 42
        }
      },
      "tuple_variant": {
        "variant_c": [
          "value_a",
          42
        ]
      },
      "empty_array": [],
      "empty_object": {},
      "array": [
        true
      ],
      "object": {
        "foo": true
      },
      "array_of_objects": [
        {
          "foo": "bar",
          "bar": "xxx"
        }
      ],
      "unit_variant": "value",
      "u8": 8,
      "u16": 16,
      "u32": 32,
      "u64": 64,
      "u128": 128,
      "i8": 8,
      "i16": 16,
      "i32": 32,
      "i64": 64,
      "i128": 128,
      "f32": 32.0,
      "f64": 64.0,
      "char": "A",
      "bytes": [
        104,
        101,
        104,
        101
      ],
      "null": null,
      "unit": null,
      "crazy_string": "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
    }
    "##);
}
