use std::fmt::{Debug, Display, Formatter};

/// Visits [`OptionsMetadata`].
///
/// An instance of [`Visit`] represents the logic for inspecting an object's options metadata.
pub trait Visit {
    /// Visits an [`OptionField`] value named `name`.
    fn record_field(&mut self, name: &str, field: OptionField);

    /// Visits an [`OptionSet`] value named `name`.
    fn record_set(&mut self, name: &str, group: OptionSet);
}

/// Returns metadata for its options.
pub trait OptionsMetadata {
    /// Visits the options metadata of this object by calling `visit` for each option.
    fn record(visit: &mut dyn Visit);

    fn documentation() -> Option<&'static str> {
        None
    }

    /// Returns the extracted metadata.
    fn metadata() -> OptionSet
    where
        Self: Sized + 'static,
    {
        OptionSet::of::<Self>()
    }
}

impl<T> OptionsMetadata for Option<T>
where
    T: OptionsMetadata,
{
    fn record(visit: &mut dyn Visit) {
        T::record(visit);
    }
}

/// Metadata of an option that can either be a [`OptionField`] or [`OptionSet`].
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize), serde(untagged))]
pub enum OptionEntry {
    /// A single option.
    Field(OptionField),

    /// A set of options.
    Set(OptionSet),
}

impl OptionEntry {
    pub fn into_field(self) -> Option<OptionField> {
        match self {
            OptionEntry::Field(field) => Some(field),
            OptionEntry::Set(_) => None,
        }
    }
}

impl Display for OptionEntry {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            OptionEntry::Set(set) => std::fmt::Display::fmt(set, f),
            OptionEntry::Field(field) => std::fmt::Display::fmt(&field, f),
        }
    }
}

/// A set of options.
///
/// It extracts the options by calling the [`OptionsMetadata::record`] of a type implementing
/// [`OptionsMetadata`].
#[derive(Copy, Clone)]
pub struct OptionSet {
    record: fn(&mut dyn Visit),
    doc: fn() -> Option<&'static str>,
}

impl OptionSet {
    pub fn of<T>() -> Self
    where
        T: OptionsMetadata + 'static,
    {
        Self {
            record: T::record,
            doc: T::documentation,
        }
    }

    /// Visits the options in this set by calling `visit` for each option.
    pub fn record(&self, visit: &mut dyn Visit) {
        let record = self.record;
        record(visit);
    }

    pub fn documentation(&self) -> Option<&'static str> {
        let documentation = self.doc;
        documentation()
    }

    /// Returns `true` if this set has an option that resolves to `name`.
    ///
    /// The name can be separated by `.` to find a nested option.
    ///
    /// ## Examples
    ///
    /// ### Test for the existence of a child option
    ///
    /// ```rust
    /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
    ///
    /// struct WithOptions;
    ///
    /// impl OptionsMetadata for WithOptions {
    ///     fn record(visit: &mut dyn Visit) {
    ///         visit.record_field("ignore-git-ignore", OptionField {
    ///             doc: "Whether Ruff should respect the gitignore file",
    ///             default: "false",
    ///             value_type: "bool",
    ///             example: "",
    ///             scope: None,
    ///             deprecated: None,
    ///         });
    ///     }
    /// }
    ///
    /// assert!(WithOptions::metadata().has("ignore-git-ignore"));
    /// assert!(!WithOptions::metadata().has("does-not-exist"));
    /// ```
    /// ### Test for the existence of a nested option
    ///
    /// ```rust
    /// # use ruff_options_metadata::{OptionField, OptionsMetadata, Visit};
    ///
    /// struct Root;
    ///
    /// impl OptionsMetadata for Root {
    ///     fn record(visit: &mut dyn Visit) {
    ///         visit.record_field("ignore-git-ignore", OptionField {
    ///             doc: "Whether Ruff should respect the gitignore file",
    ///             default: "false",
    ///             value_type: "bool",
    ///             example: "",
    ///             scope: None,
    ///             deprecated: None
    ///         });
    ///
    ///         visit.record_set("format", Nested::metadata());
    ///     }
    /// }
    ///
    /// struct Nested;
    ///
    /// impl OptionsMetadata for Nested {
    ///     fn record(visit: &mut dyn Visit) {
    ///         visit.record_field("hard-tabs", OptionField {
    ///             doc: "Use hard tabs for indentation and spaces for alignment.",
    ///             default: "false",
    ///             value_type: "bool",
    ///             example: "",
    ///             scope: None,
    ///             deprecated: None
    ///         });
    ///     }
    /// }
    ///
    /// assert!(Root::metadata().has("format.hard-tabs"));
    /// assert!(!Root::metadata().has("format.spaces"));
    /// assert!(!Root::metadata().has("lint.hard-tabs"));
    /// ```
    pub fn has(&self, name: &str) -> bool {
        self.find(name).is_some()
    }

    /// Returns `Some` if this set has an option that resolves to `name` and `None` otherwise.
    ///
    /// The name can be separated by `.` to find a nested option.
    ///
    /// ## Examples
    ///
    /// ### Find a child option
    ///
    /// ```rust
    /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
    ///
    /// struct WithOptions;
    ///
    /// static IGNORE_GIT_IGNORE: OptionField = OptionField {
    ///     doc: "Whether Ruff should respect the gitignore file",
    ///     default: "false",
    ///     value_type: "bool",
    ///     example: "",
    ///     scope: None,
    ///     deprecated: None
    ///  };
    ///
    /// impl OptionsMetadata for WithOptions {
    ///     fn record(visit: &mut dyn Visit) {
    ///         visit.record_field("ignore-git-ignore", IGNORE_GIT_IGNORE.clone());
    ///     }
    /// }
    ///
    /// assert_eq!(WithOptions::metadata().find("ignore-git-ignore").and_then(OptionEntry::into_field), Some(IGNORE_GIT_IGNORE.clone()));
    /// assert!(WithOptions::metadata().find("does-not-exist").is_none());
    /// ```
    /// ### Find a nested option
    ///
    /// ```rust
    /// # use ruff_options_metadata::{OptionEntry, OptionField, OptionsMetadata, Visit};
    ///
    /// static HARD_TABS: OptionField = OptionField {
    ///     doc: "Use hard tabs for indentation and spaces for alignment.",
    ///     default: "false",
    ///     value_type: "bool",
    ///     example: "",
    ///     scope: None,
    ///     deprecated: None
    /// };
    ///
    /// struct Root;
    ///
    /// impl OptionsMetadata for Root {
    ///     fn record(visit: &mut dyn Visit) {
    ///         visit.record_field("ignore-git-ignore", OptionField {
    ///             doc: "Whether Ruff should respect the gitignore file",
    ///             default: "false",
    ///             value_type: "bool",
    ///             example: "",
    ///             scope: None,
    ///             deprecated: None
    ///         });
    ///
    ///         visit.record_set("format", Nested::metadata());
    ///     }
    /// }
    ///
    /// struct Nested;
    ///
    /// impl OptionsMetadata for Nested {
    ///     fn record(visit: &mut dyn Visit) {
    ///         visit.record_field("hard-tabs", HARD_TABS.clone());
    ///     }
    /// }
    ///
    /// assert_eq!(Root::metadata().find("format.hard-tabs").and_then(OptionEntry::into_field), Some(HARD_TABS.clone()));
    /// assert!(matches!(Root::metadata().find("format"), Some(OptionEntry::Set(_))));
    /// assert!(Root::metadata().find("format.spaces").is_none());
    /// assert!(Root::metadata().find("lint.hard-tabs").is_none());
    /// ```
    pub fn find(&self, name: &str) -> Option<OptionEntry> {
        struct FindOptionVisitor<'a> {
            option: Option<OptionEntry>,
            parts: std::str::Split<'a, char>,
            needle: &'a str,
        }

        impl Visit for FindOptionVisitor<'_> {
            fn record_set(&mut self, name: &str, set: OptionSet) {
                if self.option.is_none() && name == self.needle {
                    if let Some(next) = self.parts.next() {
                        self.needle = next;
                        set.record(self);
                    } else {
                        self.option = Some(OptionEntry::Set(set));
                    }
                }
            }

            fn record_field(&mut self, name: &str, field: OptionField) {
                if self.option.is_none() && name == self.needle {
                    if self.parts.next().is_none() {
                        self.option = Some(OptionEntry::Field(field));
                    }
                }
            }
        }

        let mut parts = name.split('.');

        if let Some(first) = parts.next() {
            let mut visitor = FindOptionVisitor {
                parts,
                needle: first,
                option: None,
            };

            self.record(&mut visitor);
            visitor.option
        } else {
            None
        }
    }

    pub fn collect_fields(&self) -> Vec<(String, OptionField)> {
        struct FieldsCollector(Vec<(String, OptionField)>);

        impl Visit for FieldsCollector {
            fn record_field(&mut self, name: &str, field: OptionField) {
                self.0.push((name.to_string(), field));
            }

            fn record_set(&mut self, _name: &str, _group: OptionSet) {}
        }

        let mut visitor = FieldsCollector(vec![]);
        self.record(&mut visitor);
        visitor.0
    }
}

/// Visitor that writes out the names of all fields and sets.
struct DisplayVisitor<'fmt, 'buf> {
    f: &'fmt mut Formatter<'buf>,
    result: std::fmt::Result,
}

impl<'fmt, 'buf> DisplayVisitor<'fmt, 'buf> {
    fn new(f: &'fmt mut Formatter<'buf>) -> Self {
        Self { f, result: Ok(()) }
    }

    fn finish(self) -> std::fmt::Result {
        self.result
    }
}

impl Visit for DisplayVisitor<'_, '_> {
    fn record_set(&mut self, name: &str, _: OptionSet) {
        self.result = self.result.and_then(|()| writeln!(self.f, "{name}"));
    }

    fn record_field(&mut self, name: &str, field: OptionField) {
        self.result = self.result.and_then(|()| {
            write!(self.f, "{name}")?;

            if field.deprecated.is_some() {
                write!(self.f, " (deprecated)")?;
            }

            writeln!(self.f)
        });
    }
}

impl Display for OptionSet {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut visitor = DisplayVisitor::new(f);
        self.record(&mut visitor);
        visitor.finish()
    }
}

impl Debug for OptionSet {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        Display::fmt(self, f)
    }
}

#[derive(Debug, Eq, PartialEq, Clone)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
pub struct OptionField {
    pub doc: &'static str,
    /// Ex) `"false"`
    pub default: &'static str,
    /// Ex) `"bool"`
    pub value_type: &'static str,
    /// Ex) `"per-file-ignores"`
    pub scope: Option<&'static str>,
    pub example: &'static str,
    pub deprecated: Option<Deprecated>,
}

#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(::serde::Serialize))]
pub struct Deprecated {
    pub since: Option<&'static str>,
    pub message: Option<&'static str>,
}

impl Display for OptionField {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "{}", self.doc)?;
        writeln!(f)?;
        writeln!(f, "Default value: {}", self.default)?;
        writeln!(f, "Type: {}", self.value_type)?;

        if let Some(deprecated) = &self.deprecated {
            write!(f, "Deprecated")?;

            if let Some(since) = deprecated.since {
                write!(f, " (since {since})")?;
            }

            if let Some(message) = deprecated.message {
                write!(f, ": {message}")?;
            }

            writeln!(f)?;
        }

        writeln!(f, "Example usage:\n```toml\n{}\n```", self.example)
    }
}

#[cfg(feature = "serde")]
mod serde {
    use super::{OptionField, OptionSet, Visit};
    use serde::{Serialize, Serializer};
    use std::collections::BTreeMap;

    impl Serialize for OptionSet {
        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            let mut entries = BTreeMap::new();
            let mut visitor = SerializeVisitor {
                entries: &mut entries,
            };
            self.record(&mut visitor);
            entries.serialize(serializer)
        }
    }

    struct SerializeVisitor<'a> {
        entries: &'a mut BTreeMap<String, OptionField>,
    }

    impl Visit for SerializeVisitor<'_> {
        fn record_set(&mut self, name: &str, set: OptionSet) {
            // Collect the entries of the set.
            let mut entries = BTreeMap::new();
            let mut visitor = SerializeVisitor {
                entries: &mut entries,
            };
            set.record(&mut visitor);

            // Insert the set into the entries.
            for (key, value) in entries {
                self.entries.insert(format!("{name}.{key}"), value);
            }
        }

        fn record_field(&mut self, name: &str, field: OptionField) {
            self.entries.insert(name.to_string(), field);
        }
    }
}
