use std::{fs, path::PathBuf};

use crate::checkers::base::CheckError;

use super::generic::{Array, Mapping, MappingError, Value};
pub(crate) fn from_path(path: PathBuf) -> Result<Box<dyn Mapping>, CheckError> {
    let file_contents = fs::read_to_string(path)?;
    from_string(&file_contents)
}

pub(crate) fn from_string(
    doc: &str,
) -> Result<Box<dyn Mapping>, crate::checkers::base::CheckError> {
    let doc = doc
        .parse::<toml_edit::DocumentMut>()
        .map_err(|e| CheckError::InvalidFileFormat(e.to_string()))?;
    Ok(Box::new(doc.clone()))
}

impl Mapping for toml_edit::DocumentMut {
    fn to_string(&self, _indent: usize) -> Result<String, CheckError> {
        Ok(std::string::ToString::to_string(&self))
    }

    fn get_mapping(
        &mut self,
        key: &str,
        create_missing: bool,
    ) -> Result<&mut dyn Mapping, MappingError> {
        self.as_table_mut().get_mapping(key, create_missing)
    }

    fn contains_key(&self, key: &str) -> bool {
        self.as_table().contains_key(key)
    }

    fn get_array(
        &mut self,
        key: &str,
        create_missing: bool,
    ) -> Result<&mut dyn Array, MappingError> {
        self.as_table_mut().get_array(key, create_missing)
    }

    fn get_string(&self, key: &str) -> Result<String, MappingError> {
        self.as_table().get_string(key)
    }

    fn insert(&mut self, key: &toml_edit::Key, value: &toml_edit::Item) {
        self.as_table_mut()
            .insert_formatted(key, toml_edit::Item::from_toml_value(value));
    }
    fn remove(&mut self, key: &str) {
        if self.as_table().contains_key(key) {
            self.as_table_mut().remove(key);
        }
    }
}

impl Mapping for toml_edit::Table {
    fn to_string(&self, _indent: usize) -> Result<String, CheckError> {
        log::error!("not implemented, call to_string on Document instead");
        std::process::exit(1);
    }

    fn contains_key(&self, key: &str) -> bool {
        self.contains_key(key)
    }
    fn get_mapping(
        &mut self,
        key: &str,
        create_missing: bool,
    ) -> Result<&mut dyn Mapping, MappingError> {
        if !self.contains_key(key) {
            if !create_missing {
                return Err(MappingError::MissingKey(key.to_string()));
            }
            self.insert_formatted(
                &toml_edit::Key::new(key),
                toml_edit::Item::Table(toml_edit::Table::new()),
            );
        }
        let value = self.get_mut(key).unwrap();
        if !value.is_table_like() {
            Err(MappingError::WrongType(format!("{key} is not a mapping")))
        } else if value.is_table() {
            let value = value.as_table_mut().unwrap();
            Ok(value)
        } else {
            let value = value.as_inline_table_mut().unwrap();
            Ok(value)
        }
    }
    fn get_array(
        &mut self,
        key: &str,
        create_missing: bool,
    ) -> Result<&mut dyn Array, MappingError> {
        if !self.contains_key(key) {
            if !create_missing {
                return Err(MappingError::MissingKey(key.to_string()));
            }
            self.insert_formatted(
                &toml_edit::Key::new(key),
                toml_edit::Item::Value(toml_edit::Value::Array(toml_edit::Array::new())),
            );
        }
        let value = self.get_mut(key).unwrap();
        if !value.is_array() {
            Err(MappingError::WrongType(format!("`{key}` is not an array")))
        } else {
            Ok(value.as_array_mut().unwrap())
        }
    }
    fn get_string(&self, key: &str) -> Result<String, MappingError> {
        if !self.contains_key(key) {
            return Err(MappingError::MissingKey(key.to_string()));
        }
        let value = self.get(key).unwrap();
        if !value.is_str() {
            Err(MappingError::WrongType(format!("{key} is not a string")))
        } else {
            Ok(value.as_str().unwrap().to_string())
        }
    }
    fn insert(&mut self, key: &toml_edit::Key, value: &toml_edit::Item) {
        self.insert_formatted(key, toml_edit::Item::from_toml_value(value));
    }
    fn remove(&mut self, key: &str) {
        if self.contains_key(key) {
            self.remove(key);
        }
    }
}

impl Mapping for toml_edit::InlineTable {
    fn to_string(&self, _indent: usize) -> Result<String, CheckError> {
        log::error!("not implemented, call to_string on Document instead");
        std::process::exit(1);
    }

    fn contains_key(&self, key: &str) -> bool {
        self.contains_key(key)
    }
    fn get_mapping(
        &mut self,
        key: &str,
        create_missing: bool,
    ) -> Result<&mut dyn Mapping, MappingError> {
        if !self.contains_key(key) {
            if !create_missing {
                return Err(MappingError::MissingKey(key.to_string()));
            }
            self.insert_formatted(
                &toml_edit::Key::new(key),
                toml_edit::Value::InlineTable(toml_edit::InlineTable::new()),
            );
        }
        let value = self.get_mut(key).unwrap();
        if !value.is_inline_table() {
            Err(MappingError::WrongType(format!("{key} is not a mapping")))
        } else {
            let value = value.as_inline_table_mut().unwrap();
            Ok(value)
        }
    }
    fn get_array(
        &mut self,
        key: &str,
        create_missing: bool,
    ) -> Result<&mut dyn Array, MappingError> {
        if !self.contains_key(key) {
            if !create_missing {
                return Err(MappingError::MissingKey(key.to_string()));
            }
            self.insert_formatted(
                &toml_edit::Key::new(key),
                toml_edit::Value::Array(toml_edit::Array::new()),
            );
        }
        let value = self.get_mut(key).unwrap();
        if !value.is_array() {
            Err(MappingError::WrongType(format!("`{key}` is not an array")))
        } else {
            Ok(value.as_array_mut().unwrap())
        }
    }
    fn get_string(&self, key: &str) -> Result<String, MappingError> {
        if !self.contains_key(key) {
            return Err(MappingError::MissingKey(key.to_string()));
        }
        let value = self.get(key).unwrap();
        if !value.is_str() {
            Err(MappingError::WrongType(format!("{key} is not a string")))
        } else {
            Ok(value.as_str().unwrap().to_string())
        }
    }
    fn insert(&mut self, key: &toml_edit::Key, value: &toml_edit::Item) {
        self.insert_formatted(
            key,
            toml_edit::Item::from_toml_value(value)
                .as_value()
                .expect("expect value, no table")
                .to_owned(),
        );
    }
    fn remove(&mut self, key: &str) {
        if self.contains_key(key) {
            self.remove(key);
        }
    }
}

impl Array for toml_edit::Array {
    fn insert_when_not_present(&mut self, value: &toml_edit::Item) {
        let value = toml_edit::Item::from_toml_value(value);

        if toml_array_index_equal_without_formatting(self, &value).is_none() {
            self.push_formatted(value.as_value().expect("expect value, no table").to_owned());
        }
    }

    fn remove(&mut self, value: &toml_edit::Item) {
        let value = toml_edit::Item::from_toml_value(value);
        if let Some(idx) = toml_array_index_equal_without_formatting(self, &value) {
            self.remove(idx);
        }
    }

    fn contains_item(&self, value: &toml_edit::Item) -> bool {
        let value = toml_edit::Item::from_toml_value(value);
        toml_array_index_equal_without_formatting(self, &value).is_some()
    }
}

fn item_value_equals(item: &toml_edit::Value, value: &toml_edit::Value) -> bool {
    match (item, value) {
        (toml_edit::Value::String(item), toml_edit::Value::String(value)) => {
            item.value() == value.value()
        }
        (toml_edit::Value::Integer(item), toml_edit::Value::Integer(value)) => {
            item.value() == value.value()
        }
        (toml_edit::Value::Float(item), toml_edit::Value::Float(value)) => {
            item.value() == value.value()
        }
        (toml_edit::Value::Boolean(item), toml_edit::Value::Boolean(value)) => {
            item.value() == value.value()
        }
        (toml_edit::Value::Array(items), toml_edit::Value::Array(values)) => {
            if items.len() != values.len() {
                return false;
            }
            for (i, j) in items.iter().zip(values) {
                if !item_value_equals(i, j) {
                    return false;
                }
            }
            true
        }
        (toml_edit::Value::InlineTable(items), toml_edit::Value::InlineTable(values)) => {
            if items.len() != values.len() {
                return false;
            }
            for (i, j) in items.iter().zip(values) {
                if i.0 != j.0 || !item_value_equals(i.1, j.1) {
                    return false;
                }
            }
            true
        }
        _ => false,
    }
}

fn toml_array_index_equal_without_formatting(
    array: &toml_edit::Array,
    item: &toml_edit::Item,
) -> Option<usize> {
    for (idx, array_item) in array.iter().enumerate() {
        if item_value_equals(array_item, item.as_value().unwrap()) {
            return Some(idx);
        }
    }
    None
}

impl Value for toml_edit::Item {
    fn from_toml_value(value: &toml_edit::Item) -> Self {
        value.clone()
    }
}
#[cfg(test)]
mod tests {

    use super::super::generic::tests::get_test_table;
    use super::super::generic::tests::test_mapping;
    use super::*;

    #[test]
    fn test_access_map() {
        let table = get_test_table().into_table().unwrap();

        test_mapping(Box::new(table.clone()));
    }

    #[test]
    fn test_from_toml_value() {
        let table = get_test_table();

        let toml_table = toml_edit::Item::from_toml_value(&table);

        assert_eq!(
            toml_table.to_string(),
            r#"str = "string"
# comment for int
int = 1
float = 1.1
bool = true
array = [
  # comment for item 1
  1,
  # comment for item 2
  2
]
dict = { str = "string", int = 1, float = 1.1, bool = true, array = [1, 2] }
"#
        );
    }

    #[test]
    fn test_insert_and_remove_with_comments() {
        let old_doc = "
[table]
# prefix comment Key \"keep_int\"
keep_int = 2  # suffix comment Value 2
# prefix comment Key \"remove_int\"
remove_int = 3  # suffix comment Value 3
keep_array = [ # prefix comment Value \"keep_item_1\"
    \"keep_item_1\", # prefix comment remove_item_2
    \"remove_item_2\"  # suffix comment remove_item_2
    ,
    # prefix comment item keep_item_3
    \"keep_item_3\" # suffix comment item keep_item_3
] # suffix comment Array \"keep_array\"
        "
        .parse::<toml_edit::DocumentMut>()
        .unwrap();

        let items_to_add = "
[table]
# prefix comment Key \"add_int\"
add_int = 2  # suffix comment Value 2
keep_array = [ # prefix comment \"add_item_4\"
    \"add_item_4\" # suffix comment \"add_item_4\"
]
"
        .parse::<toml_edit::DocumentMut>()
        .unwrap()["table"]
            .as_table()
            .unwrap()
            .clone();

        let items_to_remove = "
[table]
remove_int = 2
keep_array = [
    \"remove_item_2\"
]
"
        .parse::<toml_edit::DocumentMut>()
        .unwrap()["table"]
            .as_table()
            .unwrap()
            .clone();

        let mut new_doc = old_doc.clone();

        let (key, value) = items_to_add.get_key_value("add_int").unwrap();
        Mapping::insert(new_doc["table"].as_table_mut().unwrap(), key, value);

        let item = items_to_add["keep_array"]
            .as_array()
            .unwrap()
            .get(0)
            .unwrap();
        Array::insert_when_not_present(
            new_doc["table"]["keep_array"].as_array_mut().unwrap(),
            &toml_edit::Item::Value(item.clone()),
        );

        let (key, _value) = items_to_remove.get_key_value("remove_int").unwrap();
        Mapping::remove(new_doc["table"].as_table_mut().unwrap(), key);

        let item = items_to_remove["keep_array"]
            .as_array()
            .unwrap()
            .get(0)
            .unwrap();
        Array::remove(
            new_doc["table"].as_table_mut().unwrap()["keep_array"]
                .as_array_mut()
                .unwrap(),
            &toml_edit::Item::Value(item.clone()),
        );

        new_doc.fmt();

        assert_eq!(
            Mapping::to_string(&new_doc, 0).unwrap(),
            "\n[table]\n# prefix comment Key \"keep_int\"\nkeep_int = 2  # suffix comment Value 2\nkeep_array = [ # prefix comment Value \"keep_item_1\"\n    \"keep_item_1\",\n    # prefix comment item keep_item_3\n    \"keep_item_3\" # suffix comment item keep_item_3\n, # prefix comment \"add_item_4\"\n    \"add_item_4\" # suffix comment \"add_item_4\"\n] # suffix comment Array \"keep_array\"\n# prefix comment Key \"add_int\"\nadd_int = 2  # suffix comment Value 2\n        "
        );
    }
}
