/*
Copyright 2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
*/

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

pub trait Constructable {
    fn new() -> Self;
}

#[derive(Clone, Debug, PartialEq)]
pub enum Change<Key: Eq + Hash, Value> {
    DeleteRecord(Key),
    UpdateRecord((Key, Value)),
    NewRecord(Value),
}

#[derive(Clone, Debug, Default)]
pub struct Changeset<Key: Clone + Eq + Hash, Value> {
    delete: HashSet<Key>,
    update: HashMap<Key, Value>,
    new: HashMap<Key, Value>,
}

impl<Key: Clone + Constructable + Eq + Hash, Value> Changeset<Key, Value> {
    pub fn add(&mut self, r: Value) -> Key {
        let k = Key::new();
        self.new.insert(k.clone(), r);
        k.clone()
    }

    pub fn update(&mut self, k: Key, v: Value) {
        match self.new.get_mut(&k) {
            Some(record) => *record = v,
            None => {
                let _ = self.update.insert(k, v);
            }
        }
    }

    pub fn delete(&mut self, k: Key) {
        let new_v = self.new.remove(&k);
        let updated_v = self.update.remove(&k);
        match (new_v.is_some(), updated_v.is_some()) {
            (false, false) => {
                let _ = self.delete.insert(k);
            }
            (true, false) => (),
            (false, true) => {
                let _ = self.delete.insert(k);
            }
            (true, true) => {
                panic!("state error: key exists in the new and updtade columns at once")
            }
        }
    }
}

impl<Key: Clone + Eq + Hash, Value> From<Changeset<Key, Value>> for Vec<Change<Key, Value>> {
    fn from(changes: Changeset<Key, Value>) -> Self {
        let Changeset {
            delete,
            update,
            new,
        } = changes;
        delete
            .into_iter()
            .map(|k| Change::DeleteRecord(k))
            .chain(
                update
                    .into_iter()
                    .map(|(k, v)| Change::UpdateRecord((k, v))),
            )
            .chain(new.into_values().map(|v| Change::NewRecord(v)))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use uuid::Uuid;

    #[derive(Clone, PartialEq, Eq, Hash, Default)]
    struct Id(Uuid);
    impl Constructable for Id {
        fn new() -> Self {
            Id(Uuid::new_v4())
        }
    }

    #[test]
    fn it_generates_a_new_record() {
        let mut set: Changeset<Id, String> = Changeset::default();
        set.add("efgh".to_string());
        let changes = Vec::from(set.clone());
        assert_eq!(changes.len(), 1);
        assert!(changes.contains(&Change::NewRecord("efgh".to_string())));

        set.add("wxyz".to_string());
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 2);
        assert!(changes.contains(&Change::NewRecord("efgh".to_string())));
        assert!(changes.contains(&Change::NewRecord("wxyz".to_string())));
    }

    #[test]
    fn it_generates_a_delete_record() {
        let mut set: Changeset<Id, String> = Changeset::default();
        let id1 = Id::new();
        set.delete(id1.clone());
        let changes = Vec::from(set.clone());
        assert_eq!(changes.len(), 1);
        assert!(changes.contains(&Change::DeleteRecord(id1.clone())));

        let id2 = Id::new();
        set.delete(id2.clone());
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 2);
        assert!(changes.contains(&Change::DeleteRecord(id1)));
        assert!(changes.contains(&Change::DeleteRecord(id2)));
    }

    #[test]
    fn update_unrelated_records() {
        let mut set: Changeset<Id, String> = Changeset::default();
        let id1 = Id::new();
        let id2 = Id::new();
        set.update(id1.clone(), "abcd".to_owned());
        set.update(id2.clone(), "efgh".to_owned());
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 2);
        assert!(changes.contains(&Change::UpdateRecord((id1, "abcd".to_owned()))));
        assert!(changes.contains(&Change::UpdateRecord((id2, "efgh".to_owned()))));
    }

    #[test]
    fn delete_cancels_new() {
        let mut set: Changeset<Id, String> = Changeset::default();
        let key = set.add("efgh".to_string());
        set.delete(key);
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 0);
    }

    #[test]
    fn delete_cancels_update() {
        let mut set: Changeset<Id, String> = Changeset::default();
        let id = Id::new();
        set.update(id.clone(), "efgh".to_owned());
        set.delete(id.clone());
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 1);
        assert!(changes.contains(&Change::DeleteRecord(id)));
    }

    #[test]
    fn update_atop_new_is_new() {
        let mut set: Changeset<Id, String> = Changeset::default();
        let key = set.add("efgh".to_owned());
        set.update(key, "wxyz".to_owned());
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 1);
        assert!(changes.contains(&Change::NewRecord("wxyz".to_string())));
    }

    #[test]
    fn updates_get_squashed() {
        let mut set: Changeset<Id, String> = Changeset::default();
        let id1 = Id::new();
        let id2 = Id::new();
        set.update(id1.clone(), "efgh".to_owned());
        set.update(id2.clone(), "efgh".to_owned());
        let changes = Vec::from(set.clone());
        assert_eq!(changes.len(), 2);
        assert!(changes.contains(&Change::UpdateRecord((id1.clone(), "efgh".to_string()))));
        assert!(changes.contains(&Change::UpdateRecord((id2.clone(), "efgh".to_string()))));

        set.update(id1.clone(), "wxyz".to_owned());
        let changes = Vec::from(set);
        assert_eq!(changes.len(), 2);
        assert!(changes.contains(&Change::UpdateRecord((id1, "wxyz".to_string()))));
        assert!(changes.contains(&Change::UpdateRecord((id2, "efgh".to_string()))));
    }
}
