# -*- coding: utf-8 -*-
##############################################################################
#
#    OpenERP, Open Source Management Solution
#    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import datetime
import json
import logging
import select
import time

import openerp
import openerp.tools.config
import openerp.modules.registry
from openerp import http
from openerp.http import request
from openerp.osv import osv, fields, expression
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT

_logger = logging.getLogger(__name__)

def listen_channel(cr, channel_name, handle_message, check_stop=(lambda: False), check_stop_timer=60.):
    """
        Begin a loop, listening on a PostgreSQL channel. This method does never terminate by default, you need to provide a check_stop
        callback to do so. This method also assume that all notifications will include a message formated using JSON (see the
        corresponding notify_channel() method).

        :param db_name: database name
        :param channel_name: the name of the PostgreSQL channel to listen
        :param handle_message: function that will be called when a message is received. It takes one argument, the message
            attached to the notification.
        :type handle_message: function (one argument)
        :param check_stop: function that will be called periodically (see the check_stop_timer argument). If it returns True
            this function will stop to watch the channel.
        :type check_stop: function (no arguments)
        :param check_stop_timer: The maximum amount of time between calls to check_stop_timer (can be shorter if messages
            are received).
    """
    try:
        conn = cr._cnx
        cr.execute("listen " + channel_name + ";")
        cr.commit();
        stopping = False
        while not stopping:
            if check_stop():
                stopping = True
                break
            if select.select([conn], [], [], check_stop_timer) == ([],[],[]):
                pass
            else:
                conn.poll()
                while conn.notifies:
                    message = json.loads(conn.notifies.pop().payload)
                    handle_message(message)
    finally:
        try:
            cr.execute("unlisten " + channel_name + ";")
            cr.commit()
        except:
            pass # can't do anything if that fails

def notify_channel(cr, channel_name, message):
    """
        Send a message through a PostgreSQL channel. The message will be formatted using JSON. This method will
        commit the given transaction because the notify command in Postgresql seems to work correctly when executed in
        a separate transaction (despite what is written in the documentation).

        :param cr: The cursor.
        :param channel_name: The name of the PostgreSQL channel.
        :param message: The message, must be JSON-compatible data.
    """
    cr.commit()
    cr.execute("notify " + channel_name + ", %s", [json.dumps(message)])
    cr.commit()

POLL_TIMER = 30
DISCONNECTION_TIMER = POLL_TIMER + 5
WATCHER_ERROR_DELAY = 10

class LongPollingController(http.Controller):

    @http.route('/longpolling/im/poll', type="json", auth="none")
    def poll(self, last=None, users_watch=None, db=None, uid=None, password=None, uuid=None):
        assert_uuid(uuid)
        if not openerp.evented:
            raise Exception("Not usable in a server not running gevent")
        from openerp.addons.im.watcher import ImWatcher
        if db is not None:
            openerp.service.security.check(db, uid, password)
        else:
            uid = request.session.uid
            db = request.session.db

        registry = openerp.modules.registry.RegistryManager.get(db)
        with registry.cursor() as cr:
            registry.get('im.user').im_connect(cr, uid, uuid=uuid, context=request.context)
            my_id = registry.get('im.user').get_my_id(cr, uid, uuid, request.context)
        num = 0
        while True:
            with registry.cursor() as cr:
                res = registry.get('im.message').get_messages(cr, uid, last, users_watch, uuid=uuid, context=request.context)
            if num >= 1 or len(res["res"]) > 0:
                return res
            last = res["last"]
            num += 1
            ImWatcher.get_watcher(res["dbname"]).stop(my_id, users_watch or [], POLL_TIMER)

    @http.route('/longpolling/im/activated', type="json", auth="none")
    def activated(self):
        return not not openerp.evented

    @http.route('/longpolling/im/gen_uuid', type="json", auth="none")
    def gen_uuid(self):
        import uuid
        return "%s" % uuid.uuid1()

def assert_uuid(uuid):
    if not isinstance(uuid, (str, unicode, type(None))) and uuid != False:
        raise Exception("%s is not a uuid" % uuid)


class im_message(osv.osv):
    _name = 'im.message'

    _order = "date desc"

    _columns = {
        'message': fields.text(string="Message", required=True),
        'from_id': fields.many2one("im.user", "From", required= True, ondelete='cascade'),
        'session_id': fields.many2one("im.session", "Session", required=True, select=True, ondelete='cascade'),
        'to_id': fields.many2many("im.user", "im_message_users", 'message_id', 'user_id', 'To'),
        'date': fields.datetime("Date", required=True, select=True),
        'technical': fields.boolean("Technical Message"),
    }

    _defaults = {
        'date': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
        'technical': False,
    }
    
    def get_messages(self, cr, uid, last=None, users_watch=None, uuid=None, context=None):
        assert_uuid(uuid)
        users_watch = users_watch or []

        # complex stuff to determine the last message to show
        users = self.pool.get("im.user")
        my_id = users.get_my_id(cr, uid, uuid, context=context)
        c_user = users.browse(cr, openerp.SUPERUSER_ID, my_id, context=context)
        if last:
            if c_user.im_last_received < last:
                users.write(cr, openerp.SUPERUSER_ID, my_id, {'im_last_received': last}, context=context)
        else:
            last = c_user.im_last_received or -1

        # how fun it is to always need to reorder results from read
        mess_ids = self.search(cr, openerp.SUPERUSER_ID, ["&", ['id', '>', last], "|", ['from_id', '=', my_id], ['to_id', 'in', [my_id]]], order="id", context=context)
        mess = self.read(cr, openerp.SUPERUSER_ID, mess_ids, ["id", "message", "from_id", "session_id", "date", "technical"], context=context)
        index = {}
        for i in xrange(len(mess)):
            index[mess[i]["id"]] = mess[i]
        mess = []
        for i in mess_ids:
            mess.append(index[i])

        if len(mess) > 0:
            last = mess[-1]["id"]
        users_status = users.read(cr, openerp.SUPERUSER_ID, users_watch, ["im_status"], context=context)
        return {"res": mess, "last": last, "dbname": cr.dbname, "users_status": users_status}

    def post(self, cr, uid, message, to_session_id, technical=False, uuid=None, context=None):
        assert_uuid(uuid)
        my_id = self.pool.get('im.user').get_my_id(cr, uid, uuid)
        session_user_ids = self.pool.get('im.session').get_session_users(cr, uid, to_session_id, context=context).get("user_ids", [])
        to_ids = [user_id for user_id in session_user_ids if user_id != my_id]
        self.create(cr, openerp.SUPERUSER_ID, {"message": message, 'from_id': my_id,
            'to_id': [(6, 0, to_ids)], 'session_id': to_session_id, 'technical': technical}, context=context)
        notify_channel(cr, "im_channel", {'type': 'message', 'receivers': [my_id] + to_ids})
        return False

class im_session(osv.osv):
    _name = 'im.session'

    def _calc_name(self, cr, uid, ids, something, something_else, context=None):
        res = {}
        for obj in self.browse(cr, uid, ids, context=context):
            res[obj.id] = ", ".join([x.name for x in obj.user_ids])
        return res

    _columns = {
        'user_ids': fields.many2many('im.user', 'im_session_im_user_rel', 'im_session_id', 'im_user_id', 'Users'),
        "name": fields.function(_calc_name, string="Name", type='char'),
    }

    # Todo: reuse existing sessions if possible
    def session_get(self, cr, uid, users_to, uuid=None, context=None):
        my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
        users = [my_id] + users_to
        domain = []
        for user_to in users:
            domain.append(('user_ids', 'in', [user_to]))
        sids = self.search(cr, openerp.SUPERUSER_ID, domain, context=context, limit=1)
        session_id = None
        for session in self.browse(cr, uid, sids, context=context):
            if len(session.user_ids) == len(users):
                session_id = session.id
                break
        if not session_id:
            session_id = self.create(cr, openerp.SUPERUSER_ID, {
                'user_ids': [(6, 0, users)]
            }, context=context)
        return self.read(cr, uid, session_id, context=context)

    def get_session_users(self, cr, uid, session_id, context=None):
        return self.read(cr, openerp.SUPERUSER_ID, session_id, ['user_ids'], context=context)

    def add_to_session(self, cr, uid, session_id, user_id, uuid=None, context=None):
        my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
        session = self.read(cr, uid, session_id, context=context)
        if my_id not in session.get("user_ids"):
            raise Exception("Not allowed to modify a session when you are not in it.")
        self.write(cr, uid, session_id, {"user_ids": [(4, user_id)]}, context=context)

    def remove_me_from_session(self, cr, uid, session_id, uuid=None, context=None):
        my_id = self.pool.get("im.user").get_my_id(cr, uid, uuid, context=context)
        self.write(cr, openerp.SUPERUSER_ID, session_id, {"user_ids": [(3, my_id)]}, context=context)

class im_user(osv.osv):
    _name = "im.user"

    def _im_status(self, cr, uid, ids, something, something_else, context=None):
        res = {}
        current = datetime.datetime.now()
        delta = datetime.timedelta(0, DISCONNECTION_TIMER)
        data = self.read(cr, openerp.SUPERUSER_ID, ids, ["im_last_status_update", "im_last_status"], context=context)
        for obj in data:
            last_update = datetime.datetime.strptime(obj["im_last_status_update"], DEFAULT_SERVER_DATETIME_FORMAT)
            res[obj["id"]] = obj["im_last_status"] and (last_update + delta) > current
        return res

    def _status_search(self, cr, uid, obj, name, domain, context=None):
        current = datetime.datetime.now()
        delta = datetime.timedelta(0, DISCONNECTION_TIMER)
        field, operator, value = domain[0]
        if operator in expression.NEGATIVE_TERM_OPERATORS:
            value = not value
        if value:
            return ['&', ('im_last_status', '=', True), ('im_last_status_update', '>', (current - delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
        else:
            return ['|', ('im_last_status', '=', False), ('im_last_status_update', '<=', (current - delta).strftime(DEFAULT_SERVER_DATETIME_FORMAT))]
    # TODO: Remove fields arg in trunk. Also in im.js.
    def search_users(self, cr, uid, text_search, fields, limit, context=None):
        my_id = self.get_my_id(cr, uid, None, context)
        group_employee = self.pool['ir.model.data'].get_object_reference(cr, uid, 'base', 'group_user')[1]
        found = self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", True], ["user_id.groups_id", "in", [group_employee]]],
            order="name asc", limit=limit, context=context)
        if len(found) < limit:
            found += self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", True], ["id", "not in", found]],
                order="name asc", limit=limit, context=context)
        if len(found) < limit:
            found += self.search(cr, uid, [["name", "ilike", text_search], ["id", "<>", my_id], ["uuid", "=", False], ["im_status", "=", False], ["id", "not in", found]],
                order="name asc", limit=limit-len(found), context=context)
        users = self.read(cr,openerp.SUPERUSER_ID, found, ["name", "user_id", "uuid", "im_status"], context=context)
        users.sort(key=lambda obj: found.index(obj['id']))
        return users

    def im_connect(self, cr, uid, uuid=None, context=None):
        assert_uuid(uuid)
        return self._im_change_status(cr, uid, True, uuid, context)

    def im_disconnect(self, cr, uid, uuid=None, context=None):
        assert_uuid(uuid)
        return self._im_change_status(cr, uid, False, uuid, context)

    def _im_change_status(self, cr, uid, new_one, uuid=None, context=None):
        assert_uuid(uuid)
        id = self.get_my_id(cr, uid, uuid, context=context)
        current_status = self.read(cr, openerp.SUPERUSER_ID, id, ["im_status"], context=None)["im_status"]
        self.write(cr, openerp.SUPERUSER_ID, id, {"im_last_status": new_one, 
            "im_last_status_update": datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
        if current_status != new_one:
            notify_channel(cr, "im_channel", {'type': 'status', 'user': id})
        return True

    def get_my_id(self, cr, uid, uuid=None, context=None):
        assert_uuid(uuid)
        if uuid:
            users = self.search(cr, openerp.SUPERUSER_ID, [["uuid", "=", uuid]], context=None)
        else:
            users = self.search(cr, openerp.SUPERUSER_ID, [["user_id", "=", uid]], context=None)
        my_id = users[0] if len(users) >= 1 else False
        if not my_id:
            my_id = self.create(cr, openerp.SUPERUSER_ID, {"user_id": uid if not uuid else False, "uuid": uuid if uuid else False}, context=context)
        return my_id

    def assign_name(self, cr, uid, uuid, name, context=None):
        assert_uuid(uuid)
        id = self.get_my_id(cr, uid, uuid, context=context)
        self.write(cr, openerp.SUPERUSER_ID, id, {"assigned_name": name}, context=context)
        return True

    def _get_name(self, cr, uid, ids, name, arg, context=None):
        res = {}
        for record in self.browse(cr, uid, ids, context=context):
            res[record.id] = record.assigned_name
            if record.user_id:
                res[record.id] = record.user_id.name
                continue
        return res

    def get_users(self, cr, uid, ids, context=None):
        return self.read(cr,openerp.SUPERUSER_ID, ids, ["name", "im_status", "uuid"], context=context)

    _columns = {
        'name': fields.function(_get_name, type='char', size=200, string="Name", store=True, readonly=True),
        'assigned_name': fields.char(string="Assigned Name", size=200, required=False),
        'image': fields.related('user_id', 'image_small', type='binary', string="Image", readonly=True),
        'user_id': fields.many2one("res.users", string="User", select=True, ondelete='cascade', oldname='user'),
        'uuid': fields.char(string="UUID", size=50, select=True),
        'im_last_received': fields.integer(string="Instant Messaging Last Received Message"),
        'im_last_status': fields.boolean(strint="Instant Messaging Last Status"),
        'im_last_status_update': fields.datetime(string="Instant Messaging Last Status Update"),
        'im_status': fields.function(_im_status, string="Instant Messaging Status", type='boolean', fnct_search=_status_search),
    }

    _defaults = {
        'im_last_received': -1,
        'im_last_status': False,
        'im_last_status_update': lambda *args: datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
    }

    _sql_constraints = [
        ('user_uniq', 'unique (user_id)', 'Only one chat user per OpenERP user.'),
        ('uuid_uniq', 'unique (uuid)', 'Chat identifier already used.'),
    ]

class res_users(osv.osv):
    _inherit = "res.users"

    def _get_im_user(self, cr, uid, ids, field_name, arg, context=None):
        result = dict.fromkeys(ids, False)
        for index, im_user in enumerate(self.pool['im.user'].search_read(cr, uid, domain=[('user_id', 'in', ids)], fields=['name', 'user_id'], context=context)):
            result[ids[index]] = im_user.get('user_id') and (im_user['user_id'][0], im_user['name']) or False
        return result

    _columns = {
        'im_user_id' : fields.function(_get_im_user, type='many2one', string="IM User", relation="im.user"),
    }
