import gettext
import os

from translationstring import (
    Translator,
    Pluralizer,
    TranslationString,  # API
    TranslationStringFactory,  # API
)

from pyramid.compat import PY2
from pyramid.decorator import reify

from pyramid.interfaces import (
    ILocalizer,
    ITranslationDirectories,
    ILocaleNegotiator,
)

from pyramid.threadlocal import get_current_registry

TranslationString = TranslationString  # PyFlakes
TranslationStringFactory = TranslationStringFactory  # PyFlakes

DEFAULT_PLURAL = lambda n: int(n != 1)


class Localizer(object):
    """
    An object providing translation and pluralizations related to
    the current request's locale name.  A
    :class:`pyramid.i18n.Localizer` object is created using the
    :func:`pyramid.i18n.get_localizer` function.
    """

    def __init__(self, locale_name, translations):
        self.locale_name = locale_name
        self.translations = translations
        self.pluralizer = None
        self.translator = None

    def translate(self, tstring, domain=None, mapping=None):
        """
        Translate a :term:`translation string` to the current language
        and interpolate any *replacement markers* in the result.  The
        ``translate`` method accepts three arguments: ``tstring``
        (required), ``domain`` (optional) and ``mapping`` (optional).
        When called, it will translate the ``tstring`` translation
        string to a ``unicode`` object using the current locale.  If
        the current locale could not be determined, the result of
        interpolation of the default value is returned.  The optional
        ``domain`` argument can be used to specify or override the
        domain of the ``tstring`` (useful when ``tstring`` is a normal
        string rather than a translation string).  The optional
        ``mapping`` argument can specify or override the ``tstring``
        interpolation mapping, useful when the ``tstring`` argument is
        a simple string instead of a translation string.

        Example::

           from pyramid.18n import TranslationString
           ts = TranslationString('Add ${item}', domain='mypackage',
                                  mapping={'item':'Item'})
           translated = localizer.translate(ts)

        Example::

           translated = localizer.translate('Add ${item}', domain='mypackage',
                                            mapping={'item':'Item'})

        """
        if self.translator is None:
            self.translator = Translator(self.translations)
        return self.translator(tstring, domain=domain, mapping=mapping)

    def pluralize(self, singular, plural, n, domain=None, mapping=None):
        """
        Return a Unicode string translation by using two
        :term:`message identifier` objects as a singular/plural pair
        and an ``n`` value representing the number that appears in the
        message using gettext plural forms support.  The ``singular``
        and ``plural`` objects should be unicode strings. There is no
        reason to use translation string objects as arguments as all
        metadata is ignored.

        ``n`` represents the number of elements. ``domain`` is the
        translation domain to use to do the pluralization, and ``mapping``
        is the interpolation mapping that should be used on the result. If
        the ``domain`` is not supplied, a default domain is used (usually
        ``messages``).

        Example::

           num = 1
           translated = localizer.pluralize('Add ${num} item',
                                            'Add ${num} items',
                                            num,
                                            mapping={'num':num})

        If using the gettext plural support, which is required for
        languages that have pluralisation rules other than n != 1, the
        ``singular`` argument must be the message_id defined in the
        translation file. The plural argument is not used in this case.

        Example::

           num = 1
           translated = localizer.pluralize('item_plural',
                                            '',
                                            num,
                                            mapping={'num':num})


        """
        if self.pluralizer is None:
            self.pluralizer = Pluralizer(self.translations)
        return self.pluralizer(
            singular, plural, n, domain=domain, mapping=mapping
        )


def default_locale_negotiator(request):
    """ The default :term:`locale negotiator`.  Returns a locale name
    or ``None``.

    - First, the negotiator looks for the ``_LOCALE_`` attribute of
      the request object (possibly set by a view or a listener for an
      :term:`event`). If the attribute exists and it is not ``None``,
      its value will be used.

    - Then it looks for the ``request.params['_LOCALE_']`` value.

    - Then it looks for the ``request.cookies['_LOCALE_']`` value.

    - Finally, the negotiator returns ``None`` if the locale could not
      be determined via any of the previous checks (when a locale
      negotiator returns ``None``, it signifies that the
      :term:`default locale name` should be used.)
    """
    name = '_LOCALE_'
    locale_name = getattr(request, name, None)
    if locale_name is None:
        locale_name = request.params.get(name)
        if locale_name is None:
            locale_name = request.cookies.get(name)
    return locale_name


def negotiate_locale_name(request):
    """ Negotiate and return the :term:`locale name` associated with
    the current request."""
    try:
        registry = request.registry
    except AttributeError:
        registry = get_current_registry()
    negotiator = registry.queryUtility(
        ILocaleNegotiator, default=default_locale_negotiator
    )
    locale_name = negotiator(request)

    if locale_name is None:
        settings = registry.settings or {}
        locale_name = settings.get('default_locale_name', 'en')

    return locale_name


def get_locale_name(request):
    """
    .. deprecated:: 1.5
        Use :attr:`pyramid.request.Request.locale_name` directly instead.
        Return the :term:`locale name` associated with the current request.
    """
    return request.locale_name


def make_localizer(current_locale_name, translation_directories):
    """ Create a :class:`pyramid.i18n.Localizer` object
    corresponding to the provided locale name from the
    translations found in the list of translation directories."""
    translations = Translations()
    translations._catalog = {}

    locales_to_try = []
    if '_' in current_locale_name:
        locales_to_try = [current_locale_name.split('_')[0]]
    locales_to_try.append(current_locale_name)

    # intent: order locales left to right in least specific to most specific,
    # e.g. ['de', 'de_DE'].  This services the intent of creating a
    # translations object that returns a "more specific" translation for a
    # region, but will fall back to a "less specific" translation for the
    # locale if necessary.  Ordering from least specific to most specific
    # allows us to call translations.add in the below loop to get this
    # behavior.

    for tdir in translation_directories:
        locale_dirs = []
        for lname in locales_to_try:
            ldir = os.path.realpath(os.path.join(tdir, lname))
            if os.path.isdir(ldir):
                locale_dirs.append(ldir)

        for locale_dir in locale_dirs:
            messages_dir = os.path.join(locale_dir, 'LC_MESSAGES')
            if not os.path.isdir(os.path.realpath(messages_dir)):
                continue
            for mofile in os.listdir(messages_dir):
                mopath = os.path.realpath(os.path.join(messages_dir, mofile))
                if mofile.endswith('.mo') and os.path.isfile(mopath):
                    with open(mopath, 'rb') as mofp:
                        domain = mofile[:-3]
                        dtrans = Translations(mofp, domain)
                        translations.add(dtrans)

    return Localizer(
        locale_name=current_locale_name, translations=translations
    )


def get_localizer(request):
    """
    .. deprecated:: 1.5
        Use the :attr:`pyramid.request.Request.localizer` attribute directly
        instead.  Retrieve a :class:`pyramid.i18n.Localizer` object
        corresponding to the current request's locale name.
    """
    return request.localizer


class Translations(gettext.GNUTranslations, object):
    """An extended translation catalog class (ripped off from Babel) """

    DEFAULT_DOMAIN = 'messages'

    def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN):
        """Initialize the translations catalog.

        :param fileobj: the file-like object the translation should be read
                        from
        """
        # germanic plural by default; self.plural will be overwritten by
        # GNUTranslations._parse (called as a side effect if fileobj is
        # passed to GNUTranslations.__init__) with a "real" self.plural for
        # this domain; see https://github.com/Pylons/pyramid/issues/235
        # It is only overridden the first time a new message file is found
        # for a given domain, so all message files must have matching plural
        # rules if they are in the same domain. We keep track of if we have
        # overridden so we can special case the default domain, which is always
        # instantiated before a message file is read.
        # See also https://github.com/Pylons/pyramid/pull/2102
        self.plural = DEFAULT_PLURAL
        gettext.GNUTranslations.__init__(self, fp=fileobj)
        self.files = list(filter(None, [getattr(fileobj, 'name', None)]))
        self.domain = domain
        self._domains = {}

    @classmethod
    def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
        """Load translations from the given directory.

        :param dirname: the directory containing the ``MO`` files
        :param locales: the list of locales in order of preference (items in
                        this list can be either `Locale` objects or locale
                        strings)
        :param domain: the message domain
        :return: the loaded catalog, or a ``NullTranslations`` instance if no
                 matching translations were found
        :rtype: `Translations`
        """
        if locales is not None:
            if not isinstance(locales, (list, tuple)):
                locales = [locales]
            locales = [str(l) for l in locales]
        if not domain:
            domain = cls.DEFAULT_DOMAIN
        filename = gettext.find(domain, dirname, locales)
        if not filename:
            return gettext.NullTranslations()
        with open(filename, 'rb') as fp:
            return cls(fileobj=fp, domain=domain)

    def __repr__(self):
        return '<%s: "%s">' % (
            type(self).__name__,
            self._info.get('project-id-version'),
        )

    def add(self, translations, merge=True):
        """Add the given translations to the catalog.

        If the domain of the translations is different than that of the
        current catalog, they are added as a catalog that is only accessible
        by the various ``d*gettext`` functions.

        :param translations: the `Translations` instance with the messages to
                             add
        :param merge: whether translations for message domains that have
                      already been added should be merged with the existing
                      translations
        :return: the `Translations` instance (``self``) so that `merge` calls
                 can be easily chained
        :rtype: `Translations`
        """
        domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
        if domain == self.DEFAULT_DOMAIN and self.plural is DEFAULT_PLURAL:
            self.plural = translations.plural

        if merge and domain == self.domain:
            return self.merge(translations)

        existing = self._domains.get(domain)
        if merge and existing is not None:
            existing.merge(translations)
        else:
            translations.add_fallback(self)
            self._domains[domain] = translations

        return self

    def merge(self, translations):
        """Merge the given translations into the catalog.

        Message translations in the specified catalog override any messages
        with the same identifier in the existing catalog.

        :param translations: the `Translations` instance with the messages to
                             merge
        :return: the `Translations` instance (``self``) so that `merge` calls
                 can be easily chained
        :rtype: `Translations`
        """
        if isinstance(translations, gettext.GNUTranslations):
            self._catalog.update(translations._catalog)
            if isinstance(translations, Translations):
                self.files.extend(translations.files)

        return self

    def dgettext(self, domain, message):
        """Like ``gettext()``, but look the message up in the specified
        domain.
        """
        return self._domains.get(domain, self).gettext(message)

    def ldgettext(self, domain, message):
        """Like ``lgettext()``, but look the message up in the specified
        domain.
        """
        return self._domains.get(domain, self).lgettext(message)

    def dugettext(self, domain, message):
        """Like ``ugettext()``, but look the message up in the specified
        domain.
        """
        if PY2:
            return self._domains.get(domain, self).ugettext(message)
        else:
            return self._domains.get(domain, self).gettext(message)

    def dngettext(self, domain, singular, plural, num):
        """Like ``ngettext()``, but look the message up in the specified
        domain.
        """
        return self._domains.get(domain, self).ngettext(singular, plural, num)

    def ldngettext(self, domain, singular, plural, num):
        """Like ``lngettext()``, but look the message up in the specified
        domain.
        """
        return self._domains.get(domain, self).lngettext(singular, plural, num)

    def dungettext(self, domain, singular, plural, num):
        """Like ``ungettext()`` but look the message up in the specified
        domain.
        """
        if PY2:
            return self._domains.get(domain, self).ungettext(
                singular, plural, num
            )
        else:
            return self._domains.get(domain, self).ngettext(
                singular, plural, num
            )


class LocalizerRequestMixin(object):
    @reify
    def localizer(self):
        """ Convenience property to return a localizer """
        registry = self.registry

        current_locale_name = self.locale_name
        localizer = registry.queryUtility(ILocalizer, name=current_locale_name)

        if localizer is None:
            # no localizer utility registered yet
            tdirs = registry.queryUtility(ITranslationDirectories, default=[])
            localizer = make_localizer(current_locale_name, tdirs)

            registry.registerUtility(
                localizer, ILocalizer, name=current_locale_name
            )

        return localizer

    @reify
    def locale_name(self):
        locale_name = negotiate_locale_name(self)
        return locale_name
