########################################
# The contents of this file are subject to the MLX PUBLIC LICENSE version
# 1.0 (the "License"); you may not use this file except in
# compliance with the License.
# 
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See
# the License for the specific language governing rights and limitations
# under the License.
# 
# The Original Source Code is "compClust", released 2003 September 03.
# 
# The Original Source Code was developed by the California Institute of
# Technology (Caltech).  Portions created by Caltech are Copyright (C)
# 2002-2003 California Institute of Technology. All Rights Reserved.
########################################

"""Assist with constructing classes that manage access to algorithm parameters.
"""
__docformat__ = "restructuredtext en"

import types

class Priority:
  REQUIRED = 0x1
  OPTIONAL = 0x2
  EXPERIMENTAL = 0x10
  INTERNAL =0x4000
  ALL = REQUIRED | OPTIONAL | EXPERIMENTAL

class Property(object):
  """Base property type used by WrapperParameters
  """
  def __init__(self, name, default, doc, priority, compatibility_name):
    # process names
    if priority != Priority.INTERNAL:
      self.name = name
    else:
      # hide the internal parameters from tab completion
      self.name = "_"+name
    self.compatibility_name = compatibility_name
    if compatibility_name is not None:
      self.private_name = compatibility_name
    else:
      self.private_name = name

    self.default = default
    self.doc = doc
    self.priority = priority

    # standard getter & deleter
    self.__property = None
    def pget(self, private_name=self.private_name):
      return self._parameters[private_name]
    self.pget = pget

    def pdel(self, private_name=self.private_name):
      del self._parameters[private_name]
    self.pdel = pdel


    self._property = None
    
  def _get_property(self):
    """Make property from the get/set/del functions we've created.
    """
    if self._property is None:
      self._property = property(self.pget, self.pset, self.pdel, self.doc)
    return self._property
  prop = property(_get_property)

class NumberProperty(Property):
  """Intermediate class for numeric properties.
  """
  def __init__(self, name, default=None, min=None, max=None, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Store attributes common to all numeric properties.
    
    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `default`: The default value for the property
      - `min`: The minimum integer value this propoerty can have
      - `max`: The maximum integer value this property can have
      - `doc`: A docstring that will be attached to the property
      - `priority`: Set the importance of the property
      - `compatibility_name`: The name used by the wrapper setParameter call
    """
    super(NumberProperty, self).__init__(name, default, doc, priority, compatibility_name)
    self.min = min
    self.max = max
    
class IntProperty(NumberProperty):
  """
  Declares an integer property for use with WrapperParameters
  """
  def __init__(self, name, default=None, min=None, max=None, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Declares an integer property for use with WrapperParameters
    
    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `default`: The default value for the property
      - `min`: The minimum integer value this propoerty can have
      - `max`: The maximum integer value this property can have
      - `doc`: A docstring that will be attached to the property
      - `priority`: Set the importance of the property
      - `compatibility_name`: The name used by the wrapper setParameter call
    """
    super(IntProperty, self).__init__(name, default, min, max, doc, priority, compatibility_name)
    def pset(self, value, private_name=self.private_name):
      if value is None:
        # don't do any further checks for None
        pass
      elif type(value) != types.IntType:
        # try to do some coercion from string types, as the web renders
        # everything as strings
        if type(value) in types.StringTypes:
          value = int(value)
        else:
          raise ValueError("%s must be an integer not %s" % (private_name, str(type(value))))
      elif min is not None and value < min:
        raise ValueError("%s value %d was less than the minimum %d" %(private_name, value, min))
      elif max is not None and value > max:
        raise ValueError("%s value %d was more than the maximum %d" %(private_name, value, max))

      self._parameters[private_name] = value
    self.pset = pset


class FloatProperty(NumberProperty):
  """
  Declares floating point property for use with WrapperParameters
  """
  def __init__(self, name, default=None, min=None, max=None, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Instantiate a FloatProperty
  
    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `default`: The default value for the property
      - `min`: The minimum float value this propoerty can have
      - `max`: The maximum float value this property can have
      - `doc`: A docstring that will be attached to the property
      - `priority`: Declare if the property is required, optional, experimental.
      - `compatibility_name`: What name should this property have in a dictionary
    """
    super(FloatProperty, self).__init__(name, default, min, max, doc, priority, compatibility_name)
    def pset(self, value, private_name=self.private_name):
      if value is None:
        # don't do any further checks for None
        pass
      elif type(value) != types.FloatType:
        # try to do some coercion from string types, as the web renders
        # everything as strings
        if type(value) in types.StringTypes:
          value = float(value)
        else:
          raise ValueError("parameter must be a float not %s" % (str(type(value))))
      elif min is not None and value < min:
        raise ValueError("Value %d was less than the minimum %d" %(value, min))
      elif max is not None and value > max:
        raise ValueError("Value %d was more than the maximum %d" %(value, max))
      self._parameters[private_name] = value
    self.pset = pset

class ComboProperty(Property):
  """
  Declares a property with a fixed list of possible values
  """
  def __init__(self, name, default, choices, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Declares a property with a fixed list of possible values

    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `choices`: A list of allowable choices for the user to select from
      - `default`: The default value for the property
      - `doc`: A docstring that will be attached to the property
      - `priority`: Declare if the property is required, optional, experimental.
      - `compatibility_name`: What name should this property have in a dictionary
    """
    super(ComboProperty, self).__init__(name, default, doc, priority, compatibility_name)

    if type(choices) != types.ListType:
      raise ValueError("Need list of choices for a combo property not %s" %
                       (str(type(choices))))
  
    # default really needs to be one of the valid choices
    if default is not None:
      assert default in choices
    self.choices = choices
  
    # we can make up a help string for this
    if doc is None:
      doc="The property parameter must be one of the following values: %s" %(str(choices))
    def pset(self, value, private_name=self.private_name):
      if value is None:
        # don't do any further checks for None
        pass
      elif value not in choices:
        raise ValueError("%s %s was not one of %s" % (private_name, str(value), str(choices)))
      else:
        self._parameters[private_name] = value
    self.pset = pset

class ListProperty(Property):
  """
  Declares a property that holds a user provided list of values
  """
  def __init__(self, name, default=None, element_type=None, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Initialize a propertery containing a user provided list.
  
    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `default`: The default value for the property
      - `element_type`: If defined each element must have the listed type.
      - `doc`: A docstring that will be attached to the property
      - `priority`: Declare if the property is required, optional, experimental.
      - `compatibility_name`: What name should this property have in a dictionary
    """
    super(ListProperty, self).__init__(name, default, doc, priority, compatibility_name)
    
    # move the checking for type checking here instead of at runtime
    if element_type is None:
      def pset(self, value, private_name=self.private_name):
        if value is not None and type(value) != types.ListType:
          raise ValueError("parameter must be a list")
        self._parameters[private_name] = value
    else:
      def pset(self, value, private_name=self.private_name):
        if value is not None and type(value) != types.ListType:
          raise ValueError("parameter must be a list")
        elif len([ x for x in value if type(x) != element_type]) > 0:
          raise ValueError("components must have type %s" % (str(element_type)))
        else:
          self._parameters[private_name] = value
    self.pset = pset

class StrProperty(Property):
  """
  Declares a string property for use with WrapperParameters
  """
  def __init__(self,name, default=None, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Declares a string property for use with WrapperParameters
  
    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `default`: The default value for the property
      - `doc`: A docstring that will be attached to the property
      - `priority`: Declare if the property is required, optional, experimental.
      - `compatibility_name`: What name should this property have in a dictionary
    """
    super(StrProperty, self).__init__(name, default, doc, priority, compatibility_name)
    def pset(self, value, private_name=self.private_name):
      if value is None:
        pass
      elif type(value) not in types.StringTypes:
        raise ValueError("parameter must be a string not %s" % (str(type(value))))
      self._parameters[private_name] = value
    self.pset = pset

class FunctionProperty(Property):
  """
  Declares a property that holds a function for use with WrapperParameters
  """
  def __init__(self,name, default=None, doc=None, priority=Priority.OPTIONAL, compatibility_name=None):
    """
    Declares a string property for use with WrapperParameters
  
    :Parameters:
      - `name`: The name of the property in the gerenated class
      - `default`: The default value for the property
      - `doc`: A docstring that will be attached to the property
      - `priority`: Declare if the property is required, optional, experimental.
      - `compatibility_name`: What name should this property have in a dictionary
    """
    super(FunctionProperty, self).__init__(name, default, doc, priority, compatibility_name)
    def pset(self, value, private_name=self.private_name):
      if value is None:
        pass
      elif callable(value):
        raise ValueError("parameter must be callable %s" % (str(value)))
      self._parameters[private_name] = value
    self.pset = pset

class MetaWrapperParameters(type):
  """
  Magically transform a parameters definition into a fully featured class
  """
  def __new__(cls, name, bases, dictionary):
    """Customize the derived classes.
    """
    dictionary['_properties'] = {}
    dictionary['_default_values'] = {}
    dictionary['_compatibility_names'] = {}
    dictionary['_combo_values'] = {}
    if dictionary.has_key('_params'):
      for param in dictionary['_params']:
        dictionary[param.name] = param.prop
        dictionary['_default_values'][param.private_name] = param.default
        dictionary['_properties'][param.name] = param
        dictionary['_compatibility_names'][param.private_name] = param.name
        if isinstance(param, ComboProperty):
          dictionary['_combo_values'][param.name] = param.choices
      del dictionary['_params']
    if not (dictionary.has_key("__doc__") and len(dictionary['__doc__'])==0):
      dictionary['__doc__'] = """
This provides both a member variable and dictionary interface to the
various parameters needed to run a clustering algorithm. Each parameter
should have a doc string attached to it, so help(algorithm.parameters.k)
or the ipython equivalent algorithm.parameters.k? should return the
docstring for the k parameter."""
    return type.__new__(cls,name,bases,dictionary)
  
  #def __init__(cls, name, bases, dictionary):
  #  super(MetaWrapperParameters, cls).__init__(name, bases, dictionary)

  
class WrapperParameters(object):
  __metaclass__ = MetaWrapperParameters
  def __init__(self, parameters=None):
    self._parameters = {}
    self.resetParameters(parameters)

  def copy(self):
    p = self.getParametersDictionary()
    newclass = self.__class__()
    newclass.setParametersDictionary(p.copy())
    return newclass
    
  def setdefault(self, name, value=None):
    """D.setdefault(k,[d] -> D.get(k,d) also sets D[k] = d if k is not in D"""
    name = self._compatibility_names[name]
    self._parameters.setdefault(name, value)

  def resetParameters(self, parameters=None):
    """Set all defined parameters back to their default values
    """
    self._parameters.update(self._default_values)
    # if we don't have parameters
    if parameters is not None:
      if isinstance(parameters, WrapperParameters):
        parameters_dictionary = parameters.getParametersDictionary()
        self.setParametersDictionary(parameters_dictionary)
      elif isinstance(parameters, dict):
        self.setParametersDictionary(parameters)
      else:
        raise ValueError("Unrecognized parameter type")
      
  def getProperty(self, name):
    """
    Return a property class object by name
    """
    return self._properties[name]
  
  def getProperties(self, priority=Priority.ALL):
    """
    Return list of properties filtered by their priority
    """
    selected_props = {}
    for prop in self._properties.values():
      if prop.priority & priority:
        selected_props[prop.name] = prop
    return selected_props
    
  def getParametersDictionary(self):
    p = {}
    p.update(self._parameters)
    return p
    
  def setParametersDictionary(self, parameters_dict):
    for private_name, value in parameters_dict.items():
      self[private_name] = value
      
  def __getitem__(self, name):
    name = self._compatibility_names[name]
    return getattr(self, name)
  
  def __setitem__(self, name, value):
    name = self._compatibility_names[name]
    return setattr(self, name, value)

  def has_key(self, key):
    return self._parameters.has_key(key)

  def items(self):
    return self._parameters.items()
  
  def keys(self):
    return self._parameters.keys()
  
  def __len__(self):
    return len(self._parameters)

  def __str__(self):
    return str(self._parameters)

  def values(self):
    return self._parameters.values()



def HTMLFormatParameter(display_name, prop, value, variable_name=None):
  """
  Formats one of the parameters from a WrapperParameters class
  
  :Parameters:
    - `display_name`: The name to show the user
    - `prop`: the internal property class
    - `value`: the current value of the property
    - `variable_name`: an internal name for the html variable,
                       defaults to diesplay_name if None.

  The variable_name is so when there is a list of algorithms being
  rendered on the same page which have a parameter with the same name
  there is some way of telling them apart. Since the variable name
  needs to be parsed by the application calling this code, I'm letting
  it choose the name.
  """
  from quixote.html import htmlescape
  
  def format_number(display_name, param, value, variable_name):
    """Format a number type
    """
    d =  {'display_name': htmlescape(display_name),
          'variable_name': htmlescape(variable_name),
          'value': htmlescape(value),
          'description': htmlescape(param.__doc__)}
    html = ['''   <p>
    <label for="%(variable_name)s">%(display_name)s</label>
    <input id="%(variable_name)s" name="%(variable_name)s" type="text" value="%(value)s"/>%(description)s</p>\n'''% d]
    return html

  def format_combo(display_name, param, value, variable_name):
    """Format a list of choices type (providing the user a list)
    """
    html = ['''    <p>
      <label for="%(variable_name)s">%(display_name)s</label> 
      <select id="%(variable_name)s" name="%(variable_name)s">\n'''% \
            {'display_name': htmlescape(display_name),
             'variable_name': htmlescape(variable_name)}]
    for choice in param.choices:
      d = { 'option': htmlescape(choice) } 
      if choice == value:
        d['selected'] = 'selected="selected"'
      else:
        d['selected'] = ""
      html += ['    <option %(selected)s>%(option)s</option>\n' % (d)]
    html += ['''
    </select> 
    %s
    </p>\n''' % (htmlescape(param.__doc__))]
    return html

  def format_list(display_name, param, value, variable_name):
    """Format a list of choices type (asking the user for list)
    """
    d =  {'display_name': htmlescape(display_name),
          'variable_name': htmlescape(variable_name),
          'value': htmlescape(value),
          'description': htmlescape(param.__doc__)}
    html = ['''    <p><label for="%(variable_name)s">%(display_name)s</label> (enter one parameter value per line)
    <textarea id="%(variable_name)s" name="%(variable_name)s" rows="5" cols="60"/>
    <br/>
    %(description)s
    </p>\n'''% d]
    return html
    
  def format_string(display_name, param, value, variable_name):
    """Format a string type
    """
    d =  {'display_name': htmlescape(display_name),
          'variable_name': htmlescape(variable_name),
          'value': htmlescape(value),
          'description': htmlescape(param.__doc__)}
    html = ['''    <p><label for="%(variable_name)s">%(display_name)s</label>
    <input id="%(variable_name)s" name="%(variable_name)s" type="text" value="%(value)s"/> 
    %(description)s
    </p>\n'''% d]
    return html

  if variable_name is None:
    variable_name = display_name
    
  if isinstance(prop, ComboProperty):
    return format_combo(display_name, prop, value, variable_name)
  elif isinstance(prop, ListProperty):
    return format_list(display_name, prop, value, variable_name)
  elif isinstance(prop, NumberProperty):
    return format_number(display_name, prop, value, variable_name)
  elif isinstance(prop, StrProperty):
    return format_string(display_name, prop, value, variable_name)
  else:
    return "<p><b>Unrecognized parameter %s of type %s</b></p>" %(display_name, str(param.dataType)[1:-1])

