from collections import OrderedDict
from importlib import import_module
from itertools import zip_longest
import regex as re
from copy import deepcopy

from ..data import language_order, language_locale_dict
from .locale import Locale

LOCALE_SPLIT_PATTERN = re.compile(r'-(?=[A-Z0-9]+$)')


def _isvalidlocale(locale):
    language = LOCALE_SPLIT_PATTERN.split(locale)[0]
    if language not in language_order:
        return False
    else:
        locales_list = language_locale_dict[language]
        if locale == language or locale in locales_list:
            return True
        else:
            return False


def _filter_valid_locales(locales):
    return [locale for locale in locales if _isvalidlocale(locale)]


def _construct_locales(languages, region):
    if region:
        possible_locales = [language + '-' + region for language in languages]
        locales = _filter_valid_locales(possible_locales)
    else:
        locales = languages
    return locales


class LocaleDataLoader:
    """Class that handles loading of locale instances."""

    _loaded_languages = {}
    _loaded_locales = {}

    def get_locale_map(self, languages=None, locales=None, region=None,
                       use_given_order=False, allow_conflicting_locales=False):
        """
        Get an ordered mapping with locale codes as keys
        and corresponding locale instances as values.

        :param languages:
            A list of language codes, e.g. ['en', 'es', 'zh-Hant'].
            If locales are not given, languages and region are
            used to construct locales to load.
        :type languages: list

        :param locales:
            A list of codes of locales which are to be loaded,
            e.g. ['fr-PF', 'qu-EC', 'af-NA']
        :type locales: list

        :param region:
            A region code, e.g. 'IN', '001', 'NE'.
            If locales are not given, languages and region are
            used to construct locales to load.
        :type region: str

        :param use_given_order:
            If True, the returned mapping is ordered in the order locales are given.
        :type use_given_order: bool

        :param allow_conflicting_locales:
            if True, locales with same language and different region can be loaded.
        :type allow_conflicting_locales: bool

        :return: ordered locale code to locale instance mapping
        """
        return OrderedDict(self._load_data(
            languages=languages, locales=locales, region=region, use_given_order=use_given_order,
            allow_conflicting_locales=allow_conflicting_locales))

    def get_locales(self, languages=None, locales=None, region=None,
                    use_given_order=False, allow_conflicting_locales=False):
        """
        Yield locale instances.

        :param languages:
            A list of language codes, e.g. ['en', 'es', 'zh-Hant'].
            If locales are not given, languages and region are
            used to construct locales to load.
        :type languages: list

        :param locales:
            A list of codes of locales which are to be loaded,
            e.g. ['fr-PF', 'qu-EC', 'af-NA']
        :type locales: list

        :param region:
            A region code, e.g. 'IN', '001', 'NE'.
            If locales are not given, languages and region are
            used to construct locales to load.
        :type region: str

        :param use_given_order:
            If True, the returned mapping is ordered in the order locales are given.
        :type use_given_order: bool

        :param allow_conflicting_locales:
            if True, locales with same language and different region can be loaded.
        :type allow_conflicting_locales: bool

        :yield: locale instances
        """
        for _, locale in self._load_data(
                languages=languages, locales=locales, region=region,
                use_given_order=use_given_order,
                allow_conflicting_locales=allow_conflicting_locales):
            yield locale

    def get_locale(self, shortname):
        """
        Get a locale instance.

        :param shortname:
            A locale code, e.g. 'fr-PF', 'qu-EC', 'af-NA'.
        :type shortname: str

        :return: locale instance
        """
        return list(self.get_locales(locales=[shortname]))[0]

    def _load_data(self, languages=None, locales=None, region=None,
                   use_given_order=False, allow_conflicting_locales=False):
        locale_dict = OrderedDict()
        if locales:
            invalid_locales = []
            for locale in locales:
                lang_reg = LOCALE_SPLIT_PATTERN.split(locale)
                if len(lang_reg) == 1:
                    lang_reg.append('')
                locale_dict[locale] = tuple(lang_reg)
                if not _isvalidlocale(locale):
                    invalid_locales.append(locale)
            if invalid_locales:
                raise ValueError("Unknown locale(s): %s"
                                 % ', '.join(map(repr, invalid_locales)))

            if not allow_conflicting_locales:
                if len(set(locales)) > len({t[0] for t in locale_dict.values()}):
                    raise ValueError("Locales should not have same language and different region")

        else:
            if languages is None:
                languages = language_order
            unsupported_languages = set(languages) - set(language_order)
            if unsupported_languages:
                raise ValueError("Unknown language(s): %s"
                                 % ', '.join(map(repr, unsupported_languages)))
            if region is None:
                region = ''
            locales = _construct_locales(languages, region)
            locale_dict.update(zip_longest(locales,
                               tuple(zip_longest(languages, [], fillvalue=region))))

        if not use_given_order:
            locale_dict = OrderedDict(sorted(locale_dict.items(),
                                      key=lambda x: language_order.index(x[1][0])))

        for shortname, lang_reg in locale_dict.items():
            if shortname not in self._loaded_locales:
                lang, reg = lang_reg
                if lang in self._loaded_languages:
                    locale = Locale(shortname, language_info=deepcopy(self._loaded_languages[lang]))
                    self._loaded_locales[shortname] = locale
                else:
                    language_info = getattr(
                        import_module('dateparser.data.date_translation_data.' + lang), 'info')
                    locale = Locale(shortname, language_info=deepcopy(language_info))
                    self._loaded_languages[lang] = language_info
                    self._loaded_locales[shortname] = locale
            yield shortname, self._loaded_locales[shortname]


default_loader = LocaleDataLoader()
