from datetime import datetime
from ngram_viewer.models import *
from speakers.models import Speaker
from watson import search as watson
from collections import defaultdict, OrderedDict
import logging


class NgramSearch(object):
    """
    Class that handles the search for ngrams per year. Inputs are the user query
    and search options. User query will be splitted and every split will be used
    as a single query.
    Every singel query returns a QuerySet. Data from those will be
    retrived and converted to valid chart.js data sets. Besides the query the
    user can pass some search options to the class like case sensitive and case
    insensitve. This Class handles search per year which is kind of the default.
    """

    def __init__(self, clean_data):
        super(NgramSearch, self).__init__()

        self.cs_query = clean_data["query"]
        self.case_sensitive = clean_data["case_sensitive"]
        self.search_plus = clean_data["search_plus"]
        self.ignore_missing = clean_data["ignore_missing"]
        self.corpus_choice = clean_data["corpus_choice"]
        self.sub_querys_dict = defaultdict(list)
        self.filtered_sets_dict = defaultdict(list)
        self.raw_data = []

    def get_time_from_year_str(self, query_data, date_format="%Y"):
        """
        This function creates a valid datetime object from an input string.
        Works with strings consisting of %Y, %Y-%m or %Y.%m.%d. Not needed for
        now.
        """
        for ngram_dict in query_data:
            for key in ngram_dict:
                data_series = ngram_dict[key]
                for value_pair in data_series:
                    valid_time = datetime.strptime(value_pair["x"], date_format)
                    valid_time_str = valid_time.strftime("%Y-%m-%dT%H:%M:%S")
                    value_pair["x"] = valid_time_str
        return query_data

    def get_sub_querys(self):
        """
        This function takes the comma separated query string and splits it into
        the needed substring and sorts them into a dictionary according to their
        length to distinguish between unigrams, bigrams and so on.
        """
        # Some checks to see if the input query is valid
        if(self.cs_query.startswith(",")):
            self.cs_query = self.cs_query[1:]
        elif(self.cs_query.endswith(",")):
            self.cs_query = self.cs_query[:-1]

        logger = logging.getLogger(__name__)
        sub_querys = self.cs_query.split(",")
        logger.info(sub_querys)
        sub_querys_stripped = []
        for sub_query in sub_querys:
            if(sub_query.startswith(" ")):
                sub_querys_stripped.append(sub_query[1:])
            elif(sub_query.endswith(" ")):
                sub_querys_stripped.append(sub_query[:-1])
            else:
                sub_querys_stripped.append(sub_query)
        sub_querys_dict = defaultdict(list)
        for sub_query in sub_querys_stripped:
            # Checks for words starting with german Umlaut or special characters like "§$%&"
            sort_key = sub_query[0].upper()
            if(sort_key in ["Ä", "Ö", "Ü"]):
                sort_key = "_Non_ASCII"
            elif(sort_key.isascii() is True and sort_key.isalnum() is False):
                sort_key = "_Non_ASCII"
            elif(not sort_key.isascii()):
                sort_key = "_Non_ASCII"
            else:
                sort_key = sort_key

            if(len(sub_query.split()) == 1):
                main_class = "One"
            elif(len(sub_query.split()) == 2):
                main_class = "Two"
            elif(len(sub_query.split()) == 3):
                main_class = "Three"
            elif(len(sub_query.split()) == 4):
                main_class = "Four"
            elif(len(sub_query.split()) == 5):
                main_class = "Five"
            else:
                sub_querys_dict["invalid"].append(sub_query)
                continue

            model = "Key{}_{}Gram_{}".format(sort_key,
                                             main_class,
                                             self.corpus_choice)
            model = globals()[model]
            sub_querys_dict[model].append(sub_query)
        self.sub_querys_dict = sub_querys_dict


    def enhanced_search(self):
        """
        This function takes the sub_querys_dict and searches the database for every
        subquery and returns QuerySets for those. In a second step the QuerySets
        will be searched again with a regex to assure that QuerySets only contain
        objects with an exact word match.
        """
        # first broad search to catch every entry containing the query
        # Without enhanced search syntax
        if(self.search_plus is False):
            query_sets_dict = defaultdict(list)
            for key, values in self.sub_querys_dict.items():
                if(key != "invalid"):
                    for value in values:
                        query_set = key.objects.filter(ngram__icontains=value)  # Case-insensitve. Checks for entires that somehow contain the input string. Equal to LIKE SQL syntax. Should be faster than exact match and the QuerySet can be used for more specific search operations.
                        query_sets_dict[key].append((query_set, value))
            # Case-insensitive exact match of entries
            if(self.case_sensitive is False):
                filtered_sets_dict = defaultdict(list)
                for key, query_sets in query_sets_dict.items():
                    for query_set in query_sets:
                        r_filtered = query_set[0].filter(ngram__iexact=query_set[1])  # Matches entries that contain the exact query
                        filtered_sets_dict[key].append((r_filtered, query_set[1]))
            # Case-sensitive exact match of entries
            elif(self.case_sensitive is True):
                filtered_sets_dict = defaultdict(list)
                for key, query_sets in query_sets_dict.items():
                    for query_set in query_sets:
                        r_filtered = query_set[0].filter(ngram__exact=query_set[1])  # Matches entries that contain the exact query
                        filtered_sets_dict[key].append((r_filtered, query_set[1]))
        # With enhanced search syntax
        elif(self.search_plus is True):
            # Case-insensitive exact match of entries
            if(self.case_sensitive is False):
                filtered_sets_dict = defaultdict(list)
                for key, values in self.sub_querys_dict.items():
                    if(key != "invalid"):
                        for value in values:
                            if(value.endswith("__")):
                                r_filtered = key.objects.filter(ngram__iexact=value[:-2])
                            else:
                                r_filtered = key.objects.filter(ngram__iregex=value)  # Matches entries that contain regex query case-insensitive
                            filtered_sets_dict[key].append((r_filtered, value))
            # Case-sensitive exact match of entries
            elif(self.case_sensitive is True):
                filtered_sets_dict = defaultdict(list)
                for key, values in self.sub_querys_dict.items():
                    if(key != "invalid"):
                        for value in values:
                            if(value.endswith("__")):
                                r_filtered = key.objects.filter(ngram__exact=value[:-2])
                            else:
                                r_filtered = key.objects.filter(ngram__regex=value)  # Matches entries that contain regex query case-sensitive
                            filtered_sets_dict[key].append((r_filtered, value))

        self.filtered_sets_dict = filtered_sets_dict

    def query_sets_to_data(self):
        """
        Converts QuerySets to data dictionaries. Fills missing years with zero
        value counts for ngrams. Also sums upper and lower case n-grams to one
        ngram with one count.
        """
        data = []
        for key, query_sets in self.filtered_sets_dict.items():
            for query_set in query_sets:
                data_line = {}
                for ngram in query_set[0]:
                    if ngram.key in data_line:
                        data_line[ngram.key] += ngram.count
                        # print(ngram.key, ngram.count, ngram.one_gram)
                    else:
                        data_line[ngram.key] = ngram.count
                        # print(ngram.key, ngram.count, ngram.one_gram)
                    # print(data_line)
                data.append({query_set[1]: data_line})

        # checks for missing years and fills the mwith zero
        if(self.ignore_missing is False):
            years = [year for year in range(1949, 2018)]
            for data_line in data:
                for key, values in data_line.items():
                    for year in years:
                        if(str(year) not in values):
                            values[str(year)] = 0
                    data_line[key] = dict(sorted(values.items()))
        elif(self.ignore_missing is True):
            for data_line in data:
                for key, values in data_line.items():
                    data_line[key] = dict(sorted(values.items()))
        self.raw_data = data

    def convert_to_data_set(self):
        """
        Converts the cleaned data from query_sets_to_data into valid chart.js
        data set json like objects.
        """
        data_set = []
        for data_line in self.raw_data:
            data_set_line = defaultdict(list)
            for key, values in data_line.items():
                for year, count in values.items():
                    new_data_point = {}
                    new_data_point["y"] = count
                    new_data_point["x"] = year
                    data_set_line[key].append(new_data_point)
            data_set.append(data_set_line)
        self.data_set = data_set


class NgramSearchSpeaker(NgramSearch):
    """
    Class that handles the search for ngrams per speaker. Inputs are the user
    query and search options. User query can only contain one n-gram.
    The query returns a QuerySet. Data from thise will be
    retrived and converted to a valid chart.js data set. Besides the query the
    user can pass some search options to the class like case sensitive and case
    insensitve. Inherits from NgramSearch.
    """

    def __init__(self, clean_data):
        super(NgramSearch, self).__init__()

        self.cs_query = clean_data["query"].split(",")[0]
        self.case_sensitive = clean_data["case_sensitive"]
        self.search_plus = clean_data["search_plus"]
        self.ignore_missing = clean_data["ignore_missing"]
        self.corpus_choice = clean_data["corpus_choice"]
        self.sub_querys_dict = defaultdict(list)
        self.filtered_sets_dict = defaultdict(list)
        self.raw_data = []

    def get_speaker_name(self, query_data):
        """
        This function takes the speaker ID and gets the corresponding speaker
        name.
        """
        for ngram_dict in query_data:
            for key in ngram_dict:
                data_series = ngram_dict[key]
                for value_pair in data_series:
                    speaker_id = value_pair["x"]
                    if(speaker_id != "None"):
                        speaker_details = Speaker.objects.get(pk=speaker_id)
                        value_pair["x"] = (speaker_id
                                           + ": "
                                           + speaker_details.first_name
                                           + " "
                                           + speaker_details.last_name
                                           + " ({})".format(speaker_details.party))
                    elif(speaker_id == "None"):
                        value_pair["x"] = "Redner nicht identifiziert."
        return query_data

    def query_sets_to_data(self):
        """
        Converts QuerySets to data dictionaries.
        Also sums upper and lower case n-grams to one ngram
        with one count.
        """
        data = []
        for key, query_sets in self.filtered_sets_dict.items():
            for query_set in query_sets:
                data_line = {}
                for ngram in query_set[0]:
                    if ngram.key in data_line:
                        data_line[ngram.key] += ngram.count
                        # print(ngram.key, ngram.count, ngram.one_gram)
                    else:
                        data_line[ngram.key] = ngram.count
                        # print(ngram.key, ngram.count, ngram.one_gram)
                    # print(data_line)
                data.append({query_set[1]: data_line})

        for d in data:
            for key, value in d.items():
                value = OrderedDict(sorted(value.items(), key=lambda t: t[1], reverse=True))
                value = dict(value)
                d[key] = value
        self.raw_data = data
