# Copyright (C) 2008-2009 LottaNZB Development Team
# 
# 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; version 3.
# 
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 
import gobject
import threading
import types
import gettext
import gtk
import os

from logging import getLogger
log = getLogger(__name__)

from kiwi.model import Model
from kiwi.utils import gsignal
from kiwi.ui import views
from kiwi.environ import environ

from copy import deepcopy
from os import unlink
from os.path import isfile
from distutils.spawn import find_executable
from socket import gethostname

from gobject import GObject as OriginalGObject
from gobject import property as gproperty
from gobject import list_properties, GObjectMeta

from lottanzb.external.builderloader import BuilderWidgetTree

_ = lambda message: gettext.dgettext("lottanzb", message)

# Initializes the use of Python threading in the gobject module
gobject.threads_init()

class GObjectSingletonMeta(GObjectMeta):
    """GObject singleton metaclass"""
    
    def __init__(mcs, name, bases, _dict):
        GObjectMeta.__init__(mcs, name, bases, _dict)
        mcs.__instance = None
    
    def __call__(mcs, *args, **kwargs):
        """Creates an instance of mcs if it doesn't exist yet"""
        
        if mcs.__instance is None:
            mcs.__instance = GObjectMeta.__call__(mcs, *args, **kwargs)
        
        return mcs.__instance

class GObject(OriginalGObject, Model):
    """GObject class which is better at converting values than the original.
    
    When setting the value of a property, this class tries to convert the value
    if necessary to prevent GObject from raising a TypeError. For example,
    "True" is converted to True if the property should be a boolean.
    
    It also implements the model part of Kiwi's observer pattern, which is used
    to keep the data and the UI in sync.
    """
    
    def __init__(self, *args, **kwargs):
        OriginalGObject.__init__(self, *args, **kwargs)
        Model.__init__(self)
    
    def set_property(self, key, value):
        """The class documentation says it all"""
        
        value_type = self.get_property_type(key)
        
        if value_type is types.IntType:
            if value is None:
                value = 0
            else:
                value = int(value)
        
        elif value_type is types.BooleanType:
            if value == "False":
                value = False
            elif value == "True":
                value = True
            else:
                value = bool(value)
        
        elif value_type is types.FloatType:
            if value is None:
                value = 0
            else:
                value = float(value)
        
        elif value_type is types.StringType:
            if value is None:
                value = ""
            else:
                value = str(value)
        
        old_value = self.get_property(key)
        
        if old_value != value:
            OriginalGObject.set_property(self, key, value)
            
            # Notify registered proxies that the model has changed.
            if self._v_autonotify:
                self.notify_proxies(key)
    
    def keys(self):
        return [prop.name.replace("-", "_") for prop in list_properties(self)]
    
    def __getitem__(self, key):
        """So that we can use brackets to access the properties"""
        
        return self.get_property(key)
    
    def __setitem__(self, key, value):
        """So that we can use brackets to access the properties"""
        
        self.set_property(key, value)
    
    def __iter__(self):
        """Does the same as the one of dictionaries"""
        
        for key in self.keys():
            yield key
    
    def update(self, properties):
        for key in properties:
            self[key] = properties[key]
    
    def deep_copy(self):
        return deepcopy(self)
    
    def __deepcopy__(self, memo):
        args = deepcopy(getattr(self, "__getinitargs__", lambda: [])(), memo)
        result = self.__class__(*args)
        
        for key in self.__dict__:
            try:
                result.__dict__[key] = deepcopy(self.__dict__[key], memo)
            except TypeError:
                # Python won't be able to copy the proxy objects in _v_proxies
                # and _v_blockedproxies.
                if not key.startswith("_"):
                    raise
        
        for key in self.keys():
            result.set_property(key, deepcopy(self.get_property(key), memo))
        
        return result
    
    def connect_async(self, event, handler, *args):
        def new_handler(*new_args):
            gobject.idle_add(handler, *new_args)
        
        return self.connect(event, new_handler, *args)
    
    @classmethod
    def get_property_type(cls, key):
        """Returns the Python type of a certain object property"""
        
        option_type = getattr(cls, key).type
        
        if option_type in (gobject.TYPE_INT, gobject.TYPE_UINT,
            gobject.TYPE_INT64, gobject.TYPE_UINT64):
            return types.IntType
        elif option_type is gobject.TYPE_BOOLEAN:
            return types.BooleanType
        elif option_type in (gobject.TYPE_DOUBLE, gobject.TYPE_FLOAT):
            return types.FloatType
        elif option_type is gobject.TYPE_STRING:
            return types.StringType
        else:
            return types.ObjectType
    
    @classmethod
    def get_property_default_value(cls, key):
        """Returns the default value of a certain object property"""
        
        return getattr(cls, key).default

class Thread(threading.Thread, GObject):
    """Cancellable thread which uses gobject signals to return information to
    the GUI.
    """
    
    gsignal("completed")
    gsignal("success")
    gsignal("failure", object)
    
    gsignal("progress", float)
    
    def __init__(self):
        threading.Thread.__init__(self)
        GObject.__init__(self)
        
        # As we can't handle exceptions directly within the thread, they are
        # stored in this property.
        self.exception = None
        
        self.thread_stopped = threading.Event()
        self.connect("completed", self.on_thread_completed)
    
    def on_thread_completed(self, thread):
        if thread.exception:
            self.emit("failure", thread.exception)
        else:
            self.emit("success")
    
    def stop(self):
        """Threads in Python are not cancellable, so we implement our own
        cancellation logic.
        """
        
        self.thread_stopped.set()
    
    def run(self):
        """Method representing the thread's activity. Must be defined by
        subclasses.
        
        Example code:
        while not self.threadStopped.isSet():
            ...
            self.emit("progress", x)
        self.emit("completed")
        """
        
        raise NotImplementedError

def _open_glade(view, glade_file, domain):
    if not glade_file:
        raise ValueError("A glade file wasn't provided.")
    elif not isinstance(glade_file, basestring):
        raise TypeError("Glade file should be a string, found %s." % \
            type(glade_file))
    
    file_name = os.path.splitext(os.path.basename(glade_file))[0]
    glade_file = environ.find_resource("glade", file_name)
    
    return BuilderWidgetTree(view, glade_file, "lottanzb")

views._open_glade = _open_glade

class _TimerBase(GObject):
    """
    Helper class for Timer.
    """
    
    stopped = gproperty(type=bool, default=True)
    
    gsignal("tick")
    
    def __init__(self, timeout):
        GObject.__init__(self)
        
        self.timeout = timeout
    
    def stop(self):
        """
        Stop the emission of the 'tick' signal.
        """
        
        if not self.stopped:
            self.stopped = True
    
    def start(self):
        """
        Start the regulary emission of the 'tick' signal.
        
        The Times class ensures thanks to the _TimerSequence helper class,
        that it will exactly take the specific amount of time after this
        method is invoked until the 'tick' signal is emitted.
        """
        
        if self.stopped:
            self.stopped = False

class _TimerSequence(_TimerBase):
    """
    Helper class for Timer.
    """
    
    def start(self):
        """
        This is where the gobject.timeout_add is actually called.
        """
        
        _TimerBase.start(self)
        
        gobject.timeout_add(self.timeout, self.on_gobject_timeout)
    
    def on_gobject_timeout(self):
        if self.stopped:
            return False
        
        self.emit("tick")
        
        return True

class Timer(_TimerBase):
    """
    Wrapper for gobject.timeout_add, which is more object-oriented and
    which provides the two convenient methods 'start' and 'stop'.
    
    The timeout is the number of milliseconds between two emissions of the
    'tick' signal. Optionally, it's possible to directly specify a handler
    like using gobject.timeout_add.
    """
    
    def __init__(self, timeout, handler=None, *args):
        self._sequences = []
        
        _TimerBase.__init__(self, timeout)
        
        if callable(handler):
            self.connect("tick", handler, *args)
        
        self.connect("notify::stopped", self.on_state_changed)
    
    def on_state_changed(self, *args):        
        for sequence in self._sequences:
            sequence.stop()
        
        if not self.stopped:
            sequence = _TimerSequence(self.timeout)
            sequence.connect("tick", self.on_sequence_tick)
            sequence.start()
            
            self._sequences.append(sequence)
    
    def on_sequence_tick(self, *args):
        self.emit("tick")

# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/498171
class FileLock:
    """
    Class to handle creating and removing (pid) lockfiles
    """
    
    # Custom exceptions
    class LockError(Exception): pass
    class AlreadyLockedError(LockError): pass
    class LockFailed(LockError): pass
    
    class ReleaseError(Exception): pass
    class NotLockedError(ReleaseError): pass
    class NotOwnLockError(ReleaseError): pass
    
    # Convenience callables for formatting
    addr = lambda self: "%d@%s" % (self.pid, self.host)
    fddr = lambda self: "<%s %s>" % (self.path, self.addr())
    pddr = lambda self, lock: "<%s %s@%s>" % \
        (self.path, lock["pid"], lock["host"])
    
    def __init__(self, path):
        self.pid = os.getpid()
        self.host = gethostname()
        self.path = path
    
    def acquire(self):
        """Acquire a lock, returning self if successful, False otherwise"""
        
        if self.is_locked():
            lock = self.pddr(self._readlock())
            log.debug("Previous lock detected: %s", lock)
            
            raise self.AlreadyLockedError
        else:
            try:
                fh = open(self.path, "w")
                fh.write(self.addr())
                fh.close()
                
                log.debug("Acquired lock: %s", self.fddr())
            except:
                if isfile(self.path):
                    try:
                        unlink(self.path)
                    except:
                        pass
                
                raise self.LockFailed("Error acquiring lock: %s" % self.fddr())
    
    def release(self):
        """Release lock, returning self"""
        
        if self.is_locked():
            if self.own_lock():
                try:
                    unlink(self.path)
                    
                    log.debug("Released lock: %s", self.fddr())
                except Exception, e:
                    raise self.ReleaseError("Error releasing lock: %s" % \
                        self.fddr())
            else:
                raise self.NotOwnLockError
        else:
            raise self.NotLockedError
    
    def _readlock(self):
        """Internal method to read lock info"""
        
        try:
            lock = {}
            fh = open(self.path)
            data = fh.read().rstrip().split("@")
            fh.close()
            lock["pid"], lock["host"] = data
            
            return lock
        except:
            return { "pid": 8**10, "host": "" }
    
    def is_locked(self):
        """Check if we already have a lock"""
        
        try:
            lock = self._readlock()
            
            if hasattr(os, "kill"):
                os.kill(int(lock["pid"]), 0)
            else:
                # os.kill is only available on Unix systems.
                
                try:
                    import win32api
                except:
                    pass
                else:
                    assert win32api.OpenProcess(1, 0, int(lock["pid"]))
            
            return (lock["host"] == self.host)
        except:
            return False
    
    def own_lock(self):
        """Check if we own the lock"""
        
        lock = self._readlock()
        return (self.fddr() == self.pddr(lock))

def get_hellanzb_cmd():
    return \
        find_executable("hellanzb") or \
        find_executable("hellanzb.py")

def has_ssl_support():
    """
    Check whether OpenSSL is supported by the host machine. E. g. if the
    `OpenSSL` module is available.
    
    `util` is definitely not the perfect place for this function, but I didn't
    come up with a better idea.
    """
    
    try:
        import OpenSSL
    except ImportError:
        return False
    else:
        return True
