use std::collections::{HashMap, HashSet};

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

use crate::protocol::{DialectId, DialectVersion, Enum, Message, MessageId};
use crate::utils::dialect_canonical_name;

/// MAVLink dialect specification.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Dialect {
    name: String,
    canonical_name: String,
    version: Option<DialectVersion>,
    dialect: Option<DialectId>,
    messages: HashMap<MessageId, Message>,
    enums: HashMap<String, Enum>,
}

impl Dialect {
    /// Default constructor
    ///
    /// # Arguments
    ///
    /// * `name` dialect name.
    /// * `version` dialect version (if provided).
    /// * `dialect` dialect ID.
    /// * `messages` map of messages.
    /// * `enums` map of enums.
    pub fn new(
        name: String,
        version: Option<DialectVersion>,
        dialect: Option<DialectId>,
        messages: HashMap<MessageId, Message>,
        enums: HashMap<String, Enum>,
    ) -> Self {
        let canonical_name = dialect_canonical_name(&name);

        Self {
            name,
            canonical_name,
            version,
            dialect,
            messages,
            enums,
        }
    }

    /// Dialect name.
    ///
    /// See [`Self::canonical_name`] for canonical name used as a key in
    /// [`Protocol::dialects`](crate::protocol::Protocol::dialects).
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Canonical dialect name.
    ///
    /// As a dialect [name](Self::name) we use a file base name of its XML definition (without extension). However,
    /// upon XML parsing loading, we convert this name into canonical form
    /// (using [`dialect_canonical_name`]). Canonical names are used as a key in
    /// [`Protocol::dialects`](crate::protocol::Protocol::dialects). We do this in order to avoid naming
    /// collisions when someone tries to generate source code based on dialect names.
    pub fn canonical_name(&self) -> &str {
        &self.canonical_name
    }

    /// Dialect version.
    pub fn version(&self) -> Option<DialectVersion> {
        self.version
    }

    /// Dialect ID.
    pub fn dialect(&self) -> Option<DialectId> {
        self.dialect
    }

    /// Collection of dialect messages.
    pub fn messages(&self) -> &HashMap<MessageId, Message> {
        &self.messages
    }

    /// Collection of dialect enums.
    pub fn enums(&self) -> &HashMap<String, Enum> {
        &self.enums
    }

    /// Create a new instance of [`Dialect`] containing only specified `messages`.
    ///
    /// If `keep_enums` is set to `false` then only enums related to existing messages will be retained. The decision of
    /// what to keep is based on whether enum is referenced in [`MessageField`](crate::protocol::MessageField) or
    /// it was referenced by other already included enum in [`EnumEntryMavCmdParam`](crate::protocol::EnumEntryMavCmdParam).
    ///
    /// This is an immutable cloning version [`Self::retain_messages`]. The latter filters out messages and enums
    /// in-place.
    pub fn with_filtered_messages(&self, messages: &HashSet<String>, keep_enums: bool) -> Self {
        let mut dialect = self.clone();

        dialect.retain_messages_only(messages);

        if !keep_enums {
            dialect.retain_message_enums();
            dialect.add_enums_from_mav_cmd_params(self.enums());
        }

        dialect
    }

    /// Retain only specified `messages`.
    ///
    /// If `keep_enums` is set to `false` then only enums related to existing messages will be retained. The decision of
    /// what to keep is based on whether enum is referenced in [`MessageField`](crate::protocol::MessageField) or
    /// it was referenced by other already included enum in [`EnumEntryMavCmdParam`](crate::protocol::EnumEntryMavCmdParam).
    ///
    /// This is an in-place version of [`Self::with_filtered_messages`]. The latter creates a new instance of [`Dialect`].   
    pub fn retain_messages(&mut self, messages: &HashSet<String>, keep_enums: bool) {
        self.retain_messages_only(messages);

        if !keep_enums {
            let enums = self.enums.clone();
            self.retain_message_enums();
            self.add_enums_from_mav_cmd_params(&enums);
        }
    }

    fn retain_messages_only(&mut self, messages: &HashSet<String>) {
        self.messages.retain(|_, msg| messages.contains(msg.name()));
    }

    fn retain_message_enums(&mut self) {
        self.enums.retain(|name, _| {
            for msg in self.messages.values() {
                for fld in msg.fields() {
                    match fld.r#enum() {
                        Some(fld_enum_name) if fld_enum_name == name => {
                            return true;
                        }
                        _ => {}
                    }
                }
            }

            false
        });
    }

    fn add_enums_from_mav_cmd_params(&mut self, all_enums: &HashMap<String, Enum>) {
        let retained_enums = self.enums.clone();

        for mav_enum in retained_enums.values().clone() {
            for entry in mav_enum.entries().values() {
                for param in entry.params() {
                    if let Some(enum_name) = param.r#enum() {
                        if let Some(enum_to_keep) = all_enums.get(enum_name) {
                            self.enums
                                .insert(enum_name.to_string(), enum_to_keep.clone());
                        }
                    }
                }
            }
        }
    }
}
