#-*- coding: utf-8 -*-

import lxml
import os
from lxml import etree

from lodel.context import LodelContext
LodelContext.expose_modules(globals(), {
    'lodel.editorial_model.model': ['EditorialModel'],
    'lodel.editorial_model.components': ['EmComponent', 'EmClass', 'EmField',
        'EmGroup'],
    'lodel.utils.mlstring': ['MlString']})

##@package lodel.editorial_model.translator.xmlfile Translator module designed
#to load & save EM in XML
#
# Structure of a xml file which represents an editorial model:
# <ul>
# <li>\<name\>: name of the model, field <b><em>name</em></b>  in class <b><em>EditorialModel</em></b> 
# <li>\<description\>: field <b><em>description</em></b>  of a composed element, one for each language translation named 
#               <ul><li>\<fre\> for french, 
#               <li>\<eng\> for english,
#               <li>\<esp\> for spanish,
#               <li>\<ger\> for german</ul>
# <li>\<classes\>: set of all <b><em>EmClass</em></b> in the model \n
#    for each classe: \n
#       \<class\><ul>
#       <li>\<uid\>the class's id
#       <li>\<display_name\> The name of the class, field <b><em>display_name</em></b>  of the <b><em>EmClass</em></b> , in different languages if they're available :
#               <ul><li>\<fre\> for french, 
#               <li>\<eng\> for english,
#               <li>\<esp\> for spanish,
#               <li>\<ger> for german</ul>
#       <li>\<help_text\> Short explanation of the class's purpose, in different languages, as above
#       <li>\<abstract\> True or False, field <b><em>abstract</em></b> of the <b><em>EmClass</em></b> 
#       <li>\<pure_abstract\> True or False, field <b><em>pure_bastract</em></b> of the <b><em>EmClass</em></b>
#       <li>\<group\><b><em>uid</em></b> of the group of the field <b><em>group</em></b> of the <b><em>EmClass</em></b>
#       <li>\<fields\>: set of all the <b><em>EmField</em></b> of the <b><em>EmClass</em></b>\n
#         for each field: \n
#           \<field\>
#               <ul><li>\<uid\> uid of the <b><em>EmField</em></b>
#               <li>\<display_name\> field <b><em>display_name</em></b>  of the <b><em>EmField</em></b>, in different languages, as above
#               <li>\<help_text\> Short explanation of the class's purpose, in different languages, as above
#               <li>\<group\><b><em>uid</em></b> of the group of the field <b><em>group</em></b> of the <b><em>EmClass</em></b>
#               <li>\<datahandler_name\> field <b><em>datahandler_name</em></b> of the Emfield, the name of a datahandler
#               <li>\<datahandler_options\>, a list of xml items, each of them named with an option name and contains its value</ul></ul>
# <li>\<groups\>: set of all the groups <b><em>EmGroup</em></b> in the model\n
#    for each group:\n
#       <ul><li>\<uid\> uid of the <b><em>EmField</em></b>
#       <li>\<display_name\> field <b><em>display_name</em></b>  of the <b><em>EmField</em></b>, in different languages, as above
#       <li>\<help_text\> Short explanation of the class's purpose, in different languages, as above
#       <li>\<requires\> all uids of the <b><em>EmGroups</em></b> required by this group and which are in the fields <b><em>require</em></b>
#       <li>\<components\> Set of all components of the <b><em>EmGroups</em></b>, representation of the field <b><em>__components</em></b> \n
#         this item is splitted in two parts :\
#           <ul><li>\<emfields\> all the emfields with, for each of them:\n
#               \<emfield\> \n
#                  <ul><li> \<uid\> <b><em>uid</em></b> of the <b><em>EmField</em></b></ul>
#           <li>\<emclasses\> all the emclasses with, for each of them:\n
#               \<emclass\> \n
#                   <ul><li> \<uid\> <b><em>uid</em></b> of the <b><em>EmClass</em></b></ul></ul></ul>






##@brief Saves a model in a xml file
# @param model EditorialModel : the model to save
# @param filename str|None : if None display on stdout else writes in the file filename

def save(model, **kwargs):
    Em = etree.Element("editorial_model")
    em_name = etree.SubElement(Em, 'name')
    write_mlstring_xml(etree, em_name, model.name)
    
    em_description = etree.SubElement(Em, 'description')
    write_mlstring_xml(etree, em_description, model.description)
    
    em_classes = etree.SubElement(Em, 'classes')
    classes = model.all_classes()
    for emclass in classes:
        write_emclass_xml(etree, em_classes, classes[emclass].uid, classes[emclass].display_name, 
                          classes[emclass].help_text, classes[emclass].group, 
                          classes[emclass].fields(no_parents=True), classes[emclass].parents, 
                          classes[emclass].abstract, classes[emclass].pure_abstract)

    em_groups = etree.SubElement(Em, 'groups')
    groups = model.all_groups()
    for group in groups:
        requires = groups[group].dependencies()
        write_emgroup_xml(etree, em_groups, groups[group].uid, groups[group].display_name, groups[group].help_text, 
                          list(requires.keys()), groups[group].components())
        
    emodel = etree.tostring(Em, encoding='utf-8',  xml_declaration=True, method='xml', pretty_print= True)
    if len(kwargs) == 0:
        print(emodel.decode())
    else:
        outfile = open(kwargs['filename'], "w")
        outfile.write(emodel.decode())
        outfile.close()
    
    
##@brief Writes a representation of a MlString in xml
# @param etree : the xml object
# @param elem : the element which represents a MlString
# @param mlstr : the mlstr to write
def write_mlstring_xml(etree, elem, mlstr):
    for lang in mlstr.values:
        ss_mlstr = etree.SubElement(elem,lang)
        ss_mlstr.text = mlstr.get(lang)
        
##@brief Writes the definition of a datahandler in xml
# @param etree : the xml object
# @param elem : the element which defines a datahandler
# @param dhdl_name : the name of the datahandler
# @param kwargs : the options of the datahandler
def write_datahandler_xml(etree, elem, dhdl_name, **kwargs):
    dhdl = etree.SubElement(elem,'datahandler_name')
    dhdl.text = dhdl_name
    dhdl_opt = etree.SubElement(elem, 'datahandler_options')

    for argname, argval in kwargs.items():
        arg = etree.SubElement(dhdl_opt, argname)
        opt_val=''
        if (isinstance(argval, str)):
            opt_val=argval
        elif (isinstance(argval, bool)):
            opt_val = str(argval)
        elif (isinstance(argval, list) | isinstance(argval, tuple) | isinstance(argval, dict)):
            for argu in argval:
                if len(opt_val) > 0:
                    opt_val = opt_val + ','
                if isinstance(argu, EmComponent):
                    opt_val = opt_val + argu.uid
                elif isinstance(argu, str):
                    opt_val = opt_val + argu
                else:
                    opt_val = str(argu)
        arg.text = opt_val
        
##@brief Writes a representation in xml of a EmField
# @param etree : the xml object
# @param elem : the element for the EmField
# @param uid : the uid of the EmField
# @param name : the name of the field
# @param help_text : explanations of the EmField
# @param group_uid : the uid of a group, can be None
# @datahandler_name
# @**kwargs : options of the datahandler
def write_emfield_xml(etree, elem, uid, name, help_text, group, datahandler_name, **kwargs):
    emfield = etree.SubElement(elem,'field')
    emfield_uid = etree.SubElement(emfield, 'uid')
    emfield_uid.text = uid
    emfield_name = etree.SubElement(emfield, 'display_name')
    if name is None:
        pass
    else:
        write_mlstring_xml(etree, emfield_name, name)
    emfield_help = etree.SubElement(emfield, 'help_text')
    if help_text is None:
        pass
    else:
        write_mlstring_xml(etree, emfield_help, help_text)
    emfield_group = etree.SubElement(emfield, 'group')

    if group is not None:
        emfield_group.text = group.uid #write_emgroup_xml(etree, emfield_group, group.uid, group.display_name, group.help_text, group.requires)
    write_datahandler_xml(etree,emfield,datahandler_name, **kwargs)

##@brief Writes a representation of a EmGroup in xml
# @param etree : the xml object
# @param elem : the element for the EmGroup
# @param name  : the name of the group
# @param help_text : explanations of the EmGroup
# @param requires : a list of the group's uids whose this group depends
def write_emgroup_xml(etree, elem, uid, name, help_text, requires, components):
    emgroup = etree.SubElement(elem, 'group')
    emgroup_uid = etree.SubElement(emgroup, 'uid')
    emgroup_uid.text = uid
    emgroup_name = etree.SubElement(emgroup, 'display_name')
    if name is None:
        pass
    else:
        write_mlstring_xml(etree, emgroup_name, name)
    emgroup_help = etree.SubElement(emgroup, 'help_text')
    if help_text is None:
        pass
    else:
        write_mlstring_xml(etree, emgroup_help, help_text)
    emgroup_requires = etree.SubElement(emgroup, 'requires')
    emgroup_requires.text = ",".join(requires)
    emgroup_comp = etree.SubElement(emgroup, 'components')
    emgroup_comp_cls = etree.SubElement(emgroup_comp, 'emclasses')
    emgroup_comp_fld = etree.SubElement(emgroup_comp, 'emfields')
    
    for component in components:
        if isinstance(component, EmField):
            emgroup_comp_fld_ins = etree.SubElement(emgroup_comp_fld, 'emfield')
            em_group_comp_fld_ins_uid =  etree.SubElement(emgroup_comp_fld_ins,'uid')
            em_group_comp_fld_ins_uid.text = component.uid
            em_group_comp_fld_ins_cls = etree.SubElement(emgroup_comp_fld_ins,'class')
            em_group_comp_fld_ins_cls.text = component.get_emclass_uid()
        elif isinstance(component, EmClass):
            em_group_comp_cls_ins = etree.SubElement(emgroup_comp_cls, 'emclass')
            em_group_comp_cls_ins.text = component.uid

##@brief Writes a representation of a EmClass in xml
# @param etree : the xml object
# @param elem : the element for the EmClass
# @param name  : the name of the group
# @param help_text : explanations of the EmClass
# @param fields : a dict
# @param parents : a list of EmClass uids
# @param abstract : a boolean
# @param pure_abstract : a boolean
def write_emclass_xml(etree, elem, uid, name, help_text, group, fields, parents, abstract = False, pure_abstract = False):
    emclass = etree.SubElement(elem, 'class')
    emclass_uid  = etree.SubElement(emclass, 'uid')
    emclass_uid.text = uid
    emclass_name = etree.SubElement(emclass, 'display_name')
    if name is None:
        pass
    else:
        write_mlstring_xml(etree, emclass_name, name)
    emclass_help = etree.SubElement(emclass, 'help_text')
    if help_text is None:
        pass
    else:
        write_mlstring_xml(etree, emclass_help, help_text)
    emclass_abstract = etree.SubElement(emclass, 'abstract')
    emclass_abstract.text ="True" if abstract else "False"
    emclass_pure_abstract = etree.SubElement(emclass, 'pure_abstract')
    emclass_pure_abstract.text = "True" if pure_abstract else "False"
    emclass_group = etree.SubElement(emclass, 'group')
    if group is not None:
        emclass_group.text = group.uid
    emclass_fields = etree.SubElement(emclass, 'fields')
    for field in fields:
        write_emfield_xml(etree, emclass_fields, field.uid, field.display_name, field.help_text, 
                          field.group,field.data_handler_name, **field.data_handler_options)
    parents_list=list()
    for parent in parents:
        parents_list.append(parent.uid)
    emclass_parents = etree.SubElement(emclass, 'parents')
    emclass_parents.text = ",".join(parents_list)

##@brief Loads a model from a xml file
# @param model EditorialModel : the model to load
# @return a new EditorialModel object
def load(filename):

    Em = etree.parse(filename)
    emodel = Em.getroot()
    name = emodel.find('name')
    description = emodel.find('description')

    model = EditorialModel(load_mlstring_xml(name), load_mlstring_xml(description))
        
    classes = emodel.find('classes')
    for emclass in classes:
        em_class = load_class_xml(model, emclass)
        if em_class.uid not in model.all_classes():
            model.add_class(em_class)
    
    groups = emodel.find('groups')
    i = 0
    for group in groups:
        grp = load_group_xml(model, group)
        if grp.uid not in model.all_groups():
            grp = model.add_group(grp)
    return model

##@brief Creates a EmClass from a xml description
# @param elem : the element which represents the EmClass
# @param model  : the model which will contain the new class
# @return a new EmClass object
def load_class_xml(model, elem):
    uid = elem.find('uid').text
    if elem.find('display_name').text is None:
        name = None
    else:
        name = load_mlstring_xml(elem.find('display_name'))
    if elem.find('help_text').text is None:
        help_text = None
    else:
        help_text = load_mlstring_xml(elem.find('help_text'))
        
    abstract = (elem.find('abstract').text == 'True')
    pure_abstract = (elem.find('pure_abstract').text == 'True')
    requires = list()
    classes = model.all_classes()
    req = elem.find('parents')
    if req.text is not None:
        l_req = req.text.split(',')
        for r in l_req:
            if r in classes:
                requires.append(model.all_classes_ref(r))
            else:
                requires.append(model.add_class(EmClass(r)))
    group = elem.find('group')
    if group.text is not None:
        if group.text in model.all_groups():
            grp = model.all_groups_ref(group.text)
        else:
            grp = model.add_group(EmGroup(group.text))
    else:
        grp = None

    if uid in classes:
        emclass = model.all_classes_ref(uid)
        emclass.display_name = name
        emclass.help_text = help_text
        emclass.parents=requires
        emclass.group = grp
        emclass.abstract = abstract
        emclass.pure_abstract = pure_abstract
    else:
        emclass = EmClass(uid, name, help_text, abstract,requires, grp, pure_abstract)
        model.add_class(emclass)
        
    fields = elem.find('fields')
    for field in fields:
        emfield = load_field_xml(model, field, emclass)
        l_emfields = emclass.fields()
        incls = False
        for emf in l_emfields:
            if emfield.uid == emf.uid:
                incls = True
                break
        if not incls:
            emclass.add_field(emfield)
            
    return emclass
    
##@brief Creates a EmField from a xml description
#@param elem : the element which represents the EmField
#@param model  : the model which will contain the new field
#@param emclass EmClass : the EmClass of the field
#@return a new EmField object
def load_field_xml(model, elem, emclass):
    uid = elem.find('uid').text
    if elem.find('display_name').text is None:
        name = None
    else:
        name = load_mlstring_xml(elem.find('display_name'))
        
    if elem.find('help_text').text is None:
        help_text = None
    else:
        help_text = load_mlstring_xml(elem.find('help_text'))
        
    emgroup = elem.find('group')
    if emgroup.text is not None:
        if emgroup.text in model.all_groups():
            group = model.all_groups_ref(emgroup.text)
        else:
            group = model.add_group(EmGroup(emgroup.text))
    else:
        group = None
        
    dhdl = elem.find('datahandler_name')
    dhdl_opts = {}
    if dhdl.text is not None:
        dhdl_opts = elem.find('datahandler_options')
        if dhdl_opts is not None:
            dhdl_options = load_dhdl_options_xml(model, dhdl_opts) 
    emfield = EmField(
        uid, dhdl.text, emclass, name, help_text, group, **dhdl_options)
    
    return emfield

##@brief Returns datahandler options from a xml description
# @param elem : the element which represents the datahandler
# @param model  : the model which will contain the new field
# @return datahandler options
def load_dhdl_options_xml(model, elem):
    dhdl_options=dict()
    for opt in elem:
        if (opt.tag == 'allowed_classes'):
            classes = list()
            if opt.text is not None:
                clss = opt.text.split(',')
                for classe in clss:
                    if classe in model.all_classes():
                        classes.append(model.all_classes_ref(classe))
                    else:
                        new_cls = model.add_class(EmClass(classe))
                        classes.append(new_cls)
            dhdl_options['allowed_classes'] = classes
        elif (opt.tag == 'back_reference'):
            dhdl_options['back_reference'] = tuple(opt.text.split(','))
        elif ((opt.text == 'True') | (opt.text == 'False')):
            dhdl_options[opt.tag] = (opt.text == 'True')
        else:
            dhdl_options[opt.tag] = opt.text
    return dhdl_options
             
    
##@brief Creates a EmGroup from a xml description
# @param elem : the element which represents the EmGroup
# @param model  : the model which will contain the new group
# @return a new EmGroup object
def load_group_xml(model, elem):
    uid = elem.find('uid')
    
    if elem.find('display_name').text is None:
        name = None
    else:
        name = load_mlstring_xml(elem.find('display_name'))
        
    if elem.find('help_text').text is None:
        help_text = None
    else:
        help_text = load_mlstring_xml(elem.find('help_text'))
        
    requires = list()
    groups = model.all_groups()
    req = elem.find('requires')

    if req.text is not None:
        l_req = req.text.split(',')
        for r in l_req:
            if r in groups:
                requires.append(model.all_groups_ref(r))
            else:
                grp = model.new_group(r)
                requires.append(grp)
                
    comp= list()
    components = elem.find('components')
    fields = components.find('emfields')
    for field in fields:
        fld_uid = field.find('uid').text
        fld_class = field.find('class').text
        fld = model.all_classes_ref(fld_class).fields(fld_uid)
        comp.append(fld)
    classes = components.find('emclasses')
    for classe in classes:
        comp.append(model.all_classes_ref(classe.text))
        
    groups = model.all_groups()
    if uid.text in groups:
        group = model.all_groups_ref(uid.text)
        group.display_name = name
        group.help_text = help_text
        group.add_dependencie(requires)
    else:
        group = EmGroup(uid.text, requires, name, help_text)

    group.add_components(comp)     
    return group

##@brief Constructs a MlString from a xml description
# @param elem : the element which represents the MlString
# @param model  : the model which will contain the new group
# @return a new MlString object
def load_mlstring_xml(elem):
    mlstr = dict()
    for lang in elem:
        mlstr[lang.tag] = lang.text 
    return MlString(mlstr)