use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CanonicalJsonError {
    #[error("Unsupported non-finite JSON number")]
    NonFiniteNumber,
    #[error("Unsupported value for canonicalization")]
    UnsupportedValue,
}

pub(crate) fn canonicalize_json(value: &Value) -> Result<Vec<u8>, CanonicalJsonError> {
    let mut output = String::new();
    write_canonical_json(value, &mut output)?;
    Ok(output.into_bytes())
}

fn write_canonical_json(value: &Value, output: &mut String) -> Result<(), CanonicalJsonError> {
    match value {
        Value::Null | Value::Bool(_) | Value::String(_) => {
            output.push_str(&serde_json::to_string(value).map_err(|_| CanonicalJsonError::UnsupportedValue)?);
            Ok(())
        }
        Value::Number(number) => {
            if number.as_f64().map(|it| it.is_finite()).unwrap_or(true) {
                output.push_str(&number.to_string());
                Ok(())
            } else {
                Err(CanonicalJsonError::NonFiniteNumber)
            }
        }
        Value::Array(items) => {
            output.push('[');
            for (index, item) in items.iter().enumerate() {
                if index > 0 {
                    output.push(',');
                }
                write_canonical_json(item, output)?;
            }
            output.push(']');
            Ok(())
        }
        Value::Object(map) => {
            output.push('{');
            let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
            keys.sort_unstable();
            for (index, key) in keys.iter().enumerate() {
                if index > 0 {
                    output.push(',');
                }
                output.push_str(&serde_json::to_string(key).map_err(|_| CanonicalJsonError::UnsupportedValue)?);
                output.push(':');
                let value = map.get(*key).ok_or(CanonicalJsonError::UnsupportedValue)?;
                write_canonical_json(value, output)?;
            }
            output.push('}');
            Ok(())
        }
    }
}
