use collections::HashMap;
use schemars::{
    gen::SchemaSettings,
    schema::{ObjectValidation, Schema, SchemaObject},
    JsonSchema,
};
use serde::Deserialize;
use serde_json_lenient::Value;

#[derive(Deserialize)]
pub struct VSSnippetsFile {
    #[serde(flatten)]
    pub(crate) snippets: HashMap<String, VSCodeSnippet>,
}

impl VSSnippetsFile {
    pub fn generate_json_schema() -> Value {
        let schema = SchemaSettings::draft07()
            .with(|settings| settings.option_add_null_type = false)
            .into_generator()
            .into_root_schema_for::<Self>();

        serde_json_lenient::to_value(schema).unwrap()
    }
}

impl JsonSchema for VSSnippetsFile {
    fn schema_name() -> String {
        "VSSnippetsFile".into()
    }

    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
        SchemaObject {
            object: Some(Box::new(ObjectValidation {
                additional_properties: Some(Box::new(gen.subschema_for::<VSCodeSnippet>())),
                ..Default::default()
            })),
            ..Default::default()
        }
        .into()
    }
}

#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub(crate) enum ListOrDirect {
    Single(String),
    List(Vec<String>),
}

impl From<ListOrDirect> for Vec<String> {
    fn from(list: ListOrDirect) -> Self {
        match list {
            ListOrDirect::Single(entry) => vec![entry],
            ListOrDirect::List(entries) => entries,
        }
    }
}

impl std::fmt::Display for ListOrDirect {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "{}",
            match self {
                Self::Single(v) => v.to_owned(),
                Self::List(v) => v.join("\n"),
            }
        )
    }
}

#[derive(Deserialize, JsonSchema)]
pub(crate) struct VSCodeSnippet {
    /// The snippet prefix used to decide whether a completion menu should be shown.
    pub(crate) prefix: Option<ListOrDirect>,

    /// The snippet content. Use `$1` and `${1:defaultText}` to define cursor positions and `$0` for final cursor position.
    pub(crate) body: ListOrDirect,

    /// The snippet description displayed inside the completion menu.
    pub(crate) description: Option<ListOrDirect>,
}
