from collections import MutableMapping
from xmodule.x_module import XModuleDescriptor
from lxml import etree
import copy
import logging

log = logging.getLogger(__name__)

class LazyLoadingDict(MutableMapping):
    """
    A dictionary object that lazily loads it's contents from a provided
    function on reads (of members that haven't already been set)
    """

    def __init__(self, loader):
        self._contents = {}
        self._loaded = False
        self._loader = loader
        self._deleted = set()

    def __getitem__(self, name):
        if not (self._loaded or name in self._contents or name in self._deleted):
            self.load()

        return self._contents[name]

    def __setitem__(self, name, value):
        self._contents[name] = value
        self._deleted.discard(name)

    def __delitem__(self, name):
        del self._contents[name]
        self._deleted.add(name)

    def __contains__(self, name):
        self.load()
        return name in self._contents

    def __len__(self):
        self.load()
        return len(self._contents)

    def __iter__(self):
        self.load()
        return iter(self._contents)

    def __repr__(self):
        self.load()
        return repr(self._contents)

    def load(self):
        if self._loaded:
            return

        loaded_contents = self._loader()
        loaded_contents.update(self._contents)
        self._contents = loaded_contents
        self._loaded = True


class XmlDescriptor(XModuleDescriptor):
    """
    Mixin class for standardized parsing of from xml
    """

    # Extension to append to filename paths
    filename_extension = 'xml'

    # The attributes will be removed from the definition xml passed
    # to definition_from_xml, and from the xml returned by definition_to_xml
    metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
        'due', 'graded', 'name', 'slug')

    @classmethod
    def definition_from_xml(cls, xml_object, system):
        """
        Return the definition to be passed to the newly created descriptor
        during from_xml

        xml_object: An etree Element
        """
        raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__)

    @classmethod
    def clean_metadata_from_xml(cls, xml_object):
        """
        Remove any attribute named in self.metadata_attributes from the supplied xml_object
        """
        for attr in cls.metadata_attributes:
            if xml_object.get(attr) is not None:
                del xml_object.attrib[attr]

    @classmethod
    def file_to_xml(cls, file_object):
        """
        Used when this module wants to parse a file object to xml
        that will be converted to the definition.

        Returns an lxml Element
        """
        return etree.parse(file_object).getroot()

    @classmethod
    def from_xml(cls, xml_data, system, org=None, course=None):
        """
        Creates an instance of this descriptor from the supplied xml_data.
        This may be overridden by subclasses

        xml_data: A string of xml that will be translated into data and children for
            this module
        system: An XModuleSystem for interacting with external resources
        org and course are optional strings that will be used in the generated modules
            url identifiers
        """
        xml_object = etree.fromstring(xml_data)

        def metadata_loader():
            metadata = {}
            for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'):
                from_xml = xml_object.get(attr)
                if from_xml is not None:
                    metadata[attr] = from_xml

            if xml_object.get('graded') is not None:
                metadata['graded'] = xml_object.get('graded') == 'true'

            if xml_object.get('name') is not None:
                metadata['display_name'] = xml_object.get('name')

            return metadata

        def definition_loader():
            filename = xml_object.get('filename')
            if filename is None:
                definition_xml = copy.deepcopy(xml_object)
            else:
                filepath = cls._format_filepath(xml_object.tag, filename)
                log.debug('filepath=%s, resources_fs=%s' % (filepath,system.resources_fs))
                with system.resources_fs.open(filepath) as file:
                    try:
                        definition_xml = cls.file_to_xml(file)
                    except:
                        log.exception("Failed to parse xml in file %s" % filepath)
                        raise

            cls.clean_metadata_from_xml(definition_xml)
            return cls.definition_from_xml(definition_xml, system)

        return cls(
            system,
            LazyLoadingDict(definition_loader),
            location=['i4x',
                      org,
                      course,
                      xml_object.tag,
                      xml_object.get('slug')],
            metadata=LazyLoadingDict(metadata_loader),
        )

    @classmethod
    def _format_filepath(cls, category, name):
        return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension)

    def export_to_xml(self, resource_fs):
        """
        Returns an xml string representing this module, and all modules underneath it.
        May also write required resources out to resource_fs

        Assumes that modules have single parantage (that no module appears twice in the same course),
        and that it is thus safe to nest modules as xml children as appropriate.

        The returned XML should be able to be parsed back into an identical XModuleDescriptor
        using the from_xml method with the same system, org, and course
        """
        xml_object = self.definition_to_xml(resource_fs)
        self.__class__.clean_metadata_from_xml(xml_object)

        # Put content in a separate file if it's large (has more than 5 descendent tags)
        if len(list(xml_object.iter())) > 5:

            filepath = self.__class__._format_filepath(self.category, self.name)
            resource_fs.makedir(self.category, allow_recreate=True)
            with resource_fs.open(filepath, 'w') as file:
                file.write(etree.tostring(xml_object, pretty_print=True))

            for child in xml_object:
                xml_object.remove(child)

            xml_object.set('filename', self.name)

        xml_object.set('slug', self.name)
        xml_object.tag = self.category

        for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'):
            if attr in self.metadata and attr not in self._inherited_metadata:
                xml_object.set(attr, self.metadata[attr])

        if 'graded' in self.metadata and 'graded' not in self._inherited_metadata:
            xml_object.set('graded', str(self.metadata['graded']).lower())

        if 'display_name' in self.metadata:
            xml_object.set('name', self.metadata['display_name'])

        return etree.tostring(xml_object, pretty_print=True)

    def definition_to_xml(self, resource_fs):
        """
        Return a new etree Element object created from this modules definition.
        """
        raise NotImplementedError("%s does not implement definition_to_xml" % self.__class__.__name__)
