import copy, urllib2, json
from contextlib import closing
from urllib2 import urlopen


class ConfigurationSection(dict):
    """ modified dictionary that returns none for invalid keys
    """

    def __init__(self):

        dict.__init__(self)

        self.mutable = True

    def __getitem__(self, key):

        try: return dict.__getitem__(self, key)
        except (IndexError, KeyError) as e:
            raise KeyError("Unknown configuration key '%s'." % (key))

    def __setitem__(self, key, value):

        if self.mutable:
            self.update({ key: value })
        else:
            raise AttributeError("This section is not mutable.")

    def set_mutable(self, mutable):

        self.mutable = mutable

    def set(self, key, value):

        return self.__setitem__(key, value)

    def get(self, key):

        return self.__getitem__(key)

    def get_list(self, key, delimiter = ",", default = []):

        try:
            val = self.get(key)
            l = val.split(delimiter) if len(val) > 0 else default
            return [item.strip() for item in l]
        except: return default

    def get_string(self, key, default = ""):

        try:
            if str(self.get(key)) == '!None': return None
            return str(self.get(key)) or default
        except: return default

    def get_int(self, key, default = None):

        try: return int(self.get(key)) or default
        except: return default

    def get_bool(self, key, default = False):

        try:
            val = self.get(key) or default
            if isinstance(val, bool):
                return val
            elif isinstance(val, str):
                if val.lower() == 'true':
                    self.set(key, True)
                    return True
                elif val.lower() == 'false':
                    self.set(key, False)
                    return False
                else:
                    return default
            elif isinstanct(val, int):
                if val == 1:
                    self.set(key, True)
                    return True
                elif val == 0:
                    self.set(key, False)
                    return False
                else:
                    return default
            else:
                return default
        except: return default


class SectionPromise(object):
    """ this is a configuration section promise
        to make resolution of linked sections post-load
        easier.
    """

    promises = []

    def __init__(self, config, section, key, link):

        self.config = config
        self.section = section
        self.key = key
        self.link = link
        self.__fulfilled = False

        SectionPromise.promises.append(self)

    def __str__(self):
        """ Convert directly to a string for recreating the link
            during config write.  Better for serialization.
        """

        return '@' + self.link

    def resolve(self):

        if self.__fulfilled:
            return

        section = self.config.get_section(self.section)
        link = self.config.get_section(self.link)
        target = section.get(self.key)

        if isinstance(target, list):
            target.remove(self)
            target.append(link)
            section.set(self.key, target)
        else:
            section.set(self.key, link)

        # Preserve the promise for writing back out.
        section.set("_%s_promise" % (self.key), self)
        self.__fulfilled = True


class Configuration(object):
    """
        This definitely needs to be documented.
    """

    def __init__(self):
        """ initialise the container
            store in key:value format withing the certain category
        """

        self.__container = ConfigurationSection()

        self._filename = None

        self.loaded = False

    def __resolve_links(self):
        """ resolves all linked references.
        """

        for promise in SectionPromise.promises:
            promise.resolve()

        SectionPromise.promises = []

    def add_section(self, section_name):
        """ adds a new configuration section to the main dictionary.
        """

        section = ConfigurationSection()
        self.__container.set(section_name, section)

        return section

    def remove_section(self, section_name):
        """ removes a section from the main dictionary.
        """

        del self.__container[section_name]

    @property
    def sections(self):
        """ returns a list of all sections in the configuration.
        """

        return self.__container.keys()

    def has_section(self, section_name):
        """ return if x has a section
        """

        return section_name in self.__container

    def get_section(self, section_name):
        """ return a raw/direct reference to a section
        """

        return self.__container[section_name] if self.__container.__contains__(section_name) else None

    def unload(self):
        """ unload an entire configuration
        """

        self.__container.clear()
        self.loaded = False

    def reload(self):
        """ reload the configuration from the initially specified file
        """

        self.unload()
        self.load(self._filename)

    def save(self, filename = None):

        if filename is None:
            filename = self._filename

        if filename is None:
            raise ValueError('No filename specified and no stored filename.')

        with closing(open(filename, 'w')) as config:
            for section, smap in self.__container.iteritems():
                config.write("[%s]\n" % (section))
                for key, value in smap.iteritems():
                    if isinstance(value, list):
                        value = "+list:" + json.dumps(value)
                    elif isinstance(value, ConfigurationSection):
                        if "_%s_promise" % (key) in smap:
                            value = str(smap["_%s_promise" % (key)])
                        else:
                            value = str(value)
                    elif isinstance(value, SectionPromise):
                        continue
                    elif isinstance(value, file):
                        value = "+file:" + value.name
                    else:
                        value = str(value)
                    config.write("%s = %s\n" % (key, value))
                config.write("\n")

    def load(self, filename):
        """
            load(filename)

            filename -> name of the file to load.
        """

        try:
            fobj = open(filename, 'r')
            self._filename = filename
            self.load_file(fobj)
            fobj.close()
        except IOError as e:
            raise ValueError("Invalid filename '%s'." % (filename))
        except:
            raise

    def load_file(self, fobj):
        """ load a file and read in the categories and variables
        """

        if self.loaded: self.__container.clear()
        section_name = None
        option_key = None
        option_value = None

        for line in fobj.readlines():
            line = line.strip('\n').lstrip()

            if line.startswith('#') or line.startswith('//') or line.startswith(';'):
                continue
            elif line.startswith('[') and line.endswith(']'):
                # This is the beginning of a section tag.
                section_name = line[1:-1]
                if not self.get_section(section_name):
                    self.add_section(section_name)
                continue
            elif '=' in line:
                s = line.split('=', 1)
                l = len(s[0])
                # strip whitespace
                option_key = s[0].strip()
                option_value = s[1].strip() if s[1] is not '' else None

                if option_value and option_value[-1] == ';':
                    option_value = option_value[0:-1]
                section = self.get_section(section_name)

                if not option_value:
                    section.set(option_key, option_value)
                    continue

                if option_value.startswith('+'): # typed reference / variable
                    dobj_type = option_value.split(':')[0][1:]
                    if len(option_value.split(':')) > 2:
                        dobj_value = ':'.join(option_value.split(':')[1:])
                    else:
                        dobj_value = option_value.split(':')[1]

                    if dobj_type.lower() == 'file':
                        try:
                            section.set(option_key, open(dobj_value, 'r'))
                        except:
                            try:
                                section.set(option_key, open(dobj_value, 'w+'))
                            except:
                                section.set(option_key, None)
                    elif dobj_type.lower() == 'url' or dobj_type.lower() == "uri":
                        try:
                            section.set(option_key, urlopen(dobj_value).read())
                        except:
                            raise
                        section.set(option_key, None)
                    elif dobj_type.lower() == 'list':
                        try:
                            dobj_list = json.loads('%s' % (dobj_value))
                        except:
                            dobj_list = []
                        dobj_repl = []
                        for item in dobj_list:
                            if item.startswith('@'):
                                link_name = item[1:]
                                if not self.get_section(link_name):
                                    dobj_repl.append(SectionPromise(self, section_name, option_key, link_name))
                                else:
                                    link = self.get_section(link_name)
                                    dobj_repl.append(link)
                            else: dobj_repl.append(item)
                        section.set(option_key, dobj_repl)
                    else:
                        section.set(option_key, option_value)
                elif option_value.startswith('@'): # section reference
                    link_name = option_value[1:]
                    if not self.get_section(link_name):
                        section.set(option_key, SectionPromise(self, section_name, option_key, link_name))
                    else:
                        link = self.get_section(link_name)
                        section.set(option_key, link)
                else:
                    section.set(option_key, option_value)
                continue
            else:
                continue
        self.__resolve_links()
        self.loaded = True
