# -*- coding: utf-8 -*-
"""Module for sample XML generation from WSDL/XSD

.. module:: datagen.xmlgen
   :platform: Unix
   :synopsis: Module for sample XML generation from WSDL/XSD
.. moduleauthor:: Petr Rašek <bowman@hydratk.org>

"""

"""
Events:
-------
xmlgen_before_import_spec
xmlgen_after_import_spec
xmlgen_before_write
xmlgen_after_write

"""

from hydratk.core.masterhead import MasterHead
from hydratk.core import event
from suds.client import Client, TypeNotFound
from lxml.etree import Element, SubElement, tostring
from os import path
from re import search
from logging import getLogger, CRITICAL

getLogger('suds.resolver').setLevel(CRITICAL)

''' WSDL template '''
wsdl_tmpl = """
<wsdl:definitions targetNamespace="{0}" xmlns:tns="{1}" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
                  xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
   <wsdl:types>
      <xsd:schema attributeFormDefault="qualified" targetNamespace="{2}">
      <xsd:include schemaLocation="{3}"/>      
      </xsd:schema>
   </wsdl:types>
   <wsdl:portType name="dummyPort">
   </wsdl:portType>
   <wsdl:binding name="dummyBinding" type="tns:dummyPort">
   </wsdl:binding>
   <wsdl:service name="dummy">
      <wsdl:port name="dummyPort" binding="tns:dummyBinding">
         <soap:address location="http://localhost/dummy"/>
      </wsdl:port>
   </wsdl:service>
</wsdl:definitions>
"""


class XMLGen(object):
    """Class XMLGen
    """

    _mh = None
    _path = None
    _client = None

    def __init__(self):
        """Class constructor

        Called when object is initialized

        Args:   
           none           

        """

        self._mh = MasterHead.get_head()

    @property
    def path(self):
        """ path property getter """

        return self._path

    @property
    def client(self):
        """ client property getter """

        return self._client

    def import_spec(self, filename):
        """Method imports specification

        Args:
           filename (str): filename

        Returns:
           bool: result

        Raises:
           event: xmlgen_before_import_spec
           event: xmlgen_after_import_spec     

        """

        try:

            self._mh.demsg('htk_on_debug_info', self._mh._trn.msg(
                'datagen_xmlgen_import_spec', filename), self._mh.fromhere())
            ev = event.Event('xmlgen_before_import_spec', filename)
            if (self._mh.fire_event(ev) > 0):
                filename = ev.argv(0)

            if (ev.will_run_default()):
                if (path.exists(filename)):
                    spec_type = (filename.split('.')[-1]).upper()
                    filename = path.join(
                        path.dirname(path.abspath(filename)), filename)

                    if (spec_type == 'WSDL'):
                        self._client = Client('file://' + filename, cache=None)
                        self._path = path.abspath(filename)
                    elif (spec_type == 'XSD'):
                        wsdl = self._create_dummy_wsdl(filename)
                        self._client = Client('file://' + wsdl, cache=None)
                        self._path = wsdl
                    else:
                        raise ValueError(
                            'Unknown specification type: {0}'.format(spec_type))
                else:
                    raise ValueError('File {0} not found'.format(filename))

            self._mh.demsg('htk_on_debug_info', self._mh._trn.msg(
                'datagen_xmlgen_spec_imported'), self._mh.fromhere())
            ev = event.Event('xmlgen_after_import_spec')
            self._mh.fire_event(ev)

            return True

        except (Exception, ValueError) as ex:
            self._mh.demsg(
                'htk_on_error', 'error: {0}'.format(ex), self._mh.fromhere())
            return False

    def toxml(self, root, outfile=None, envelope=False):
        """Method creates sample xml file

        Args:
           root (str): root element name
           outfile (str): output filename, default sample.xml
           envelope (bool): create SOAP envelope 

        Returns:
           bool: result

        Raises:
           event: xmlgen_before_write
           event: xmlgen_after_write    

        """

        try:

            self._mh.demsg('htk_on_debug_info', self._mh._trn.msg(
                'datagen_xmlgen_write_sample'), self._mh.fromhere())
            ev = event.Event('xmlgen_before_write', root, outfile, envelope)
            if (self._mh.fire_event(ev) > 0):
                root = ev.argv(0)
                outfile = ev.argv(1)
                envelope = ev.argv(2)

            if (ev.will_run_default()):
                if (self._client == None):
                    raise ValueError('Specification is not imported yet')

                if (envelope):
                    ns = '{%s}' % 'http://schemas.xmlsoap.org/soap/envelope/'
                    doc = Element(ns + 'Envelope')
                    SubElement(doc, 'Header')
                    body = SubElement(doc, 'Body')
                    body.append(self._toxml_rec(root))
                else:
                    doc = self._toxml_rec(root)

                outfile = 'sample.xml' if (outfile == None) else outfile
                with open(outfile, 'w') as f:
                    f.write(tostring(
                        doc, encoding='UTF-8', xml_declaration=True, pretty_print=True).decode())

            self._mh.demsg('htk_on_debug_info', self._mh._trn.msg(
                'datagen_xmlgen_sample_written', outfile), self._mh.fromhere())
            ev = event.Event('xmlgen_after_write')
            self._mh.fire_event(ev)

            return True

        except (Exception, ValueError) as ex:
            self._mh.demsg(
                'htk_on_error', 'error: {0}'.format(ex), self._mh.fromhere())
            return False

    def _toxml_rec(self, root, obj=None, ns_cur=None):
        """Method creates sample xml document

        It is used in recursive traversal

        Args:
           root (str): root element name
           obj (obj): suds element object 
           ns_cur (str): current namespace

        Returns:
           xml: xml document

        Raises:
           error: ValueError     

        """

        if (self._client == None):
            raise ValueError('Specification is not imported yet')

        try:

            if (obj == None):
                obj = self._client.factory.create(root)

            ns = '{%s}' % self._get_element_ns(obj.__class__.__name__)
            if (ns != '{None}' and ns != ns_cur):
                doc = Element(ns + root)
            else:
                doc = Element(root)
                ns = ns_cur

            for key in obj.__keylist__:
                subelem = obj[key]

                if (subelem == None):
                    SubElement(doc, key).text = '?'
                elif (subelem == [] or '[]' in subelem.__str__()):
                    inner_doc = self._toxml_rec(key, None, ns)
                    if (inner_doc != None):
                        doc.append(inner_doc)
                else:
                    el_type = self._get_element_type(
                        subelem.__class__.__name__)
                    if (el_type == 'Simple'):
                        SubElement(doc, key).text = '?'
                    elif (el_type == 'Complex'):
                        inner_doc = self._toxml_rec(key, subelem, ns)
                        if (inner_doc != None):
                            doc.append(inner_doc)

            return doc

        except TypeNotFound:
            return None

    def _get_element_type(self, element):
        """Method gets element XSD type

        It is used to determine if element is Simple or Complex    

        Args:
           element (str): element name

        Returns:
           str: element type

        Raises:
           error: ValueError         

        """

        if (self._client == None):
            raise ValueError('Specification is not imported yet')

        el_type = None
        for value in self._client.wsdl.schema.types.values():
            if (value.name == element):
                if ('Simple' in value.id):
                    el_type = 'Simple'
                elif ('Complex' in value.id):
                    el_type = 'Complex'
                break

        return el_type

    def _get_element_ns(self, element):
        """Method gets element XSD namespace 

        It is used to construct XML element with correct namespaces

        Args:
           element (str): element name

        Returns:
           str: element namespace    

        Raises:
           error: ValueError    

        """

        if (self._client == None):
            raise ValueError('Specification is not imported yet')

        ns = None
        for key in self._client.wsdl.schema.types.keys():
            if (key[0] == element):
                ns = key[1]
                break

        return ns

    def _create_dummy_wsdl(self, xsd):
        """Method creates dummy WSDL file

        Workaround method:
        Library suds is designed for SOAP and imports WSDL only
        Dummy WSDL imports given XSD and is parsed automatically
        File is stored in same folder as XSD file (with suffix .wsdl)

        Args:
           xsd (str): XSD filename

        Returns:
           str: WSDL filename 

        Raises:
           error: ValueError     

        """

        if (path.exists(xsd)):
            try:
                with open(xsd, 'r') as f:
                    tns = search(r'targetNamespace="(.*)"', f.read()).group(1)
                    if ('"' in tns):
                        tns = tns[: tns.index('"')]

                filename = xsd.split('/')[-1]
                wsdl = path.abspath(xsd)[:-3] + 'wsdl'

                with open(wsdl, 'w') as f:
                    f.write(wsdl_tmpl.format(tns, tns, tns, filename))

                return wsdl
            except AttributeError as ex:
                raise ValueError('File {0} is not valid XSD'.format(xsd))

        else:
            raise ValueError('File {0} not found'.format(xsd))
