# -*- Mode: Python; test-case-name: flumotion.test.test_common_messages -*-
# vi:si:et:sw=4:sts=4:ts=4
#
# Flumotion - a streaming media server
# Copyright (C) 2004,2005,2006-2008 Fluendo, S.L. (www.fluendo.com).
# All rights reserved.

# This file may be distributed and/or modified under the terms of
# the GNU General Public License version 2 as published by
# the Free Software Foundation.
# This file is distributed without any warranty; without even the implied
# warranty of merchantability or fitness for a particular purpose.
# See "LICENSE.GPL" in the source distribution for more information.

# Licensees having purchased or holding a valid Flumotion Advanced
# Streaming Server license may use this file in accordance with the
# Flumotion Advanced Streaming Server Commercial License Agreement.
# See "LICENSE.Flumotion" in the source distribution for more information.

# Headers in this file shall remain intact.

"""
support for serializable translatable messages from component/manager to admin
"""

import time
import gettext

from elisa.extern.log import log

from twisted.spread import pb
from twisted.python import util

ERROR = 1
WARNING = 2
INFO = 3

def N_(format):
    """
    Mark a singular string for translation, without translating it.
    """
    return format

def ngettext(singular, plural, count):
    """
    Mark a plural string for translation, without translating it.
    """
    return (singular, plural, count)

def gettexter(domain):
    """
    Return a function that takes a format string or tuple, and additional
    format args,
    and creates a L{Translatable} from it.

    Example::

        T_ = messages.gettexter('flumotion')
        t = T_(N_("Could not find '%s'."), file)

    @param domain: the gettext domain to create translatables for.
    """
    def create(format, *args):
        if isinstance(format, str):
            return TranslatableSingular(domain, format, *args)
        else:
            return TranslatablePlural(domain, format, *args)

    return lambda *args: create(*args)

class Translatable(pb.Copyable, pb.RemoteCopy):
    """
    I represent a serializable translatable gettext msg.
    """
    domain = None
    
# NOTE: subclassing FancyEqMixin allows us to compare two
# RemoteCopy instances gotten from the same Copyable; this allows
# state _append and _remove to work correctly
# Take note however that this also means that two RemoteCopy objects
# of two different Copyable objects, but with the same args, will
# also pass equality
# For our purposes, this is fine.

class TranslatableSingular(Translatable, util.FancyEqMixin):
    """
    I represent a translatable gettext msg in the singular form.
    """

    compareAttributes = ["domain", "format", "args"]

    def __init__(self, domain, format, *args):
        """
        @param domain: the text domain for translations of this message
        @param format: a format string
        @param args:   any arguments to the format string
        """
        self.domain = domain
        self.format = format
        self.args = args

    def __str__(self):
        txt = self.format % self.args
        return txt
        
pb.setUnjellyableForClass(TranslatableSingular, TranslatableSingular)

class TranslatablePlural(Translatable, util.FancyEqMixin):
    """
    I represent a translatable gettext msg in the plural form.
    """

    compareAttributes = ["domain", "singular", "plural", "count", "args"]
    
    def __init__(self, domain, format, *args):
        """
        @param domain: the text domain for translations of this message
        @param format: a (singular, plural, count) tuple
        @param args:   any arguments to the format string
        """
        singular, plural, count = format
        self.domain = domain
        self.singular = singular
        self.plural = plural
        self.count = count
        self.args = args

    def __str__(self):
        if len(txt.count) > 0:
            txt = self.plural % self.args
        else:
            txt = self.singular % self.args
        return txt
        
pb.setUnjellyableForClass(TranslatablePlural, TranslatablePlural)
    
class Translator(log.Loggable):
    """
    I translate translatables and messages.
    I need to be told where locale directories can be found for all domains
    I need to translate for.
    """

    logCategory = "translator"

    def __init__(self):
        self._localedirs = {} # domain name -> list of locale dirs

    def addLocaleDir(self, domain, dir):
        """
        Add a locale directory for the given text domain.
        """
        if not domain in self._localedirs.keys():
            self._localedirs[domain] = []

        if not dir in self._localedirs[domain]:
            self.debug('Adding localedir %s for domain %s' % (dir, domain))
            self._localedirs[domain].append(dir)

    def translateTranslatable(self, translatable, lang=None):
        """
        Translate a translatable object, in the given language.

        @param lang: language code (or the current locale if None)
        """
        self.debug("Attempting to translate %r on %r", translatable.format,
                   lang)
        # gettext.translation objects are rumoured to be cached (API docs)
        domain = translatable.domain
        t = None
        if domain in self._localedirs.keys():
            # FIXME: possibly trap IOError and handle nicely ?
            for localedir in self._localedirs[domain]:
                try:
                    t = gettext.translation(domain, localedir, lang)
                except IOError, error:
                    self.debug(error)
        else:
            self.debug('no locales for domain %s' % domain)
                
        format = None
        if not t:
            # if no translation object found, fall back to C
            self.debug('no translation found, falling back to C')
            if isinstance(translatable, TranslatableSingular):
                format = translatable.format
            elif isinstance(translatable, TranslatablePlural):
                if translatable.count == 1:
                    format = translatable.singular
                else:
                    format = translatable.plural
            else:
                raise NotImplementedError('Cannot translate translatable %r' %
                    translatable)
        else:
            # translation object found, translate
            if isinstance(translatable, TranslatableSingular):
                format = t.ugettext(translatable.format)
            elif isinstance(translatable, TranslatablePlural):
                format = t.ungettext(translatable.singular, translatable.plural,
                    translatable.count)
            else:
                raise NotImplementedError('Cannot translate translatable %r' %
                    translatable)

        if translatable.args:
            output = format % translatable.args
        else:
            output = format

        self.debug("Translated %r to %r", translatable.format, output)
        return output
    
    def translate(self, message, lang=None):
        """
        Translate a message, in the given language.
        """
        strings = []
        for t in message.translatables:
            strings.append(self.translateTranslatable(t, lang))
        return "".join(strings)

# NOTE: same caveats apply for FancyEqMixin as above
# this might be a little heavy; we could consider only comparing
# on id, once we verify that all id's are unique

class Message(pb.Copyable, pb.RemoteCopy, util.FancyEqMixin):
    """
    I am a message to be shown in a UI.
    """

    compareAttributes = ["level", "translatables", "debug", "id", "priority",
        "timestamp"]

    def __init__(self, level, translatable, debug=None, id=None, priority=50,
        timestamp=None):
        """
        @param level:        ERROR, WARNING or INFO
        @param translatable: a translatable possibly with markup for
                             linking to documentation or running commands.
        @param debug:        further, untranslated, debug information, not
                             always shown
        @param priority:     priority compared to other messages of the same
                             level
        @param timestamp:    time since epoch at which the message was
                             generated, in seconds.
        """
        self.level = level
        self.translatables = []
        self.debug = debug
        self.id = id
        self.priority = priority
        self.timestamp = timestamp or time.time() 

        self.add(translatable)

    def __repr__(self):
        return '<Message %r at %r>' % (self.id, id(self))

    def add(self, translatable):
        if not isinstance(translatable, Translatable):
            raise ValueError('%r is not Translatable' % translatable)
        self.translatables.append(translatable)
pb.setUnjellyableForClass(Message, Message)

# these are implemented as factory functions instead of classes because
# properly proxying to the correct subclass is hard with Copyable/RemoteCopy
def Error(*args, **kwargs):
    """
    Create a L{Message} at ERROR level, indicating a failure that needs
    intervention to be resolved.
    """
    return Message(ERROR, *args, **kwargs)

def Warning(*args, **kwargs):
    """
    Create a L{Message} at WARNING level, indicating a potential problem.
    """
    return Message(WARNING, *args, **kwargs)

def Info(*args, **kwargs):
    """
    Create a L{Message} at INFO level.
    """
    return Message(INFO, *args, **kwargs)

class Result(pb.Copyable, pb.RemoteCopy):
    """
    I am used in worker checks to return a result.

    @ivar value:    the result value of the check
    @ivar failed:   whether or not the check failed.  Typically triggered
                    by adding an ERROR message to the result.
    @ivar messages: list of messages
    @type messages: list of L{Message}
    """
    def __init__(self):
        self.messages = []
        self.value = None
        self.failed = False

    def succeed(self, value):
        """
        Make the result be successful, setting the given result value.
        """
        self.value = value

    def add(self, message):
        """
        Add a message to the result.

        @type message: L{Message}
        """
        self.messages.append(message)
        if message.level == ERROR:
            self.failed = True
            self.value = None
pb.setUnjellyableForClass(Result, Result)

