# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import sys
import re
import datetime
import types
import json

from configman.datetime_util import (
    datetime_from_ISO_string,
    date_from_ISO_string,
    datetime_to_ISO_string,
    date_to_ISO_string,
    timedelta_to_str,
)

# for backward compatibility these two methods get alternate names
datetime_converter = datetime_from_ISO_string
date_converter = date_from_ISO_string

from configman.config_exceptions import CannotConvertError

import datetime_util

#------------------------------------------------------------------------------
#  Utility section
#
#  various handy functions used or associated with type conversions
#------------------------------------------------------------------------------


#------------------------------------------------------------------------------
def str_dict_keys(a_dict):
    """return a modified dict where all the keys that are anything but str get
    converted to str.
    E.g.

      >>> result = str_dict_keys({u'name': u'Peter', u'age': 99, 1: 2})
      >>> # can't compare whole dicts in doctests
      >>> result['name']
      u'Peter'
      >>> result['age']
      99
      >>> result[1]
      2

    The reason for this is that in Python <= 2.6.4 doing
    ``MyClass(**{u'name': u'Peter'})`` would raise a TypeError

    Note that only unicode types are converted to str types.
    The reason for that is you might have a class that looks like this::

        class Option(object):
            def __init__(self, foo=None, bar=None, **kwargs):
                ...

    And it's being used like this::

        Option(**{u'foo':1, u'bar':2, 3:4})

    Then you don't want to change that {3:4} part which becomes part of
    `**kwargs` inside the __init__ method.
    Using integers as parameter keys is a silly example but the point is that
    due to the python 2.6.4 bug only unicode keys are converted to str.
    """
    new_dict = {}
    for key in a_dict:
        if isinstance(key, unicode):
            new_dict[str(key)] = a_dict[key]
        else:
            new_dict[key] = a_dict[key]
    return new_dict


#------------------------------------------------------------------------------
def str_quote_stripper(input_str):
    if not isinstance(input_str, basestring):
        raise ValueError(input_str)
    while (
        input_str
        and input_str[0] == input_str[-1]
        and input_str[0] in ("'", '"')
    ):
        input_str = input_str.strip(input_str[0])
    return input_str


#------------------------------------------------------------------------------
#  from string section
#
#  a set of functions that will convert from a string representation into some
#  specified type
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
# a bunch of known mappings of builtin items to strings
import __builtin__
known_mapping_str_to_type = dict(
    (key, val) for key, val in __builtin__.__dict__.iteritems()
    if val not in (True, False)
)

#------------------------------------------------------------------------------
from configman.datetime_util import (
    str_to_timedelta,  # don't worry about pyflakes here, unused in this file
    timedelta_to_str,  # but used elsewhere
)

timedelta_converter = str_to_timedelta  # for backward compatiblity


#------------------------------------------------------------------------------
def str_to_boolean(input_str):
    """ a conversion function for boolean
    """
    if not isinstance(input_str, basestring):
        raise ValueError(input_str)
    input_str = str_quote_stripper(input_str)
    return input_str.lower() in ("true", "t", "1", "y", "yes")

boolean_converter = str_to_boolean  # for backward compatiblity


#------------------------------------------------------------------------------
def str_to_python_object(input_str):
    """ a conversion that will import a module and class name
    """
    if not input_str:
        return None
    if not isinstance(input_str, basestring):
        # gosh, we didn't get a string, we can't convert anything but strings
        # we're going to assume that what we got is actually what was wanted
        # as the output
        return input_str
    input_str = str_quote_stripper(input_str)
    if '.' not in input_str and input_str in known_mapping_str_to_type:
        return known_mapping_str_to_type[input_str]
    parts = [x.strip() for x in input_str.split('.') if x.strip()]
    try:
        try:
            # first try as a complete module
            package = __import__(input_str)
        except ImportError:
            # it must be a class from a module
            if len(parts) == 1:
                # since it has only one part, it must be a class from __main__
                parts = ('__main__', input_str)
            package = __import__('.'.join(parts[:-1]), globals(), locals(), [])
        obj = package
        for name in parts[1:]:
            obj = getattr(obj, name)
        return obj
    except AttributeError, x:
        raise CannotConvertError("%s cannot be found" % input_str)
    except ImportError, x:
        raise CannotConvertError(str(x))

class_converter = str_to_python_object  # for backward compatibility


#------------------------------------------------------------------------------
def str_to_classes_in_namespaces(
    template_for_namespace="cls%d",
    name_of_class_option='cls',
    instantiate_classes=False
):
    """take a comma delimited  list of class names, convert each class name
    into an actual class as an option within a numbered namespace.  This
    function creates a closure over a new function.  That new function,
    in turn creates a class derived from RequiredConfig.  The inner function,
    'class_list_converter', populates the InnerClassList with a Namespace for
    each of the classes in the class list.  In addition, it puts the each class
    itself into the subordinate Namespace.  The requirement discovery mechanism
    of configman then reads the InnerClassList's requried config, pulling in
    the namespaces and associated classes within.

    For example, if we have a class list like this: "Alpha, Beta", then this
    converter will add the following Namespaces and options to the
    configuration:

        "cls0" - the subordinate Namespace for Alpha
        "cls0.cls" - the option containing the class Alpha itself
        "cls1" - the subordinate Namespace for Beta
        "cls1.cls" - the option containing the class Beta itself

    Optionally, the 'class_list_converter' inner function can embue the
    InnerClassList's subordinate namespaces with aggregates that will
    instantiate classes from the class list.  This is a convenience to the
    programmer who would otherwise have to know ahead of time what the
    namespace names were so that the classes could be instantiated within the
    context of the correct namespace.  Remember the user could completely
    change the list of classes at run time, so prediction could be difficult.

        "cls0" - the subordinate Namespace for Alpha
        "cls0.cls" - the option containing the class Alpha itself
        "cls0.cls_instance" - an instance of the class Alpha
        "cls1" - the subordinate Namespace for Beta
        "cls1.cls" - the option containing the class Beta itself
        "cls1.cls_instance" - an instance of the class Beta

    parameters:
        template_for_namespace - a template for the names of the namespaces
                                 that will contain the classes and their
                                 associated required config options.  The
                                 namespaces will be numbered sequentially.  By
                                 default, they will be "cls1", "cls2", etc.
        class_option_name - the name to be used for the class option within
                            the nested namespace.  By default, it will choose:
                            "cls1.cls", "cls2.cls", etc.
        instantiate_classes - a boolean to determine if there should be an
                              aggregator added to each namespace that
                              instantiates each class.  If True, then each
                              Namespace will contain elements for the class, as
                              well as an aggregator that will instantiate the
                              class.
                              """

    # these are only used within this method.  No need to pollute the module
    # scope with them and avoid potential circular imports
    from configman.namespace import Namespace
    from configman.required_config import RequiredConfig

    #--------------------------------------------------------------------------
    def class_list_converter(class_list_str):
        """This function becomes the actual converter used by configman to
        take a string and convert it into the nested sequence of Namespaces,
        one for each class in the list.  It does this by creating a proxy
        class stuffed with its own 'required_config' that's dynamically
        generated."""
        if isinstance(class_list_str, basestring):
            class_list = [x.strip() for x in class_list_str.split(',')]
            if class_list == ['']:
                class_list = []
        else:
            raise TypeError('must be derivative of a basestring')

        #======================================================================
        class InnerClassList(RequiredConfig):
            """This nested class is a proxy list for the classes.  It collects
            all the config requirements for the listed classes and places them
            each into their own Namespace.
            """
            # we're dynamically creating a class here.  The following block of
            # code is actually adding class level attributes to this new class
            required_config = Namespace()  # 1st requirement for configman
            subordinate_namespace_names = []  # to help the programmer know
                                              # what Namespaces we added
            namespace_template = template_for_namespace  # save the template
                                                         # for future reference
            class_option_name = name_of_class_option  # save the class's option
                                                      # name for the future
            # for each class in the class list
            for namespace_index, a_class in enumerate(class_list):
                # figure out the Namespace name
                namespace_name = template_for_namespace % namespace_index
                subordinate_namespace_names.append(namespace_name)
                # create the new Namespace
                required_config[namespace_name] = Namespace()
                # add the option for the class itself
                required_config[namespace_name].add_option(
                    name_of_class_option,
                    #doc=a_class.__doc__  # not helpful if too verbose
                    default=a_class,
                    from_string_converter=class_converter
                )
                if instantiate_classes:
                    # add an aggregator to instantiate the class
                    required_config[namespace_name].add_aggregation(
                        "%s_instance" % name_of_class_option,
                        lambda c, lc, a: lc[name_of_class_option](lc)
                    )

            @classmethod
            def to_str(cls):
                """this method takes this inner class object and turns it back
                into the original string of classnames.  This is used
                primarily as for the output of the 'help' option"""
                return ', '.join(
                    py_obj_to_str(v[name_of_class_option].value)
                    for v in cls.get_required_config().values()
                    if isinstance(v, Namespace)
                )

        return InnerClassList  # result of class_list_converter
    return class_list_converter  # result of classes_in_namespaces_converter

# for backward compatibility
classes_in_namespaces_converter = str_to_classes_in_namespaces


#------------------------------------------------------------------------------
def str_to_regular_expression(input_str):
    return re.compile(input_str)

regex_converter = str_to_regular_expression  # for backward compatibility

compiled_regexp_type = type(re.compile(r'x'))


#------------------------------------------------------------------------------
def str_to_list(
    input_str,
    item_converter=lambda x: x,
    item_separator=',',
    list_to_collection_converter=None,
):
    """ a conversion function for list
    """
    if not isinstance(input_str, basestring):
        raise ValueError(input_str)
    input_str = str_quote_stripper(input_str)
    result = [
        item_converter(x.strip())
        for x in input_str.split(item_separator) if x.strip()
    ]
    if list_to_collection_converter is not None:
        return list_to_collection_converter(result)
    return result

list_converter = str_to_list  # for backward compatibility


#------------------------------------------------------------------------------
#
#   To string section
#
#------------------------------------------------------------------------------

#------------------------------------------------------------------------------
str_to_instance_of_type_converters = {
    int: int,
    float: float,
    str: str,
    unicode: unicode,
    bool: boolean_converter,
    dict: json.loads,
    list: list_converter,
    datetime.datetime: datetime_converter,
    datetime.date: date_converter,
    datetime.timedelta: timedelta_converter,
    type: class_converter,
    types.FunctionType: class_converter,
    compiled_regexp_type: regex_converter,
}

 # backward compatibility
from_string_converters = str_to_instance_of_type_converters


#------------------------------------------------------------------------------
def arbitrary_object_to_string(a_thing):
    """take a python object of some sort, and convert it into a human readable
    string.  this function is used extensively to convert things like "subject"
    into "subject_key, function -> function_key, etc."""
    # is it None?
    if a_thing is None:
        return ''
    # is it already a string?
    if isinstance(a_thing, basestring):
        return a_thing
    # does it have a to_str function?
    try:
        return a_thing.to_str()
    except (AttributeError, KeyError, TypeError):
        # AttributeError - no to_str function?
        # KeyError - DotDict has no to_str?
        # TypeError - problem converting
        # nope, no to_str function
        pass
    # is this a type proxy?
    try:
        return arbitrary_object_to_string(a_thing.a_type)
    except (AttributeError, KeyError, TypeError):
        #
        # nope, no a_type property
        pass
    # is it a built in?
    try:
        return known_mapping_type_to_str[a_thing]
    except (KeyError, TypeError):
        # nope, not a builtin
        pass
    # is it something from a loaded module?
    try:
        if a_thing.__module__ not in ('__builtin__', 'exceptions'):
            if a_thing.__module__ == "__main__":
                module_name = (
                    sys.modules['__main__']
                    .__file__[:-3]
                    .replace('/', '.')
                    .strip('.')
                )
            else:
                module_name = a_thing.__module__
            return "%s.%s" % (module_name, a_thing.__name__)
    except AttributeError:
        # nope, not one of these
        pass
    # maybe it has a __name__ attribute?
    try:
        return a_thing.__name__
    except AttributeError:
        # nope, not one of these
        pass
    # punt and see what happens if we just cast it to string
    return str(a_thing)


py_obj_to_str = arbitrary_object_to_string  # for backwards compatibility


#------------------------------------------------------------------------------
def list_to_str(a_list, delimiter=', '):
    return delimiter.join(to_str(x) for x in a_list)

#------------------------------------------------------------------------------
known_mapping_type_to_str = dict(
    (val, key) for key, val in __builtin__.__dict__.iteritems()
    if val not in (True, False, list, dict)
)

#------------------------------------------------------------------------------
to_string_converters = {
    int: str,
    float: str,
    str: str,
    unicode: unicode,
    list: list_to_str,
    tuple: list_to_str,
    bool: lambda x: 'True' if x else 'False',
    dict: json.dumps,
    datetime.datetime: datetime_to_ISO_string,
    datetime.date: date_to_ISO_string,
    datetime.timedelta: timedelta_to_str,
    type: arbitrary_object_to_string,
    types.ModuleType: arbitrary_object_to_string,
    types.FunctionType: arbitrary_object_to_string,
    compiled_regexp_type: lambda x: x.pattern,
}


#------------------------------------------------------------------------------
def to_str(a_thing):
    try:
        return to_string_converters[type(a_thing)](a_thing)
    except KeyError:
        return arbitrary_object_to_string(a_thing)

#------------------------------------------------------------------------------
converters_requiring_quotes = [eval, regex_converter]
