import cherrypy
from cgi import escape
from Html_cleaner import Html_cleaner


class Validation_error( Exception ):
  """
  An exception raised when form validation fails for some reason.
  """

  MESSAGE_MAP = {
    int: u"can only contain digits",
  }

  def __init__( self, name, value, value_type, message = None ):
    Exception.__init__( self )
    self.__name = name
    self.__value = value
    self.__value_type = value_type

    if message is None:
      # if the value's type has a message member, use that. otherwise, look up the type in a map
      if hasattr( value_type, u"message" ):
        self.__message = value_type.message
      else:
        self.__message = self.MESSAGE_MAP.get( value_type, u"is invalid" )
    else:
      self.__message = message

  def __str__( self ):
    return self.__message

  def to_dict( self ):
    return dict(
      error = u"The %s %s." % ( self.__name.replace( u"_", " " ), self.__message ),
      name = self.__name,
      value = self.__value,
    )

  name = property( lambda self: self.__name )
  value = property( lambda self: self.__value )
  value_type = property( lambda self: self.__value_type )
  message = property( lambda self: self.__message )


class Valid_string( object ):
  """
  Validator for a string of certain minimum and maximum lengths.
  """
  moron_map = {
    u"\xa0": u" ",
    u"\xa9": u"(c)",
    u"\xae": u"(r)",
    u"\xb7": u"*",
    u"\u2002": u" ",
    u"\u2003": u" ",
    u"\u2009": u" ",
    u"\u2010": u"-",
    u"\u2011": u"-",
    u"\u2013": u"-",
    u"\u2014": u"--",
    u"\u2015": u"--",
    u"\u2016": u"--",
    u"\u2017": u"||",
    u"\u2018": u"'",
    u"\u2019": u"'",
    u"\u201a": u",",
    u"\u201b": u"'",
    u"\u201c": u'"',
    u"\u201d": u'"',
    u"\u201e": u",,",
    u"\u201f": u'"',
    u"\u2022": u"*",
    u"\u2023": u"*",
    u"\u2024": u".",
    u"\u2025": u"..",
    u"\u2026": u"...",
    u"\u2027": u".",
    u"\u2122": u"(tm)",
  }

  def __init__( self, min = None, max = None, escape_html = True ):
    self.min = min
    self.max = max
    self.escape_html = escape_html
    self.message = None

  def __call__( self, value ):
    value = self.__demoronize( value.strip() )

    if self.min is not None and len( value ) < self.min:
      if self.min == 1:
        self.message = u"is missing"
      else:
        self.message = u"must be at least %s characters long" % self.min
      raise ValueError()
    elif self.max is not None and len( value ) > self.max:
      self.message = u"must be no longer than %s characters" % self.max
      raise ValueError()

    # either escape all html completely or just clean up the html, stripping out everything that's
    # not on a tag/attribute whitelist
    if self.escape_html:
      return escape( value, quote = True )
    else:
      cleaner = Html_cleaner()
      return cleaner.strip( value )

  def __demoronize( self, value ):
    """
    Convert stupid Microsoft unicode symbols to saner, cross-platform equivalents.
    """
    try:
      for ( moron_symbol, replacement ) in self.moron_map.items():
        value = value.replace( moron_symbol, replacement )
    except:
      import traceback
      traceback.print_exc()
      raise

    return value


class Valid_bool( object ):
  """
  Validator for a boolean value.
  """
  def __init__( self, none_okay = False ):
    self.__none_okay = none_okay

  def __call__( self, value ):
    value = value.strip()
    if self.__none_okay and value in ( None, "None", "" ): return None

    if value in ( u"True", u"true" ): return True
    if value in ( u"False", u"false" ): return False

    raise ValueError()


class Valid_int( object ):
  """
  Validator for an integer value.
  """
  def __init__( self, min = None, max = None, none_okay = False ):
    self.min = min
    self.max = max
    self.message = None
    self.__none_okay = none_okay

  def __call__( self, value ):
    if self.__none_okay and value in ( None, "None", "" ): return None
    value = int( value )

    if self.min is not None and value < self.min:
      self.message = "is too small"
      raise ValueError()
    if self.max is not None and value > self.max:
      self.message = "is too large"
      raise ValueError()

    return value


def validate( **expected ):
  """
  validate() can be used to require that the arguments of the decorated method successfully pass
  through particular validators. The validate() method itself is evaluated where it is used as a
  decorator, which just returns decorate() to be used as the actual decorator.

  Example usage:

    @validate(
      foo = Valid_string( min = 5, max = 10 ),
      bar = int
    )
    def method( self, foo, bar ): pass

  Note that validate() currently only works for instance methods (methods that take self as the
  first argument). Also note that you can use multiple validators for a single argument.

  Example usage:

    @validate(
      foo = Valid_string( min = 5, max = 10 ),
      bar = ( int, valid_bar )
    )
    def method( self, foo, bar ): pass

  """
  def decorate( function ):
    """
    When the method being decorated is invoked, its decorator gets invoked instead and is supposed
    to return a new function to use in place of the method being decorated (or a modified version
    of that function). In this case, the decorator is our decorate() function, and the function it
    returns is the check() function. decorate()'s first argument is the method being decorated.
    """
    def check( *args, **kwargs ):
      """
      check() pretends that it's the method being decorated. It takes the same arguments and then
      invokes the actual method being decorated, passing in those arguments, but only after first
      validating all of those arguments to that function. If validation fails, a Validation_error
      is raised. Note that in Python, keyword argument names have to be str, not unicode.
      """
      args = list( args )
      args_index = 1 # skip the self argument

      # determine the expected argument names from the decorated function itself
      code = function.func_code
      expected_names = code.co_varnames[ : code.co_argcount ]

      # validate each of the expected arguments
      for expected_name in expected_names:
        if expected_name == u"self": continue
        expected_type = expected.get( expected_name )        

        # look for expected_name in kwargs and store the validated value there
        if expected_name in kwargs:
          value = kwargs.get( expected_name )
          # if there's a tuple of multiple validators for this expected_name, use all of them
          if isinstance( expected_type, tuple ):
            for validator in expected_type:
              try:
                value = validator( value )
              except ( ValueError, TypeError ):
                raise Validation_error( expected_name, value, validator )
            kwargs[ str( expected_name ) ] = value
          # otherwise, there's just a single validator
          else:
            try:
              kwargs[ str( expected_name ) ] = expected_type( value )
            except ( ValueError, TypeError ):
              raise Validation_error( expected_name, value, expected_type )
          continue

        # expected_name wasn't found in kwargs, so look for it in args. if it's not there either,
        # raise unless there's a default value for the argument in the decorated function
        if args_index >= len( args ):
          if function.func_defaults and args_index >= len( args ) - len( function.func_defaults ):
            continue
          raise Validation_error( expected_name, None, expected_type, message = u"is required" )
        value = args[ args_index ]

        # if there's a tuple of multiple validators for this expected_name, use all of them
        if isinstance( expected_type, tuple ):
          for validator in expected_type:
            try:
              value = validator( value )
            except ( ValueError, TypeError ):
              raise Validation_error( expected_name, value, validator )
          args[ args_index ] = value
        # otherwise, there's just a single validator
        else:
          try:
            args[ args_index ] = expected_type( value )
          except ( ValueError, TypeError ):
            raise Validation_error( expected_name, value, expected_type )
        args_index += 1

      # if there are any unexpected arguments, raise
      for ( arg_name, arg_value ) in kwargs.items():
        if not arg_name in expected_names:
          raise Validation_error( arg_name, arg_value, None, message = u"is an unknown argument" )

      return function( *args, **kwargs )

    return check

  return decorate
