# -*- coding: utf-8 -*-

#  Copyright © 2011-2014  B. Clausius <barcc@gmx.de>
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU 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 General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import print_function, division

import sys, os
import io
if sys.version_info[0] < 3:
    import cPickle as pickle
else:
    import pickle
import subprocess

import dbus.service
import dbus.mainloop.glib
from gi.repository import GLib, GObject

# Also defined in worker/run.py
DBUS_NAME = 'net.launchpad.bzr_panel'
DBUS_PATH = '/net/launchpad/bzr_panel'
AUTHKEY = b'Wrtlprmft'


class DBusService(dbus.service.Object):
    '''D-Bus interface for the plugin'''
    
    def __init__(self, worker):
        dbus.service.Object.__init__(self, dbus.SessionBus(), DBUS_PATH)
        self.worker = worker
        
    @dbus.service.method(DBUS_NAME, in_signature='b', out_signature=None)
    def BusyChanged(self, var):
        GLib.idle_add(self.worker._on_report_busy, bool(var))
        
    @dbus.service.method(DBUS_NAME, in_signature='tsay', out_signature=None, byte_arrays=True)
    def Reply(self, sender_id, command, result):
        result = pickle.loads(bytes(result))
        GLib.idle_add(self.worker._on_reply, int(sender_id), str(command), result)
        
    @dbus.service.method(DBUS_NAME, in_signature='ss', out_signature=None)
    def Error(self, error_type, error_message):
        GLib.idle_add(self.worker._on_error, error_type, error_message)
        
        
class TaskManager (GObject.Object):
    '''This class is responsible for starting and stopping the worker process,
    feed the worker and retrieve the results.
    '''
    __gsignals__ = {
        'report-busy': (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
    }
    _instance = None # the one and only instance
    worker_file = os.path.join(os.path.dirname(__file__), 'worker', 'run.py')
    
    def __init__(self):
        GObject.Object.__init__(self)
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        self.dbus_service = DBusService(self)
        
        self.process = None
        self.processes = {}
        self.callbacks = {}
        self.error_func = None
        self.busy = 0
        
    def _start(self):
        from multiprocessing.connection import Listener
        dbus_unique_name = self.dbus_service.connection.get_unique_name()
        listener = Listener(family='AF_UNIX', authkey=AUTHKEY)
        command = ['python', self.worker_file, 'worker_loop', dbus_unique_name, listener.address]
        self.process = subprocess.Popen(command)
        self.connection = listener.accept()
        self.processes[self.process.pid] = self.process
        GLib.child_watch_add(self.process.pid, self._on_worker_end)
        
    def _stop(self):
        self.callbacks.clear()
        if self.connection:
            self._send((0, 'stop', b''))
            self.connection.close()
            self.connection = None
        if self.busy:
            self.emit('report-busy', False)
            self.busy = 0
        self.error_func = None
        self.process = None
        
    @classmethod
    def stop(cls):
        cls._instance and cls._instance._stop()
        #cls._instance = None
        
    def _on_report_busy(self, busy):
        self.busy += 1 if busy else -1
        if self.busy <= 0:
            self.emit('report-busy', False)
            self.busy = 0
        else:
            self.emit('report-busy', True)
        
    def _on_reply(self, sender_id, command, args):
        try:
            sender = self.callbacks[sender_id]
        except KeyError:
            # ignore result for disconnected senders
            return
        try:
            getattr(sender, command)(*args)
        except:
            sys.excepthook(*sys.exc_info())
            
    # simulate connection.send, but with protocol 2 (even for Python 3)
    def _send(self, value):
        buf = pickle.dumps(value, protocol=2)
        self.connection.send_bytes(buf)
            
    def _on_error(self, error_type, error_message):
        self.error_func and self.error_func(error_type, error_message)
        
    def _task(self, callback, worker_func_name, *args):
        if self.process is None:
            self._start()
        callback_id = id(callback)
        self.callbacks[callback_id] = callback
        self._send((callback_id, worker_func_name, args))
            
    @classmethod
    def task(cls, callback, worker_func_name, *args):
        if cls._instance is None:
            cls._instance = cls()
        cls._instance._task(callback, worker_func_name, *args)
        
    @classmethod
    def connect_busy_changed(cls, callback):
        if cls._instance is None:
            cls._instance = cls()
        cls._instance.connect('report-busy', callback)
        
    @classmethod
    def set_error_func(cls, callback):
        if cls._instance is None:
            cls._instance = cls()
        cls._instance.error_func = callback
        
    def _disconnect(self, callback):
        del self.callbacks[id(callback)]
        
    @classmethod
    def disconnect(cls, callback):
        cls._instance and cls._instance._disconnect(callback)
        
    def _on_worker_end(self, pid, condition):
        process = self.processes[pid]
        self.callbacks.clear()
        if self.connection:
            self.connection.close()
            self.connection = None
        process.wait()
        del self.processes[pid]
        if process == self.process:
            self.process = None
            self.emit('report-busy', False)
            self.busy = 0
        

