use std::collections::BTreeMap;

use nom::bytes::complete::tag;
use nom::bytes::complete::take_until;
use nom::character::complete::anychar;
use nom::combinator::all_consuming;
use nom::combinator::eof;
use nom::combinator::map;
use nom::combinator::map_parser;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;

use super::object_parser::standard_set_object;
use crate::context::parser_with_context;
use crate::context::Context;
use crate::context::ContextElement;
use crate::context::GlobalSettings;
use crate::context::List;
use crate::types::AffiliatedKeywordValue;
use crate::types::AffiliatedKeywords;
use crate::types::Keyword;

pub(crate) fn parse_affiliated_keywords<'g, 's>(
    global_settings: &'g GlobalSettings<'g, 's>,
    input: Vec<Keyword<'s>>,
) -> AffiliatedKeywords<'s> {
    let mut ret = BTreeMap::new();
    for kw in input.into_iter() {
        let translated_name = translate_name(global_settings, kw.key);
        if is_single_string_keyword(global_settings, translated_name.as_str()) {
            ret.insert(
                translated_name,
                AffiliatedKeywordValue::SingleString(kw.value),
            );
        } else if is_list_of_single_string_keyword(global_settings, translated_name.as_str()) {
            let list_of_strings = ret
                .entry(translated_name)
                .or_insert_with(|| AffiliatedKeywordValue::ListOfStrings(Vec::with_capacity(1)));
            match list_of_strings {
                AffiliatedKeywordValue::ListOfStrings(list_of_strings)
                    if list_of_strings.is_empty() =>
                {
                    list_of_strings.push(kw.value);
                }
                AffiliatedKeywordValue::ListOfStrings(list_of_strings) => {
                    list_of_strings.clear();
                    list_of_strings.push(kw.value);
                }
                _ => panic!("Invalid AffiliatedKeywordValue type."),
            }
        } else if is_list_of_objects_keyword(global_settings, translated_name.as_str()) {
            let initial_context = ContextElement::document_context();
            let initial_context = Context::new(global_settings, List::new(&initial_context));

            let (_remaining, optional_objects) = opt(all_consuming(map(
                tuple((
                    take_until("["),
                    tag("["),
                    map_parser(
                        recognize(many_till(anychar, peek(tuple((tag("]"), eof))))),
                        all_consuming(many0(parser_with_context!(standard_set_object)(
                            &initial_context,
                        ))),
                    ),
                    tag("]"),
                    eof,
                )),
                |(_, _, objects, _, _)| objects,
            )))(kw.key.into())
            .expect("Object parser should always succeed.");

            // TODO: This should be omitting footnote references
            let (_remaining, objects) = all_consuming(many0(parser_with_context!(
                standard_set_object
            )(&initial_context)))(kw.value.into())
            .expect("Object parser should always succeed.");
            let list_of_lists = ret.entry(translated_name).or_insert_with(|| {
                AffiliatedKeywordValue::ListOfListsOfObjects(Vec::with_capacity(1))
            });
            match list_of_lists {
                AffiliatedKeywordValue::ListOfListsOfObjects(list_of_lists) => {
                    list_of_lists.push((optional_objects, objects));
                }
                _ => panic!("Invalid AffiliatedKeywordValue type."),
            }
        } else {
            let list_of_strings = ret
                .entry(translated_name)
                .or_insert_with(|| AffiliatedKeywordValue::ListOfStrings(Vec::with_capacity(1)));
            match list_of_strings {
                AffiliatedKeywordValue::ListOfStrings(list_of_strings) => {
                    list_of_strings.push(kw.value);
                }
                _ => panic!("Invalid AffiliatedKeywordValue type."),
            }
        }
    }
    AffiliatedKeywords { keywords: ret }
}

fn translate_name<'g, 's>(global_settings: &'g GlobalSettings<'g, 's>, name: &'s str) -> String {
    for (src, dst) in global_settings.element_keyword_translation_alist {
        if name.eq_ignore_ascii_case(src) {
            return dst.to_lowercase();
        }
    }
    name.to_lowercase()
}

fn is_single_string_keyword<'g, 's>(
    _global_settings: &'g GlobalSettings<'g, 's>,
    name: &'s str,
) -> bool {
    // TODO: Is this defined by an elisp variable?
    for single_string_name in ["plot", "name"] {
        if name.eq_ignore_ascii_case(single_string_name) {
            return true;
        }
    }
    false
}

fn is_list_of_single_string_keyword<'g, 's>(
    _global_settings: &'g GlobalSettings<'g, 's>,
    name: &'s str,
) -> bool {
    // TODO: Is this defined by an elisp variable?
    for single_string_name in ["results"] {
        if name.eq_ignore_ascii_case(single_string_name) {
            return true;
        }
    }
    false
}

fn is_list_of_objects_keyword<'g, 's>(
    global_settings: &'g GlobalSettings<'g, 's>,
    name: &'s str,
) -> bool {
    for parsed_keyword in global_settings.element_parsed_keywords {
        if name.eq_ignore_ascii_case(parsed_keyword) {
            return true;
        }
    }
    false
}