use std::collections::HashMap;

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Word(Vec<CharStatus<String>>);

impl Word {
    pub fn chars(&self) -> Vec<CharStatus<String>> {
        self.0.clone()
    }
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub enum CharStatus<T> {
    NotContained(T),
    Contained(T),
    Match(T),
    Unknown,
}

pub(super) fn compare_strings(s1: &str, s2: &str) -> Word {
    let mut result: Vec<CharStatus<String>> = Vec::with_capacity(s1.len());
    result.resize_with(s1.len(), || CharStatus::Unknown);

    let mut s1_char_count: HashMap<char, usize> = HashMap::new();
    let mut s2_char_count: HashMap<char, usize> = HashMap::new();

    for c in s1.chars() {
        *s1_char_count.entry(c).or_insert(0) += 1;
    }

    for ((c1, c2), res) in s1.chars().zip(s2.chars()).zip(result.iter_mut()) {
        if c1 == c2 {
            *res = CharStatus::Match(c2.to_string());
            *s2_char_count.entry(c2).or_insert(0) += 1;
        } else {
            *res = CharStatus::Unknown;
        }
    }

    for (res, c2) in result.iter_mut().zip(s2.chars()) {
        if res == &CharStatus::Unknown {
            let c1_count = s1_char_count.get(&c2).unwrap_or(&0);
            let c2_count = s2_char_count.get(&c2).unwrap_or(&0);

            if *c1_count > 0 && c1_count > c2_count {
                *res = CharStatus::Contained(c2.to_string());
                *s2_char_count.entry(c2).or_insert(0) += 1;
            } else {
                *res = CharStatus::NotContained(c2.to_string());
            }
        }
    }

    Word(result)
}

#[cfg(test)]
mod test {

    use super::*;
    #[test]
    fn test_compare_strings() {
        let source = "HALLO";

        let want = Word(vec![
            CharStatus::NotContained("0".to_owned()),
            CharStatus::NotContained("0".to_owned()),
            CharStatus::NotContained("0".to_owned()),
            CharStatus::NotContained("0".to_owned()),
            CharStatus::NotContained("0".to_owned()),
        ]);
        let input = "00000";

        let got = compare_strings(source, input);
        assert_eq!(want, got);
        let source = "HALLO";

        let want = Word(vec![
            CharStatus::NotContained("L".to_owned()),
            CharStatus::NotContained("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::NotContained("L".to_owned()),
        ]);
        let input = "LLLLL";

        let got = compare_strings(source, input);
        assert_eq!(want, got);

        let want = Word(vec![
            CharStatus::Match("H".to_owned()),
            CharStatus::Match("A".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::Match("O".to_owned()),
        ]);
        let input = "HALLO";

        let got = compare_strings(source, input);
        assert_eq!(want, got);

        let want = Word(vec![
            CharStatus::Match("H".to_owned()),
            CharStatus::NotContained("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::Match("O".to_owned()),
        ]);
        let input = "HLLLO";

        let got = compare_strings(source, input);
        assert_eq!(want, got);

        let want = Word(vec![
            CharStatus::Match("H".to_owned()),
            CharStatus::Contained("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::NotContained("I".to_owned()),
            CharStatus::NotContained("L".to_owned()),
        ]);
        let input = "HLLIL";

        let got = compare_strings(source, input);
        assert_eq!(want, got);

        let want = Word(vec![
            CharStatus::Contained("L".to_owned()),
            CharStatus::NotContained("L".to_owned()),
            CharStatus::Match("L".to_owned()),
            CharStatus::Contained("A".to_owned()),
            CharStatus::Match("O".to_owned()),
        ]);
        let input = "LLLAO";

        let got = compare_strings(source, input);
        assert_eq!(want, got);
    }
}