# Copyright (C) 2008 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 kiwi.utils import gsignal
from kiwi.ui import libgladeloader
from kiwi.ui.objectlist import ObjectList
from kiwi.ui.widgets import checkbutton, combobox, entry, spinbutton, textview

from copy import deepcopy
from gtk.glade import XML
from os import unlink
from os.path import isfile
from distutils.spawn import find_executable

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

from socket import gethostname

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

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

class GObject(OriginalGObject):
    """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.
    """
    
    def set_property(self, key, value):
        """The class documentation says it all"""
        
        value_type = self.get_option_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)
        
        OriginalGObject.set_property(self, key, value)
    
    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 = []
        
        if hasattr(self, "__getinitargs__"):
            args = [deepcopy(arg, memo) for arg in self.__getinitargs__()]
        
        result = self.__class__(*args)
        result.__dict__.update(deepcopy(self.__dict__, memo))
        
        for key in self.keys():
            result.set_property(key, deepcopy(self.get_property(key), memo))
         
        return result
    
    @classmethod
    def get_option_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

class Thread(threading.Thread, GObject):
    """Cancellable thread which uses gobject signals to return information to
    the GUI.
    """
    
    gsignal("completed")
    gsignal("progress", float)
    
    def __init__(self):
        threading.Thread.__init__(self)
        GObject.__init__(self)
        
        self.thread_stopped = threading.Event()
    
    def stop(self):
        """Threads in Python are not cancellable, so we implement our own
        cancellation logic.
        """
        
        self.thread_stopped.set()
    
    def emit(self, *args, **kwargs):
        gobject.idle_add(GObject.emit, self, *args, **kwargs)
    
    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

class KiwifiedLibgladeWidgetTree(libgladeloader.LibgladeWidgetTree):
    # Backported workaround, so that gtk.Entries are properly displayed using
    # Kiwi 1.9.9 and PyGTK > 2.6
    # http://svn.async.com.br/cgi-bin/viewvc.cgi?view=rev&revision=4886
    class _ProxyEntry(entry.ProxyEntry):
        def do_size_allocate(self, *args):
            if not gtk.pygtk_version[:2] == (2, 6):
                gtk.Entry.do_size_allocate(self, *args)
            
            entry.ProxyEntry.do_size_allocate(self, *args)
    
    kiwi_widget_map = {
        gtk.CheckButton: checkbutton.ProxyCheckButton,
        gtk.ComboBox: combobox.ProxyComboBox,
        gtk.Entry: _ProxyEntry,
        gtk.SpinButton: spinbutton.ProxySpinButton,
        gtk.TextView: textview.ProxyTextView,
    }
    
    widget_data_types = {
        gtk.Entry: str,
        gtk.SpinButton: int,
        gtk.CheckButton: bool
    }
    
    def _attach_widgets(self):
        for widget_name in self._widgets:
            widget = XML.get_widget(self, widget_name)
            
            if widget:
                try:
                    if widget.__class__ is gtk.ScrolledWindow \
                        and widget_name.endswith("_list"):
                        kiwi_widget = ObjectList()
                        kiwi_widget.show()
                        treeview = kiwi_widget.get_treeview()
                        
                        self.copy_properties(widget.get_child(), treeview)
                    else:
                        assert widget.__class__ in self.kiwi_widget_map
                        kiwi_widget = self.kiwi_widget_map[widget.__class__]()
                    
                    self.copy_properties(widget, kiwi_widget)
                    
                    parent = widget.get_parent()
                    child_props = {}
                    
                    if isinstance(parent, gtk.Container):
                        child_props = self.get_child_properties(parent, widget)
                    
                    parent.remove(widget)
                    parent.add(kiwi_widget)
                    
                    self.set_child_properties(parent, kiwi_widget, child_props)
                    
                    if widget.__class__ in self.widget_data_types:
                        data_type = self.widget_data_types[widget.__class__]
                        kiwi_widget.data_type = data_type
                        
                        # More recent versions of Kiwi do this on their own:
                        kiwi_widget.model_attribute = widget_name
                    
                    widget = kiwi_widget
                except:
                    pass
                
                setattr(self._view, widget_name, widget)
    
    def copy_properties(self, dest, target):
        for prop in list_properties(dest):
            try:
                copy_complex_objects = (gtk.Adjustment)
                key = prop.name
                dest_value = dest.get_property(key)
                target_value = target.get_property(key)
                
                # Don't copy complex objects based on GObject by default. This
                # is not desired in most cases (e.g. parent or style property).
                # There are some type of objects that need to be copied though.
                # The values contained by a gtk.Adjustment object are needed
                # by gtk.SpinButtons for example.
                if isinstance(dest_value, OriginalGObject):
                    if isinstance(dest_value, copy_complex_objects) \
                        and type(dest_value) is type(target_value):
                            self.copy_properties(dest_value, target_value)
                elif not isinstance(target_value, OriginalGObject):
                    target.set_property(key, dest_value)
            
            except:
                pass
    
    def get_child_properties(self, parent, child):
        props = {}
        
        for prop in parent.list_child_properties():
            try:
                props[prop.name] = parent.child_get_property(child, prop.name)
            except:
                pass
        
        return props
    
    def set_child_properties(self, parent, child, properties):
        for key, value in properties.iteritems():
            try:
                parent.child_set_property(child, key, value)
            except:
                pass

def setup_kiwi_conversion():
    libgladeloader.LibgladeWidgetTree = KiwifiedLibgladeWidgetTree

# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/498171
class Flock:
    '''Class to handle creating and removing (pid) lockfiles'''
    
    # custom exceptions
    class FileLockAcquisitionError(Exception): pass
    class FileLockReleaseError(Exception): 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, debug=None):
        self.pid = os.getpid()
        self.host = gethostname()
        self.path = path
        self.debug = debug # set this to get status messages
    
    def acquire(self):
        '''Acquire a lock, returning self if successful, False otherwise'''
        if self.islocked():
            if self.debug:
                lock = self._readlock()
                print "Previous lock detected: %s" % self.pddr(lock)
            
            return False
        try:
            fh = open(self.path, "w")
            fh.write(self.addr())
            fh.close()
            
            if self.debug:
                print "Acquired lock: %s" % self.fddr()
        except:
            if isfile(self.path):
                try:
                    unlink(self.path)
                except:
                    pass
            raise (self.FileLockAcquisitionError, "Error acquiring lock: %s" % self.fddr())
        return self
    
    def release(self):
        '''Release lock, returning self'''
        if self.ownlock():
            try:
                os.unlink(self.path)
                
                if self.debug:
                    print "Released lock: %s" % self.fddr()
            except:
                raise (self.FileLockReleaseError, "Error releasing lock: %s" % self.fddr())
        
        return self
    
    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 islocked(self):
        '''Check if we already have a lock'''
        try:
            lock = self._readlock()
            os.kill(int(lock["pid"]), 0)
            return (lock["host"] == self.host)
        except:
            return False
    
    def ownlock(self):
        '''Check if we own the lock'''
        lock = self._readlock()
        return (self.fddr() == self.pddr(lock))
    
    def __del__(self):
        '''Magic method to clean up lock when program exits'''
        self.release()

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