import datetime
import babel
import dateutil.relativedelta
import logging
import time
import traceback
import sys
import xmlrpclib

import openerp

_logger = logging.getLogger(__name__)

#----------------------------------------------------------
# OpenERPSession RPC openerp backend access
#----------------------------------------------------------
class AuthenticationError(Exception):
    pass

class SessionExpiredException(Exception):
    pass

class Service(object):
    def __init__(self, session, service_name):
        self.session = session
        self.service_name = service_name

    def __getattr__(self, method):
        def proxy_method(*args):
            result = self.session.send(self.service_name, method, *args)
            return result
        return proxy_method

class Model(object):
    def __init__(self, session, model):
        self.session = session
        self.model = model
        self.proxy = self.session.proxy('object')

    def __getattr__(self, method):
        def proxy(*args, **kw):
            result = self.proxy.execute_kw(self.session._db, self.session._uid, self.session._password, self.model, method, args, kw)
            # reorder read
            if method == "read":
                if isinstance(result, list) and len(result) > 0 and "id" in result[0]:
                    index = {}
                    for r in result:
                        index[r['id']] = r
                    result = [index[x] for x in args[0] if x in index]
            return result
        return proxy

    def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None, context=None):
        record_ids = self.search(domain or [], offset, limit or False, order or False, context or {})
        if not record_ids: return []
        records = self.read(record_ids, fields or [], context or {})
        return records

class OpenERPSession(object):
    """
    An OpenERP RPC session, a given user can own multiple such sessions
    in a web session.

    .. attribute:: context

        The session context, a ``dict``. Can be reloaded by calling
        :meth:`openerpweb.openerpweb.OpenERPSession.get_context`

    .. attribute:: domains_store

        A ``dict`` matching domain keys to evaluable (but non-literal) domains.

        Used to store references to non-literal domains which need to be
        round-tripped to the client browser.
    """
    def __init__(self):
        self._creation_time = time.time()
        self._db = False
        self._uid = False
        self._login = False
        self._password = False
        self._suicide = False
        self.context = {}
        self.jsonp_requests = {}     # FIXME use a LRU

    def send(self, service_name, method, *args):
        code_string = u"warning -- %s\n\n%s"
        try:
            return openerp.netsvc.dispatch_rpc(service_name, method, args)
        except openerp.osv.osv.except_osv, e:
            raise xmlrpclib.Fault(code_string % (e.name, e.value), '')
        except openerp.exceptions.Warning, e:
            raise xmlrpclib.Fault(code_string % ("Warning", e), '')
        except openerp.exceptions.AccessError, e:
            raise xmlrpclib.Fault(code_string % ("AccessError", e), '')
        except openerp.exceptions.AccessDenied, e:
            raise xmlrpclib.Fault('AccessDenied', openerp.tools.ustr(e))
        except openerp.exceptions.DeferredException, e:
            formatted_info = "".join(traceback.format_exception(*e.traceback))
            raise xmlrpclib.Fault(openerp.tools.ustr(e), formatted_info)
        except Exception, e:
            formatted_info = "".join(traceback.format_exception(*(sys.exc_info())))
            raise xmlrpclib.Fault(openerp.tools.ustr(e), formatted_info)

    def proxy(self, service):
        return Service(self, service)

    def bind(self, db, uid, login, password):
        self._db = db
        self._uid = uid
        self._login = login
        self._password = password

    def authenticate(self, db, login, password, env=None):
        uid = self.proxy('common').authenticate(db, login, password, env)
        self.bind(db, uid, login, password)

        if uid: self.get_context()
        return uid

    def assert_valid(self, force=False):
        """
        Ensures this session is valid (logged into the openerp server)
        """
        if self._uid and not force:
            return
        # TODO use authenticate instead of login
        uid = self.proxy("common").login(self._db, self._login, self._password)
        if not uid:
            raise AuthenticationError("Authentication failure")

    def ensure_valid(self):
        if self._uid:
            try:
                self.assert_valid(True)
            except Exception:
                self._uid = None

    def execute(self, model, func, *l, **d):
        self.assert_valid()
        model = self.model(model)
        r = getattr(model, func)(*l, **d)
        return r

    def exec_workflow(self, model, id, signal):
        self.assert_valid()
        r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id)
        return r

    def model(self, model):
        """ Get an RPC proxy for the object ``model``, bound to this session.

        :param model: an OpenERP model name
        :type model: str
        :rtype: a model object
        """
        if self._db == False:
            raise SessionExpiredException("Session expired")

        return Model(self, model)

    def get_context(self):
        """ Re-initializes the current user's session context (based on
        his preferences) by calling res.users.get_context() with the old
        context

        :returns: the new context
        """
        assert self._uid, "The user needs to be logged-in to initialize his context"
        self.context = self.model('res.users').context_get() or {}
        self.context['uid'] = self._uid
        self._fix_lang(self.context)
        return self.context

    def _fix_lang(self, context):
        """ OpenERP provides languages which may not make sense and/or may not
        be understood by the web client's libraries.

        Fix those here.

        :param dict context: context to fix
        """
        lang = context['lang']

        # inane OpenERP locale
        if lang == 'ar_AR':
            lang = 'ar'

        # lang to lang_REGION (datejs only handles lang_REGION, no bare langs)
        if lang in babel.core.LOCALE_ALIASES:
            lang = babel.core.LOCALE_ALIASES[lang]

        context['lang'] = lang or 'en_US'

# vim:et:ts=4:sw=4:
