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

'''
Task Coach - Your friendly task manager
Copyright (C) 2004-2012 Task Coach developers <developers@taskcoach.org>

Task Coach 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.

Task Coach 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/>.
'''

import uuid
from taskcoachlib import patterns
import attribute
from taskcoachlib.domain.attribute import icon


class SynchronizedObject(object):
    STATUS_NONE    = 0
    STATUS_NEW     = 1
    STATUS_CHANGED = 2
    STATUS_DELETED = 3

    def __init__(self, *args, **kwargs):
        self.__status = kwargs.pop('status', self.STATUS_NEW)
        super(SynchronizedObject, self).__init__(*args, **kwargs)

    @classmethod
    def markDeletedEventType(class_):
        return 'object.markdeleted'

    @classmethod
    def markNotDeletedEventType(class_):
        return 'object.marknotdeleted'
        
    def __getstate__(self):
        try:
            state = super(SynchronizedObject, self).__getstate__()
        except AttributeError:
            state = dict()

        state['status'] = self.__status
        return state

    @patterns.eventSource
    def __setstate__(self, state, event=None):
        try:
            super(SynchronizedObject, self).__setstate__(state, event=event)
        except AttributeError:
            pass
        if state['status'] != self.__status:
            if state['status'] == self.STATUS_CHANGED:
                self.markDirty(event=event)
            elif state['status'] == self.STATUS_DELETED:
                self.markDeleted(event=event)
            elif state['status'] == self.STATUS_NEW:
                self.markNew(event=event)
            elif state['status'] == self.STATUS_NONE:
                self.cleanDirty(event=event)

    def getStatus(self):
        return self.__status
        
    @patterns.eventSource
    def markDirty(self, force=False, event=None):
        if not self.setStatusDirty(force):
            return
        event.addSource(self, self.__status, 
                        type=self.markNotDeletedEventType())

    def setStatusDirty(self, force=False):
        oldStatus = self.__status
        if self.__status == self.STATUS_NONE or force:
            self.__status = self.STATUS_CHANGED
            return oldStatus == self.STATUS_DELETED
        else:
            return False

    @patterns.eventSource
    def markNew(self, event=None):
        if not self.setStatusNew():
            return
        event.addSource(self, self.__status,
                        type=self.markNotDeletedEventType())
            
    def setStatusNew(self):
        oldStatus = self.__status
        self.__status = self.STATUS_NEW
        return oldStatus == self.STATUS_DELETED

    @patterns.eventSource
    def markDeleted(self, event=None):
        self.setStatusDeleted()
        event.addSource(self, self.__status, type=self.markDeletedEventType())

    def setStatusDeleted(self):
        self.__status = self.STATUS_DELETED

    @patterns.eventSource
    def cleanDirty(self, event=None):
        if not self.setStatusNone():
            return
        event.addSource(self, self.__status, 
                        type=self.markNotDeletedEventType())
            
    def setStatusNone(self):
        oldStatus = self.__status
        self.__status = self.STATUS_NONE
        return oldStatus == self.STATUS_DELETED

    def isNew(self):
        return self.__status == self.STATUS_NEW

    def isModified(self):
        return self.__status == self.STATUS_CHANGED

    def isDeleted(self):
        return self.__status == self.STATUS_DELETED

        
class Object(SynchronizedObject):
    def __init__(self, *args, **kwargs):
        Attribute = attribute.Attribute
        self.__subject = Attribute(kwargs.pop('subject', ''), self, 
                                   self.subjectChangedEvent)
        self.__description = Attribute(kwargs.pop('description', ''), self,
                                       self.descriptionChangedEvent)
        self.__fgColor = Attribute(kwargs.pop('fgColor', None), self, 
                                   self.appearanceChangedEvent)
        self.__bgColor = Attribute(kwargs.pop('bgColor', None), self,
                                   self.appearanceChangedEvent)
        self.__font = Attribute(kwargs.pop('font', None), self,
                                self.appearanceChangedEvent)
        self.__icon = Attribute(kwargs.pop('icon', ''), self,
                                self.appearanceChangedEvent)
        self.__selectedIcon = Attribute(kwargs.pop('selectedIcon', ''), self,
                                        self.appearanceChangedEvent)
        self.__ordering = Attribute(kwargs.pop('ordering', 0L), self,
                                    self.orderingChangedEvent)
        self.__id = kwargs.pop('id', None) or str(uuid.uuid1())
        super(Object, self).__init__(*args, **kwargs)
        
    def __repr__(self):
        return self.subject()

    def __getstate__(self):
        try:
            state = super(Object, self).__getstate__()
        except AttributeError:
            state = dict()
        state.update(dict(id=self.__id, subject=self.__subject.get(), 
                          description=self.__description.get(),
                          fgColor=self.__fgColor.get(),
                          bgColor=self.__bgColor.get(),
                          font=self.__font.get(),
                          icon=self.__icon.get(),
                          ordering=self.__ordering.get(),
                          selectedIcon=self.__selectedIcon.get()))
        return state
    
    @patterns.eventSource
    def __setstate__(self, state, event=None):
        try:
            super(Object, self).__setstate__(state, event=event)
        except AttributeError:
            pass
        self.setId(state['id'])
        self.setSubject(state['subject'], event=event)
        self.setDescription(state['description'], event=event)
        self.setForegroundColor(state['fgColor'], event=event)
        self.setBackgroundColor(state['bgColor'], event=event)
        self.setFont(state['font'], event=event)
        self.setIcon(state['icon'], event=event)
        self.setSelectedIcon(state['selectedIcon'], event=event)
        self.setOrdering(state['ordering'])

    def __getcopystate__(self):
        ''' Return a dictionary that can be passed to __init__ when creating
            a copy of the object. 
            
            E.g. copy = obj.__class__(**original.__getcopystate__()) '''
        try:
            state = super(Object, self).__getcopystate__()
        except AttributeError:
            state = dict()
        # Note: we don't put the id in the state dict, because a copy should
        # get a new id:
        state.update(dict(\
            subject=self.__subject.get(), description=self.__description.get(),
            fgColor=self.__fgColor.get(), bgColor=self.__bgColor.get(),
            font=self.__font.get(), icon=self.__icon.get(),
            selectedIcon=self.__selectedIcon.get()),
            ordering=self.__ordering.get())
        return state
    
    def copy(self):
        return self.__class__(**self.__getcopystate__())
 
    # Id:
       
    def id(self):
        return self.__id
    
    def setId(self, id_):
        self.__id = id_
        
    # Subject:
    
    def subject(self):
        return self.__subject.get()
    
    def setSubject(self, subject, event=None):
        self.__subject.set(subject, event=event)
        
    def subjectChangedEvent(self, event):
        event.addSource(self, self.subject(), type=self.subjectChangedEventType())
    
    @classmethod    
    def subjectChangedEventType(class_):
        return '%s.subject'%class_
    
    @staticmethod
    def subjectSortFunction(**kwargs):
        ''' Function to pass to list.sort when sorting by subject. '''
        if kwargs.get('sortCaseSensitive', False):
            return lambda item: item.subject()
        else:
            return lambda item: item.subject().lower()
        
    @classmethod
    def subjectSortEventTypes(class_):
        ''' The event types that influence the subject sort order. '''
        return (class_.subjectChangedEventType(),)

    # Ordering:

    def ordering(self):
        return self.__ordering.get()

    def setOrdering(self, ordering, event=None):
        self.__ordering.set(ordering, event=event)

    def orderingChangedEvent(self, event):
        event.addSource(self, self.ordering(), type=self.orderingChangedEventType())

    @classmethod
    def orderingChangedEventType(class_):
        return '%s.ordering'%class_

    @staticmethod
    def orderingSortFunction(**kwargs):
        return lambda item: item.ordering()

    @classmethod
    def orderingSortEventTypes(class_):
        return (class_.orderingChangedEventType(),)

    # Description:
    
    def description(self):
        return self.__description.get()
    
    def setDescription(self, description, event=None):
        self.__description.set(description, event=event)
        
    def descriptionChangedEvent(self, event):
        event.addSource(self, self.description(), 
                        type=self.descriptionChangedEventType())
            
    @classmethod    
    def descriptionChangedEventType(class_):
        return '%s.description'%class_

    @staticmethod
    def descriptionSortFunction(**kwargs):
        ''' Function to pass to list.sort when sorting by description. '''
        if kwargs.get('sortCaseSensitive', False):
            return lambda item: item.description()
        else:
            return lambda item: item.description().lower()
    
    @classmethod
    def descriptionSortEventTypes(class_):
        ''' The event types that influence the description sort order. '''
        return (class_.descriptionChangedEventType(),)
    
    # Color:
    
    def setForegroundColor(self, color, event=None):
        self.__fgColor.set(color, event=event)
    
    def foregroundColor(self, recursive=False): # pylint: disable-msg=W0613
        # The 'recursive' argument isn't actually used here, but some
        # code assumes composite objects where there aren't. This is
        # the simplest workaround.
        return self.__fgColor.get()

    def setBackgroundColor(self, color, event=None):
        self.__bgColor.set(color, event=event)
        
    def backgroundColor(self, recursive=False): # pylint: disable-msg=W0613
        # The 'recursive' argument isn't actually used here, but some
        # code assumes composite objects where there aren't. This is
        # the simplest workaround.
        return self.__bgColor.get()
    
    # Font:
    
    def font(self, recursive=False): # pylint: disable-msg=W0613
        # The 'recursive' argument isn't actually used here, but some
        # code assumes composite objects where there aren't. This is
        # the simplest workaround.
        return self.__font.get()
    
    def setFont(self, font, event=None):
        self.__font.set(font, event=event)

    # Icons:

    def icon(self):
        return self.__icon.get()

    def setIcon(self, icon, event=None):
        self.__icon.set(icon, event=event)

    def selectedIcon(self):
        return self.__selectedIcon.get()

    def setSelectedIcon(self, selectedIcon, event=None):
        self.__selectedIcon.set(selectedIcon, event=event)
    
    # Event types:
    
    @classmethod
    def appearanceChangedEventType(class_):
        return '%s.appearance'%class_
    
    def appearanceChangedEvent(self, event):
        event.addSource(self, type=self.appearanceChangedEventType())
    
    @classmethod
    def modificationEventTypes(class_):
        try:
            eventTypes = super(Object, class_).modificationEventTypes()
        except AttributeError:
            eventTypes = []
        return eventTypes + [class_.subjectChangedEventType(),
                             class_.descriptionChangedEventType(),
                             class_.appearanceChangedEventType()]


class CompositeObject(Object, patterns.ObservableComposite):
    def __init__(self, *args, **kwargs):
        self.__expandedContexts = set(kwargs.pop('expandedContexts', []))
        super(CompositeObject, self).__init__(*args, **kwargs)

    def __getcopystate__(self):
        state = super(CompositeObject, self).__getcopystate__()
        state.update(dict(expandedContexts=self.expandedContexts()))
        return state

    # Subject:
    
    def subject(self, recursive=False): # pylint: disable-msg=W0221
        subject = super(CompositeObject, self).subject()
        if recursive and self.parent():
            subject = u'%s -> %s'%(self.parent().subject(recursive=True), subject)
        return subject

    def subjectChangedEvent(self, event):
        super(CompositeObject, self).subjectChangedEvent(event)
        for child in self.children():
            child.subjectChangedEvent(event)

    @staticmethod
    def subjectSortFunction(**kwargs):
        ''' Function to pass to list.sort when sorting by subject. '''
        recursive = kwargs.get('treeMode', False)
        if kwargs.get('sortCaseSensitive', False):
            return lambda item: item.subject(recursive=recursive)
        else:
            return lambda item: item.subject(recursive=recursive).lower()
        
    # Description:
        
    def description(self, recursive=False): # pylint: disable-msg=W0221,W0613
        # Allow for the recursive flag, but ignore it
        return super(CompositeObject, self).description()
        
    # Expansion state:

    # Note: expansion state is stored by context. A context is a simple string
    # identifier (without comma's) to distinguish between different contexts,
    # i.e. viewers. A composite object may be expanded in one context and
    # collapsed in another.
    
    def isExpanded(self, context='None'):
        ''' Returns a boolean indicating whether the composite object is 
            expanded in the specified context. ''' 
        return context in self.__expandedContexts

    def expandedContexts(self):
        ''' Returns a list of contexts where this composite object is 
            expanded. ''' 
        return list(self.__expandedContexts)
    
    @patterns.eventSource
    def expand(self, expand=True, context='None', event=None):
        ''' Expands (or collapses) the composite object in the specified 
            context. ''' 
        if expand == self.isExpanded(context):
            return
        if expand:
            self.__expandedContexts.add(context)
        else:
            self.__expandedContexts.discard(context)
        self.expansionChangedEvent(event)

    @classmethod
    def expansionChangedEventType(class_):
        return '%s.expanded'%class_

    def expansionChangedEvent(self, event):
        event.addSource(self, type=self.expansionChangedEventType())
    
    # Appearance:

    def appearanceChangedEvent(self, event):
        super(CompositeObject, self).appearanceChangedEvent(event)
        # Assume that most of the times our children change appearance too
        for child in self.children():
            child.appearanceChangedEvent(event)

    def foregroundColor(self, recursive=False):
        myFgColor = super(CompositeObject, self).foregroundColor()
        if not myFgColor and recursive and self.parent():
            return self.parent().foregroundColor(recursive=True)
        else:
            return myFgColor
                
    def backgroundColor(self, recursive=False):
        myBgColor = super(CompositeObject, self).backgroundColor()
        if not myBgColor and recursive and self.parent():
            return self.parent().backgroundColor(recursive=True)
        else:
            return myBgColor
    
    def font(self, recursive=False):
        myFont = super(CompositeObject, self).font()
        if not myFont and recursive and self.parent():
            return self.parent().font(recursive=True)
        else:
            return myFont

    def icon(self, recursive=False):
        myIcon = super(CompositeObject, self).icon()
        if not recursive:
            return myIcon
        if not myIcon and self.parent():
            myIcon = self.parent().icon(recursive=True)
        return self.pluralOrSingularIcon(myIcon)

    def selectedIcon(self, recursive=False):
        myIcon = super(CompositeObject, self).selectedIcon()
        if not recursive:
            return myIcon
        if not myIcon and self.parent():
            myIcon = self.parent().selectedIcon(recursive=True)
        return self.pluralOrSingularIcon(myIcon)

    def pluralOrSingularIcon(self, myIcon):
        hasChildren = any(child for child in self.children() if not child.isDeleted())
        mapping = icon.itemImagePlural if hasChildren else icon.itemImageSingular
        return mapping.get(myIcon, myIcon)
    
    # Event types:

    @classmethod
    def modificationEventTypes(class_):
        return super(CompositeObject, class_).modificationEventTypes() + \
            [class_.expansionChangedEventType()]

    # Override SynchronizedObject methods to also mark child objects

    @patterns.eventSource
    def markDeleted(self, event=None):
        super(CompositeObject, self).markDeleted(event=event)
        for child in self.children():
            child.markDeleted(event=event)

    @patterns.eventSource
    def markNew(self, event=None):
        super(CompositeObject, self).markNew(event=event)
        for child in self.children():
            child.markNew(event=event)

    @patterns.eventSource
    def markDirty(self, force=False, event=None):
        super(CompositeObject, self).markDirty(force, event=event)
        for child in self.children():
            child.markDirty(force, event=event)

    @patterns.eventSource            
    def cleanDirty(self, event=None):
        super(CompositeObject, self).cleanDirty(event=event)
        for child in self.children():
            child.cleanDirty(event=event)
