//! The Solidity libraries.

use std::collections::BTreeMap;
use std::collections::BTreeSet;

use serde::Deserialize;
use serde::Serialize;

/// The Solidity libraries.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Libraries {
    /// The unified representation of libraries.
    #[serde(flatten)]
    pub inner: BTreeMap<String, BTreeMap<String, String>>,
}

impl Libraries {
    /// Returns a representation of libraries suitable for the LLD linker.
    pub fn as_linker_symbols(
        &self,
    ) -> anyhow::Result<BTreeMap<String, [u8; revive_common::BYTE_LENGTH_ETH_ADDRESS]>> {
        let mut linker_symbols = BTreeMap::new();
        for (file, contracts) in self.inner.iter() {
            for (name, address) in contracts.iter() {
                let path = format!("{file}:{name}");

                let address_stripped = address.strip_prefix("0x").unwrap_or(address.as_str());
                let address_vec = hex::decode(address_stripped).map_err(|error| {
                    anyhow::anyhow!("Invalid address `{address}` of library `{path}`: {error}.")
                })?;
                let address_array: [u8; revive_common::BYTE_LENGTH_ETH_ADDRESS] = address_vec.try_into().map_err(|address_vec: Vec<u8>| {
                    anyhow::anyhow!(
                        "Incorrect size of address `{address}` of library `{path}`: expected {}, found {}.",
                        revive_common::BYTE_LENGTH_ETH_ADDRESS,
                        address_vec.len(),
                    )
                })?;

                linker_symbols.insert(path, address_array);
            }
        }
        Ok(linker_symbols)
    }

    /// Returns a representation of libraries suitable for filtering.
    pub fn as_paths(&self) -> BTreeSet<String> {
        self.inner
            .iter()
            .flat_map(|(file, names)| {
                names
                    .keys()
                    .map(|name| format!("{file}:{name}"))
                    .collect::<BTreeSet<String>>()
            })
            .collect::<BTreeSet<String>>()
    }

    /// Checks whether the libraries are empty.
    pub fn is_empty(&self) -> bool {
        self.inner.is_empty()
    }

    /// Returns a reference to the inner value.
    pub fn as_inner(&self) -> &BTreeMap<String, BTreeMap<String, String>> {
        &self.inner
    }

    /// Returns a mutable reference to the inner value.
    pub fn as_inner_mut(&mut self) -> &mut BTreeMap<String, BTreeMap<String, String>> {
        &mut self.inner
    }
}

impl From<BTreeMap<String, BTreeMap<String, String>>> for Libraries {
    fn from(inner: BTreeMap<String, BTreeMap<String, String>>) -> Self {
        Self { inner }
    }
}

impl TryFrom<&[String]> for Libraries {
    type Error = anyhow::Error;

    fn try_from(arguments: &[String]) -> Result<Self, Self::Error> {
        let mut libraries = BTreeMap::new();
        for (index, library) in arguments.iter().enumerate() {
            let mut path_and_address = library.split('=');
            let path = path_and_address
                .next()
                .ok_or_else(|| anyhow::anyhow!("Library #{index} path is missing."))?;
            let mut file_and_contract = path.split(':');
            let file = file_and_contract
                .next()
                .ok_or_else(|| anyhow::anyhow!("Library `{path}` file name is missing."))?;
            let contract = file_and_contract
                .next()
                .ok_or_else(|| anyhow::anyhow!("Library `{path}` contract name is missing."))?;
            let address = path_and_address
                .next()
                .ok_or_else(|| anyhow::anyhow!("Library `{path}` address is missing."))?;
            libraries
                .entry(file.to_owned())
                .or_insert_with(BTreeMap::new)
                .insert(contract.to_owned(), address.to_owned());
        }
        Ok(Self { inner: libraries })
    }
}
