use std::collections::HashMap;

type Branch<'a> = HashMap<&'a str, Node<'a>>;

#[derive(Clone, Debug, PartialEq)]
pub enum Node<'a> {
    Branch(Branch<'a>),
    Leaf(&'a str),
}

/** parser for the dbtin dump format, does not allow any escape symbols.

```text
:PATH/TO/OBJECT
KEY1: VALUE1
KEY2: VALUE2
```

or alternatively:

```text
PATH/TO/OBJECT:KEY1:VALUE1
PATH/TO/OBJECT:KEY2:VALUE2
```

**/
pub fn parse(s: &str) -> Option<Branch<'_>> {
    let mut it = s.lines().filter(|i| !i.trim().is_empty()).peekable();

    let i = it.peek()?;
    let dcl = i.find(':')?;
    // find out which type of dbtin dump we have here
    if dcl != 0 && i[dcl + 1..].find(':').is_some() {
        // (OBJ:KEY:VALUE)+
        parse_colsv(it)
    } else {
        // :OBJ\n(KEY:VALUE)+
        parse_groups(it)
    }
}

fn parse_colsv<'a>(it: impl Iterator<Item = &'a str>) -> Option<Branch<'a>> {
    let mut ret = HashMap::new();

    for i in it {
        let dcl1 = i.find(':')?;
        if dcl1 == 0 {
            // format error, this indicates a group instead
            return None;
        }
        let kvp = &i[dcl1 + 1..];
        let dcl2 = kvp.find(':')?;
        let (obj, key, value) = (&i[..dcl1], &kvp[..dcl2], &kvp[dcl2 + 1..]);
        let sel = select(&mut ret, obj)?;
        if !final_leaf_push(sel, key, value) {
            return None;
        }
    }

    Some(ret)
}

#[derive(Default)]
struct GroupParser<'a> {
    ret: Branch<'a>,
    selobj: Option<(&'a str, HashMap<&'a str, &'a str>)>,
}

fn parse_groups<'a>(it: impl Iterator<Item = &'a str>) -> Option<Branch<'a>> {
    let mut gp = GroupParser::default();

    for i in it {
        let dcl = i.find(':')?;

        if dcl == 0 {
            // object name
            if !gp.finish_selobj() {
                return None;
            }
            gp.selobj = Some((&i[1..], HashMap::new()));
        } else if let Some((_, ref mut kvm)) = &mut gp.selobj {
            // key-value pair
            kvm.insert(&i[..dcl], i[dcl + 1..].trim_start());
        } else {
            return None;
        }
    }

    if gp.finish_selobj() {
        Some(gp.ret)
    } else {
        None
    }
}

impl<'a> GroupParser<'a> {
    fn finish_selobj(&mut self) -> bool {
        if let Some((oname, okvm)) = self.selobj.take() {
            if let Some(sel) = select(&mut self.ret, oname) {
                okvm.into_iter()
                    .all(|(key, value)| final_leaf_push(sel, key, value))
            } else {
                false
            }
        } else {
            true
        }
    }
}

fn final_leaf_push<'a>(sel: &mut Branch<'a>, key: &'a str, value: &'a str) -> bool {
    use std::collections::hash_map::Entry;
    match sel.entry(key) {
        Entry::Occupied(_) => return false,
        Entry::Vacant(vac) => vac.insert(Node::Leaf(value)),
    };
    true
}

fn select<'s, 'a>(ret: &'s mut Branch<'a>, obj: &'a str) -> Option<&'s mut Branch<'a>> {
    obj.split('/')
        .filter(|i| !i.is_empty())
        .try_fold(ret, |sel, i| {
            sel.entry(i)
                .or_insert_with(|| Node::Branch(HashMap::new()))
                .branch_mut()
        })
}

impl<'a> Node<'a> {
    fn branch_mut<'s>(&'s mut self) -> Option<&'s mut Branch<'a>> {
        if let Node::Branch(ref mut x) = self {
            Some(x)
        } else {
            None
        }
    }
}

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

    fn ex0() -> HashMap<&'static str, Node<'static>> {
        let mut inner2 = HashMap::new();

        let mut inner1 = HashMap::new();
        inner1.insert("trfull", Node::Leaf("Ethik"));
        inner1.insert("tanken", Node::Leaf("Ja  "));
        inner2.insert("ETH", Node::Branch(inner1));

        let mut inner1 = HashMap::new();
        inner1.insert("trfull", Node::Leaf("Rufen"));
        inner1.insert("tanken", Node::Leaf("Nein"));
        inner2.insert("RUF", Node::Branch(inner1));

        let mut inner1 = HashMap::new();
        inner1.insert("locan ", Node::Leaf("bl	uüpp"));
        inner1.insert("Fach", Node::Branch(inner2));

        let mut inner2 = HashMap::new();
        inner2.insert("Orga", Node::Branch(inner1));

        let mut inner1 = HashMap::new();
        inner1.insert("Schule", Node::Branch(inner2));

        inner1
    }

    #[test]
    fn ex0_groups() {
        assert_eq!(
            parse(
                r#"
:Schule/Orga/Fach/ETH
trfull:	Ethik
tanken: Ja  
:Schule/Orga/Fach/RUF
trfull:	Rufen
tanken:	Nein
:Schule/Orga
locan :	bl	uüpp
 "#
            ),
            Some(ex0())
        );
    }

    #[test]
    fn ex0_colsv() {
        assert_eq!(
            parse(
                r#"
Schule/Orga/Fach/ETH:trfull:Ethik
Schule/Orga/Fach/ETH:tanken:Ja  
Schule/Orga/Fach/RUF:trfull:Rufen
Schule/Orga/Fach/RUF:tanken:Nein
Schule/Orga:locan :bl	uüpp
 "#
            ),
            Some(ex0())
        );
    }
}