"""Taken and modified from the dbsettings project.

http://code.google.com/p/django-values/
"""
from decimal import Decimal
from django import forms
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
try:
    from django.utils import simplejson
except ImportError:
    import simplejson
from collections import OrderedDict
from django.utils.encoding import smart_str
from django.utils.translation import gettext, ugettext_lazy as _
from keyedcache import cache_set
from livesettings.models import find_setting, LongSetting, Setting, SettingNotSet
from livesettings.overrides import get_overrides
from satchmo_utils import load_module, is_string_like, is_list_or_tuple
import datetime
import logging
import signals

__all__ = ['BASE_GROUP', 'ConfigurationGroup', 'Value', 'BooleanValue', 'DecimalValue', 'DurationValue',
      'FloatValue', 'IntegerValue', 'ModuleValue', 'PercentValue', 'PositiveIntegerValue', 'SortedDotDict',
      'StringValue', 'LongStringValue', 'MultipleStringValue']

_WARN = {}

log = logging.getLogger('configuration')

NOTSET = object()

class SortedDotDict(OrderedDict):

    def __getattr__(self, key):
        try:
            return self[key]
        except:
            raise AttributeError, key

    def __iter__(self):
        vals = self.values()
        for k in vals:
            yield k
            
    def values(self):
        vals = super(SortedDotDict, self).values()
        vals = [v for v in vals if isinstance(v, (ConfigurationGroup, Value))]
        vals.sort()
        return vals

class ConfigurationGroup(SortedDotDict):
    """A simple wrapper for a group of configuration values"""
    def __init__(self, key, name, *args, **kwargs):
        """Create a new ConfigurationGroup.
        
        Arguments:
        - key
        - group name - for display to user
        
        Named Arguments:
        - ordering: integer, optional, defaults to 1.
        - requires: See `Value` requires.  The default `requires` all member values will have if not overridden.
        - requiresvalue: See `Values` requires_value.  The default `requires_value` if not overridden on the `Value` objects.
        """
        self.key = key
        self.name = name
        self.ordering = kwargs.pop('ordering', 1)
        self.requires = kwargs.pop('requires', None)
        if self.requires:
            reqval = kwargs.pop('requiresvalue', key)
            if not is_list_or_tuple(reqval):
                reqval = (reqval, reqval)
                
            self.requires_value = reqval[0]
            self.requires.add_choice(reqval)
            
        super(ConfigurationGroup, self).__init__(*args, **kwargs)
                
    def __cmp__(self, other):
        return cmp((self.ordering, self.name), (other.ordering, other.name))
        
    def __eq__(self, other):
        return (type(self) == type(other) 
                and self.ordering == other.ordering 
                and self.name == other.name)

    def __ne__(self, other):
        return not self == other
        
    def values(self):
        vals = super(ConfigurationGroup, self).values()
        return [v for v in vals if v.enabled()]

BASE_GROUP = ConfigurationGroup('BASE', _('Base Settings'), ordering=0)

class Value(object):

    creation_counter = 0

    def __init__(self, group, key, **kwargs):
        """
        Create a new Value object for configuration.
        
        Args:
            - `ConfigurationGroup`
            - key - a string key
            
        Named arguments:
            - `description` - Will be passed to the field for form usage.  Should be a translation proxy.  Ex: _('example')
            - `help_text` - Will be passed to the field for form usage.
            - `choices` - If given, then the form field will use a select box
            - `ordering` - Defaults to alphabetical by key if not given.
            - `requires` - If given as a `Value`, then this field will only be rendered if that Value evaluates true (for Boolean requires) or the proper key is in the associated value.
            - `requiresvalue` - If set, then this field will only be rendered if that value is in the list returned by self.value. Defaults to self.key.
            - `hidden` - If true, then render a hidden field.
            - `default` - If given, then this Value will return that default whenever it has no assocated `Setting`.
        """
        self.group = group
        self.key = key
        self.description = kwargs.get('description', None)
        self.help_text = kwargs.get('help_text')
        self.choices = kwargs.get('choices',[])
        self.ordering = kwargs.pop('ordering', 0)
        self.hidden = kwargs.pop('hidden', False)
        
        self.requires = kwargs.pop('requires', None)
        if self.requires:
            reqval = kwargs.pop('requiresvalue', key)
            if not is_list_or_tuple(reqval):
                reqval = (reqval, reqval)
                
            self.requires_value = reqval[0]
            self.requires.add_choice(reqval)
            
        elif group.requires:
            self.requires = group.requires
            self.requires_value = group.requires_value
        
        if kwargs.has_key('default'):
            self.default = kwargs.pop('default')
            self.use_default = True                
        else:
            self.use_default = False
        
        self.creation_counter = Value.creation_counter
        Value.creation_counter += 1

    def __cmp__(self, other):
        return cmp((self.ordering, self.description, self.creation_counter), (other.ordering, other.description, other.creation_counter))
        
    def __eq__(self, other):
        if type(self) == type(other):
            return self.value == other.value
        else:
            return self.value == other
            
    def __iter__(self):
        return iter(self.value)
        
    def __unicode__(self):
        return unicode(self.value)
        
    def __str__(self):
        return str(self.value)
        
    def add_choice(self, choice):
        """Add a choice if it doesn't already exist."""
        if not is_list_or_tuple(choice):
            choice = (choice, choice)
        skip = False
        for k, v in self.choices:
            if k == choice[0]:
                skip = True
                break
        if not skip:
            self.choices += (choice, )
        
    def choice_field(self, **kwargs):
        if self.hidden:
            kwargs['widget'] = forms.MultipleHiddenInput()
        return forms.ChoiceField(choices=self.choices, **kwargs)

    def _choice_values(self):
        choices = self.choices
        vals = self.value
        return [x for x in choices if x[0] in vals]

    choice_values = property(fget=_choice_values)

    def copy(self):
        new_value = self.__class__(self.key)
        new_value.__dict__ = self.__dict__.copy()
        return new_value
        
    def _default_text(self):
        if not self.use_default:
            note = ""
        else:
            if self.default == "":
                note = _('Default value: ""')
            
            elif self.choices:
                work = []
                for x in self.choices:
                    if x[0] in self.default:
                        work.append(smart_str(x[1]))
                note = gettext('Default value: ') + ", ".join(work)
        
            else:
                note = _("Default value: %s") % unicode(self.default)
        
        return note
        
    default_text = property(fget=_default_text)
        
    def enabled(self):
        enabled = False
        try:
            if not self.requires:
                enabled = True
            else:
                v = self.requires.value
                if self.requires.choices:
                    enabled = self.requires_value == v or self.requires_value in v
                elif v:
                    enabled = True
        except SettingNotSet:
            pass
        return enabled
        
    def make_field(self, **kwargs):
        if self.choices:
            if self.hidden:
                kwargs['widget'] = forms.MultipleHiddenInput()
            field = self.choice_field(**kwargs)
        else:
            if self.hidden:
                kwargs['widget'] = forms.HiddenInput()
            field = self.field(**kwargs)
            
        field.group = self.group
        field.default_text = self.default_text
        return field

    def make_setting(self, db_value):
        log.debug('new setting %s.%s', self.group.key, self.key)
        return Setting(group=self.group.key, key=self.key, value=db_value)
        
    def _setting(self):
        return find_setting(self.group.key, self.key)
        
    setting = property(fget = _setting)

    def _value(self):            
        try:
            val = self.setting.value
            
        except SettingNotSet, sns:
            if self.use_default:
                val = self.default
            else:
                val = NOTSET
                                
        except AttributeError, ae:
            log.error("Attribute error: %s", ae)
            log.error("%s: Could not get _value of %s", self.key, self.setting)
            raise(ae)
            
        except Exception, e:
            global _WARN
            log.error(e)
            if str(e).find("configuration_setting") > -1:
                if not _WARN.has_key('configuration_setting'):
                    log.warn('Error loading setting %s.%s from table, OK if you are in syncdb', self.group.key, self.key)
                    _WARN['configuration_setting'] = True
                    
                if self.use_default:
                    val = self.default
                else:
                    raise ImproperlyConfigured("All settings used in startup must have defaults, %s.%s does not", self.group.key, self.key)
            else:
                import traceback
                traceback.print_exc()
                log.warn("Problem finding settings %s.%s, %s", self.group.key, self.key, e)
                raise SettingNotSet("Startup error, couldn't load %s.%s" %(self.group.key, self.key))
        return val

    def update(self, value):    
        use_db, overrides = get_overrides()
        
        if use_db:
            current_value = self.value

            new_value = self.to_python(value)
            if current_value != new_value:        
                db_value = self.get_db_prep_save(value)
                try:
                    s = self.setting
                    s.value = db_value
                
                except SettingNotSet:
                    s = self.make_setting(db_value)

                if self.use_default and self.default == new_value:
                    if s.id:
                        log.info("Deleted setting %s.%s", self.group.key, self.key)
                        s.delete()
                else:
                    log.info("Updated setting %s.%s = %s", self.group.key, self.key, value)
                    s.save()
                
                signals.configuration_value_changed.send(self, old_value=current_value, new_value=new_value, setting=self)
            
                return True
        else:
            log.debug('not updating setting %s.%s - livesettings db is disabled',self.group.key, self.key)
            
        return False

    def value(self):
        val = self._value()
        return self.to_python(val)

    value = property(fget = value)
        
    def editor_value(self):
        val = self._value()
        return self.to_editor(val)

    editor_value = property(fget = editor_value)

    # Subclasses should override the following methods where applicable

    def to_python(self, value):
        "Returns a native Python object suitable for immediate use"
        if value == NOTSET:
            value = None
        return value

    def get_db_prep_save(self, value):
        "Returns a value suitable for storage into a CharField"
        if value == NOTSET:
            value = ""
        return unicode(value)

    def to_editor(self, value):
        "Returns a value suitable for display in a form widget"
        if value == NOTSET:
            return NOTSET
        return unicode(value)

###############
# VALUE TYPES #
###############

class BooleanValue(Value):

    class field(forms.BooleanField):

        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.BooleanField.__init__(self, *args, **kwargs)

    def add_choice(self, choice):
        # ignore choice adding for boolean types
        pass

    def to_python(self, value):
        if value in (True, 't', 'True', 1, '1'):
            return True
        return False
        
    to_editor = to_python

class DecimalValue(Value):
    class field(forms.DecimalField):

           def __init__(self, *args, **kwargs):
               kwargs['required'] = False
               forms.DecimalField.__init__(self, *args, **kwargs)

    def to_python(self, value):
        if value==NOTSET:
            return Decimal("0")
            
        try:
            return Decimal(value)
        except TypeError, te:
            log.warning("Can't convert %s to Decimal for settings %s.%s", value, self.group.key, self.key)
            raise TypeError(te)
        
    def to_editor(self, value):
        if value == NOTSET:
            return "0"
        else:
            return unicode(value)

# DurationValue has a lot of duplication and ugliness because of issue #2443
# Until DurationField is sorted out, this has to do some extra work
class DurationValue(Value):

    class field(forms.CharField):
        def clean(self, value):
            try:
                return datetime.timedelta(seconds=float(value))
            except (ValueError, TypeError):
                raise forms.ValidationError('This value must be a real number.')
            except OverflowError:
                raise forms.ValidationError('The maximum allowed value is %s' % datetime.timedelta.max)

    def to_python(self, value):
        if value == NOTSET:
            value = 0
        if isinstance(value, datetime.timedelta):
            return value
        try:
            return datetime.timedelta(seconds=float(value))
        except (ValueError, TypeError):
            raise forms.ValidationError('This value must be a real number.')
        except OverflowError:
            raise forms.ValidationError('The maximum allowed value is %s' % datetime.timedelta.max)

    def get_db_prep_save(self, value):
        if value == NOTSET:
            return NOTSET
        else:
            return unicode(value.days * 24 * 3600 + value.seconds + float(value.microseconds) / 1000000)

class FloatValue(Value):
    
    class field(forms.FloatField):

        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.FloatField.__init__(self, *args, **kwargs)

    def to_python(self, value):
        if value == NOTSET:
            value = 0
        return float(value)
        
    def to_editor(self, value):
        if value == NOTSET:
            return "0"
        else:
            return unicode(value)
            
class IntegerValue(Value):
    class field(forms.IntegerField):

        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.IntegerField.__init__(self, *args, **kwargs)

    def to_python(self, value):
        if value == NOTSET:
            value = 0
        return int(value)

    def to_editor(self, value):
        if value == NOTSET:
            return "0"
        else:
            return unicode(value)


class PercentValue(Value):

    class field(forms.DecimalField):

        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.DecimalField.__init__(self, 100, 0, 5, 2, *args, **kwargs)

        class widget(forms.TextInput):
            def render(self, *args, **kwargs):
                # Place a percent sign after a smaller text field
                attrs = kwargs.pop('attrs', {})
                attrs['size'] = attrs['max_length'] = 6
                return forms.TextInput.render(self, attrs=attrs, *args, **kwargs) + '%'

    def to_python(self, value):
        if value == NOTSET:
            value = 0
        return Decimal(value) / 100

    def to_editor(self, value):
        if value == NOTSET:
            return "0"
        else:
            return unicode(value)

class PositiveIntegerValue(IntegerValue):

    class field(forms.IntegerField):

        def __init__(self, *args, **kwargs):
            kwargs['min_value'] = 0
            forms.IntegerField.__init__(self, *args, **kwargs)


class StringValue(Value):
    
    class field(forms.CharField):
        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.CharField.__init__(self, *args, **kwargs)
            
    def to_python(self, value):
        if value == NOTSET:
            value = ""
        return unicode(value)
            
    to_editor = to_python

class LongStringValue(Value):

    class field(forms.CharField):
        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            kwargs['widget'] = forms.Textarea()
            forms.CharField.__init__(self, *args, **kwargs)

    def make_setting(self, db_value):
        log.debug('new long setting %s.%s', self.group.key, self.key)
        return LongSetting(group=self.group.key, key=self.key, value=db_value)        

    def to_python(self, value):
        if value == NOTSET:
            value = ""
        return unicode(value)

    to_editor = to_python


class MultipleStringValue(Value):

    class field(forms.CharField):

        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.CharField.__init__(self, *args, **kwargs)
    
    def choice_field(self, **kwargs):
        kwargs['required'] = False
        return forms.MultipleChoiceField(choices=self.choices, **kwargs)
                
    def get_db_prep_save(self, value):
        if is_string_like(value):
            value = [value]
        return simplejson.dumps(value)
            
    def to_python(self, value):
        if not value or value == NOTSET:
            return []
        if is_list_or_tuple(value):
            return value
        else:
            try:
                return simplejson.loads(value)
            except:
                if is_string_like(value):
                    return [value]
                else:
                    log.warning('Could not decode returning empty list: %s', value)
                    return []
               
             
    to_editor = to_python
            
class ModuleValue(Value):
    """Handles setting modules, storing them as strings in the db."""
    
    class field(forms.CharField):

        def __init__(self, *args, **kwargs):
            kwargs['required'] = False
            forms.CharField.__init__(self, *args, **kwargs)

    def load_module(self, module):
        """Load a child module"""
        value = self._value()
        if value == NOTSET:
            raise SettingNotSet("%s.%s", self.group.key, self.key)
        else:
            return load_module("%s.%s" % (value, module))
            
    def to_python(self, value):
        if value == NOTSET:
            v = {}            
        else:
            v = load_module(value)
        return v
            
    def to_editor(self, value):
        if value == NOTSET:
            value = ""
        return value    

