####################################################################################################
#
# Musica - A Music Theory Package for Python
# Copyright (C) 2017 Fabrice Salvaire
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
# Fixme:
#   improve formater
#   code dumper, register class
####################################################################################################
import logging
import types
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
__all__ = [
    'XmlObjectifierFactory',
    'XmlObjectifierLeaf',
    'XmlObjectifierNode',
    ]
####################################################################################################
class SourceCode:
    ##############################################
    def __init__(self, indentation=4):
        self._buffer = ''
        self._indentation = indentation
        self._level = 0
    ##############################################
    def increment_level(self):
        self._level += 1
    def decrement_level(self):
        self._level -= 1
    @property
    def indentation(self):
        return ' '*(self._indentation * self._level)
    ##############################################
    def __iter__(self):
        return iter(self._buffer.split('\n'))
    ##############################################
    def __str__(self):
        return self._buffer
    ##############################################
    def __iadd__(self, codes):
        self._buffer += codes
        return self
    ##############################################
    def append_line(self, codes):
        self._buffer += self.indentation + codes + '\n'
    ##############################################
    def append_lines(self, codes):
        for line in codes.split('\n'):
            self.append_line(line)
    ##############################################
    def close_block(self, decrement=False):
        if decrement:
            self.decrement_level()
        self.append_line(')')
####################################################################################################
class XmlObjectifierMetaclass(type):
    __classes__ = {}
    _logger = _module_logger.getChild('XmlObjectifierMetaclass')
    ##############################################
    def __new__(meta_cls, class_name, base_classes, namespace):
        print(meta_cls, class_name, base_classes, namespace)
        cls = super().__new__(meta_cls, class_name, base_classes, namespace)
        meta_cls.register(cls)
        return cls
    ##############################################
    def __init__(cls, class_name, base_classes, namespace):
        print(cls, class_name, base_classes, namespace)
        type.__init__(cls, class_name, base_classes, namespace)
    ##############################################
    @classmethod
    def register(meta_cls, cls):
        class_name = cls.__name__
        XmlObjectifierMetaclass._logger.info('Register {} for {}'.format(cls, class_name))
        meta_cls.__classes__[class_name] = cls
    ##############################################
    @classmethod
    def get(meta_cls, name):
        return meta_cls.__classes__[name]
####################################################################################################
class XmlObjectifierAbc(metaclass=XmlObjectifierMetaclass):
    _logger = _module_logger.getChild('XmlObjectifierAbc')
    __register_schema__ = False
    ##############################################
    @staticmethod
    def populate_class(namespace):
        namespace['__register_schema__'] = True
        # namespace['__id__'] = 0
        # we must instantiate here so as to update the class and not the base class
        namespace['__attribute_names__'] = set()
        namespace['__attribute_map__'] = {}
    ##############################################
    @classmethod
    def get_id(cls):
        if not hasattr(cls, '__id__'):
            cls.__id__ = 0
        _id = cls.__id__
        cls.__id__ += 1
        # name = cls.__name__.lower()
        name = cls.pythonify_name(cls.__tag__) # Fixme: cache
        return '{}_{}'.format(name, _id)
    ##############################################
    @classmethod
    def register_attribute(cls, name):
        py_name = cls.pythonify_name(name)
        cls.__attribute_names__.add(py_name)
        cls.__attribute_map__[name] = py_name
        cls.__attribute_map__[py_name] = name
        return py_name
    ##############################################
    @classmethod
    def class_to_python(cls):
        class_name = cls.__name__
        base_class = cls.__mro__[1].__name__
        py_code = SourceCode()
        py_code.append_line('class {}({}):'.format(class_name, base_class))
        py_code.increment_level()
        py_code.append_line("__tag__ = '{}'".format(cls.__tag__))
        if cls.__attribute_names__:
            py_code.append_line('__attribute_names__ = (') # python name
            py_code.increment_level()
            for name in sorted(cls.__attribute_names__):
                py_code.append_line("'{}',".format(name))
            py_code.decrement_level()
            py_code.append_line(')')
        attribute_map = [(name, value)
                         for name, value in sorted(cls.__attribute_map__.items(), key=lambda x: x[0])
                         if name != value and name not in cls.__attribute_names__]
        if attribute_map:
            py_code.append_line('__attribute_map__ = {')
            py_code.increment_level()
            for name, value in attribute_map:
                py_code.append_line("'{}':'{}',".format(name, value))
            py_code.decrement_level()
            py_code.append_line('}')
        return py_code
    ##############################################
    @staticmethod
    def pythonify_name(name):
        return name.replace('-', '_')
    ##############################################
    @staticmethod
    def to_python_value(value):
        if isinstance(value, str):
            try:
                if '.' in value or 'e' in value.lower():
                    return float(value)
                else:
                    return int(value)
            except ValueError:
                return value
        else:
            return value
    ##############################################
    @staticmethod
    def value_to_python_code(value):
        py_code = str(value)
        if not isinstance(value, (int, float)):
            py_code = "'" + py_code + "'"
        return py_code
    ##############################################
    def __init__(self, **kwargs):
        self._id = self.get_id()
        for name, value in kwargs.items():
            self.set_attribute(name, value)
    ##############################################
    @property
    def class_name(self):
        return self.__class__.__name__
    @property
    def instance_id(self):
        return self._id
    ##############################################
    def __repr__(self):
        return '{} {}'.format(self.__class__.__name__, self.instance_id)
    ##############################################
    def set_attribute(self, name, value):
        if self.__register_schema__:
            py_name = self.register_attribute(name)
            value = self.to_python_value(value)
            self._logger.info("{} {} = {} {}".format(self.__class__.__name__, name, value, type(value)))
        else:
            py_name = name
        setattr(self, py_name, value)
    ##############################################
    def attributes(self):
        _attrib = {name:getattr(self, name, None)
                  for name in self.__attribute_names__
                  if hasattr(self, name)}
        return {name:value for name, value in _attrib.items() if value is not None}
    ##############################################
    def to_dom(self):
        attributes = {self.__attribute_map__[name]:str(value)
                      for name, value in self.attributes().items()}
        return Element(self.__tag__, attributes)
    ##############################################
    def to_xml(self):
        return ElementTree.tostring(self.to_dom(), encoding='utf-8')
    ##############################################
    def attributes_to_python(self):
        kwarg = self.attributes()
        kwarg_string = ', '.join('{}={}'.format(name, self.value_to_python_code(value))
                                 for name, value in sorted(kwarg.items(), key=lambda x: x[0]))
        return kwarg_string
    ##############################################
    def depth_level(self):
        return 0
    ##############################################
    # def to_python(self, anonymous=False):
    #
    #     py_code = '{}({})'.format(self.class_name, self.attributes_to_python())
    #     if anonymous:
    #         return py_code
    #     else:
    #         return '{} = {}'.format(self.instance_id, py_code)
####################################################################################################
[docs]class XmlObjectifierNode(XmlObjectifierAbc):
    _logger = _module_logger.getChild('XmlObjectifierNode')
    __is_root__ = False
    ##############################################
[docs]    @staticmethod
    def populate_class(namespace):
        XmlObjectifierAbc.populate_class(namespace)
        # namespace['__is_root__'] = False
        namespace['__child_names__'] = set() 
    ##############################################
[docs]    @classmethod
    def register_child(cls, name):
        if name not in cls.__child_names__:
            cls.__child_names__.add(name)
            # cls._add_getter(name)
            # Fixme: getter name
            setattr(cls, name, property(lambda self: self._get_child_by_name(name))) 
    ##############################################
[docs]    @classmethod
    def is_container(cls):
        return cls.__child_names__ and not cls.__attribute_names__ 
    ##############################################
    # @classmethod
    # def _add_getter(cls, name):
    ##############################################
[docs]    @classmethod
    def class_to_python(cls):
        py_code = super().class_to_python()
        if cls.__is_root__:
            py_code.append_line('__is_root__ = True')
        py_code.append_line('__child_names__ = (')
        py_code.increment_level()
        for name in sorted(cls.__child_names__):
            py_code.append_line("'{}',".format(name))
        py_code.decrement_level()
        py_code.append_line(')')
        return py_code 
    ##############################################
    def __init__(self, *args, **kwargs):
        super().__init__(**kwargs)
        self._childs = []
        self._child_map = {name:[] for name in self.__child_names__}
        for child in args:
            self.append(child)
        for child in kwargs.get('childs', ()):
            self.append(child)
    ##############################################
[docs]    def __iter__(self):
        return iter(self._childs) 
    ##############################################
[docs]    def _get_child_by_name(self, name):
        return self._child_map[name] 
    ##############################################
[docs]    def _append_child(self, child):
        self._childs.append(child)
        name = child.__class__.__name__
        if name not in self._child_map:
            self._child_map[name] = []
        self._child_map[name].append(child)
        if self.__register_schema__:
            self.register_child(name) 
    ##############################################
[docs]    def append(self, *childs):
        for child in childs:
            self._append_child(child) 
    ##############################################
[docs]    def childs_are_leaf(self):
        for cls_name in self._child_map.keys():
            if issubclass(XmlObjectifierMetaclass.get(cls_name), XmlObjectifierNode):
                return False
        return True 
    ##############################################
[docs]    def depth_level(self):
        depth_level = 0
        for child in self._childs:
            depth_level = max(depth_level, child.depth_level() +1)
        return depth_level 
    ##############################################
[docs]    def to_dom(self):
        element = super().to_dom()
        for child in self._childs:
            element.append(child.to_dom())
        return element 
    ##############################################
[docs]    def to_python(self, anonymous=False):
        # py_code = super().to_python()
        py_code = SourceCode()
        if not anonymous:
            py_code += self.instance_id + ' = '
        py_code += self.class_name + '('
        attributes = self.attributes_to_python()
        if attributes:
            py_code += attributes
        if self.depth_level() < 4: # self.childs_are_leaf()
            if attributes:
                py_code += ','
            py_code += '\n'
            py_code.increment_level()
            if attributes:  # not self.is_container
                py_code.append_line('childs=(')
                py_code.increment_level()
            for child in self._childs:
                py_code.append_lines(child.to_python(True).rstrip() + ',')
            if attributes:
                py_code.close_block(decrement=True)
            py_code.close_block(decrement=True)
        else:
            py_code.close_block()
            template = '{}.append({})\n'
            for child in self._childs:
                if isinstance(child, XmlObjectifierLeaf):
                    py_code += template.format(self.instance_id, child.to_python(True))
                else:
                    py_code += child.to_python()
                    py_code += template.format(self.instance_id, child.instance_id)
        return str(py_code) 
    ##############################################
[docs]    def to_tree(self):
        return ElementTree.ElementTree(self.to_dom()) 
    ##############################################
[docs]    def write_xml(self, path, doctype=None):
        dom = self.to_tree()
        with open(path, 'wb') as fh:
            fh.write('<?xml version="1.0" encoding="UTF-8"?>'.encode('utf-8'))
            if doctype is not None:
                fh.write(doctype.encode('utf-8'))
            dom.write(fh, encoding='utf-8', xml_declaration=False)  
        # dom.write(path, encoding='utf-8', xml_declaration=True)
####################################################################################################
[docs]class XmlObjectifierLeaf(XmlObjectifierAbc):
    _logger = _module_logger.getChild('XmlObjectifierLeaf')
    ##############################################
[docs]    @staticmethod
    def populate_class(namespace):
        XmlObjectifierAbc.populate_class(namespace) 
    ##############################################
    def __init__(self, text=None, **kwargs):
        super().__init__(**kwargs)
        self._text = text
    ##############################################
    @property
    def text(self):
        return self._text
    @text.setter
    def text(self, value):
        self._text = value
    ##############################################
[docs]    def to_dom(self):
        element = super().to_dom()
        if self._text is not None:
            text = str(self._text)
            if text:
                element.text = text
        return element 
    ##############################################
[docs]    def to_python(self, anonymous=False):
        # py_code = super().to_python()
        args = []
        if self._text is not None:
            text = str(self._text)
            if not isinstance(self._text, (int, float)):
                text = "'" + text + "'"
            # py_code += '{}.text = {}\n'.format(self.instance_id, text)
            args.append(text)
        attributes = self.attributes_to_python()
        if attributes:
            args.append(attributes)
        py_code = '{}({})'.format(self.class_name, ', '.join(args))
        if anonymous:
            return py_code
        else:
            return '{} = {}\n'.format(self.instance_id, py_code)  
####################################################################################################
[docs]class XmlObjectifierFactory:
    _logger = _module_logger.getChild('XmlObjectifierFactory')
    ##############################################
    def __init__(self, doctype=None, node_hints=()):
        self._doctype = doctype
        self._node_hints = tuple(node_hints)
        self._classes = {}
        self._root_cls = None
    ##############################################
[docs]    def _get_classe(self, element):
        name = element.tag
        class_name = ''.join([part.title() for part in name.split('-')])
        if class_name not in self._classes:
            is_node = len(element) > 0
            cls = self._build_class(class_name, name, is_node)
        else:
            cls = self._classes[class_name]
        return cls 
    ##############################################
[docs]    def _build_class(self, class_name, name, is_node):
        # Fixme:
        # if element.text is not None:
        if is_node or class_name in self._node_hints:
            base_class = XmlObjectifierNode
        else:
            base_class = XmlObjectifierLeaf
        self._logger.info('Create class {} {}'.format(class_name, base_class))
        cls = types.new_class(
            class_name,
            bases=(base_class,),
            kwds=None,
            exec_body=XmlObjectifierNode.populate_class
        )
        cls.__tag__ = name
        self._classes[class_name] = cls
        if self._root_cls is None:
            self._root_cls = cls
            cls.__is_root__ = True
        return cls 
    ##############################################
[docs]    def _cast_to_node(self, py_element):
        # Fixme: hack !
        #  old element are not recasted !!!
        cls = py_element.__class__
        class_name = cls.__name__
        self._logger.warning('{} must be reacasted to Node'.format(class_name))
        del self._classes[class_name]
        new_cls = self._build_class(class_name, cls.__tag__, True)
        new_cls.__attribute_names__ = cls.__attribute_names__
        new_cls.__attribute_map__ = cls.__attribute_map__
        return new_cls(** py_element.attributes()) 
    ##############################################
[docs]    def __getitem__(self, element):
        return self._get_classe(element) 
    ##############################################
[docs]    def __iter__(self):
        return iter(self._classes.values()) 
    ##############################################
[docs]    def parse(self, file_path):
        tree = ElementTree.parse(file_path)
        root = tree.getroot()
        return self._process_element(root) 
    ##############################################
[docs]    def _process_element(self, element):
        self._logger.info('{0.tag} {0.text}'.format(element))
        element_cls = self[element]
        py_element = element_cls()
        for name, value in element.attrib.items():
            py_element.set_attribute(name, value)
        if element.text is not None:
            py_element.text = XmlObjectifierAbc.to_python_value(element.text)
        for subelement in element:
            py_subelement = self._process_element(subelement)
            if not hasattr(py_element, 'append'):
                py_element = self._cast_to_node(py_element)
            py_element.append(py_subelement)
        return py_element