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


import json
import importlib.resources as pkg_resources
import importlib
from lxml import etree

from . import schema_dicts
from . import schemas

import collections.abc

import inspect

from copy import copy

import xgbxml.gbxml_functions as gbxml_functions
import xgbxml.xml_functions as xml_functions
import xgbxml.gbxml_xsd_functions as gbxml_xsd_functions

import math
from . import render_functions
from .geometry_functions import vector_normalize_3d, vector_multiplication_3d, vector_addition_3d


def get_parser(version='6.01'):
    """Returns a lxml.etree.XMLParser containing custom elements for gbXML files.
    
    :param version: The `gbxml version string <https://www.gbxml.org/schema_doc/6.01/GreenBuildingXML_Ver6.01.html#Link27C>`_. Default is '6.01'.
    :type version: str
    
    :rtype: lxml.etree.XMLParser

    .. note::
        
      Using this parser means that when lxml is used to read in a gbXML file, 
      the different elements (`gbXML`, `Campus`, `Building` etc.) are instantiated with
      custom classes.
      
      This means that the lxml/xgbxml elements have additional features, which are 
      specifically designed for working with gbXML files.
      
      For example, the `gbXML` element has additional properties such as 
      :code:`version` and :code:`temperatureUnit` for direct access to the
      XML attributes. It also has additional methods, such as :code:`add_Campus`,
      for creating new child elements.
      
      The *xgbxml* parser provides two types of additional properties and methods:

      1. Properties and methods that are automatically generated from the 
         gbXML schema file. This method generates the :code:`version` property and
         the :code:`add_Campus` method for the gbXML element.
      2. Properties and methods which are custom written for the element. 
         This is bespoke code written for a particular element to provide
         additional functionality. An example is the 
         :py:func:`~xgbxml.xgbxml.Surface.get_shell` method of the 
         :py:class:`~xgbxml.xgbxml.Surface` class.

    .. rubric:: Code Example
    
    .. code-block:: python
    
       from lxml import etree
       import xgbxml

       parser=xgbxml.get_parser()  # default is gbXML version 6.01

       tree=etree.parse('my_gbxml_file.xml', parser)
       gbxml=tree.getroot()
    
    """    
    #create lookup
    lookup = etree.ElementNamespaceClassLookup()
    #set namespace
    namespace = lookup.get_namespace('http://www.gbxml.org/schema')
    #set default element
    namespace[None]=gbElement
    
    # load xsd_schema
    xsd_schema_text=pkg_resources.read_text(schemas, 
                                            'GreenBuildingXML_Ver%s.xsd' % version)
    xsd_schema=etree.fromstring(xsd_schema_text.encode())#.getroot()
    
    
    # load schema_dict
    # schema_text = pkg_resources.read_text(schema_dicts, 
    #                                       'schema_dict_%s.json' % version.replace('.','_'))
    # schema_dict=json.loads(schema_text)
    
    # load autogenerated gbElements_X_XX module
    auto_gbElement_module = importlib.import_module('.auto.gbElements_%s' % version.replace('.','_'),
                                                    'xgbxml')
    #print(auto_gbElement_module)
    
    # loop through classes in module
    for k,v in auto_gbElement_module.__dict__.items():
        
        if not k.startswith('__'):
            
            element_name=k[:-5].replace('-','_')
            #print(element_name)
            
            base_classes=[gbElement,v]
            
            # add custom written element class in gbxml directory if it exists
            try:
                kls=globals()[element_name]
                base_classes.append(kls)
            except KeyError:
                pass
            
            # add class for element into namespace
            namespace[element_name.replace('_','-')]=\
                type(element_name,
                     tuple(base_classes),
                     dict(
                         #_class_schema_dict=schema_dict,
                         _xsd_schema=xsd_schema
                         )
                     )
            
    parser = etree.XMLParser()
    parser.set_element_class_lookup(lookup)
    return parser



def create_gbXML(id=None,
                 engine=None,
                 temperatureUnit='C',
                 lengthUnit='Meters',
                 areaUnit='SquareMeters',
                 volumeUnit='CubicMeters',
                 useSIUnitsForResults=True,
                 version='6.01',
                 SurfaceReferenceLocation=None
                 ):
    """Returns a root `gbXML` element for a new, blank gbXML file.
    
    The keyword arguments for this function set the XML attributes of the
    newly created `gbXML` element. `See the gbXML schema for details
    of these attributes.
    <https://www.gbxml.org/schema_doc/6.01/GreenBuildingXML_Ver6.01.html#Link105>`_.
    
    :rtype: xgbxml.xgbxml.gbXML (subclass of lxml.etree._Element)
    
    .. note::
        
        The returned object is a subclass of lxml.Element, not an lxml.ElementTree.
        
        To access the ElementTree of the returned gbXML element, the lxml 
        method :code:`getroottree()` can be used. This is needed to save the 
        gbXML file using the ElementTree :code:`write()` method.
    
    
    .. rubric:: Code Example
    
    .. code-block:: python
    
       import xgbxml
       gbxml=xgbxml.create_gbXML()
       tree=gbxml.getroottree()
       tree.write('new_gbxml_file.xml')
    
    """
    xml='<gbXML version="%s" xmlns="http://www.gbxml.org/schema"></gbXML>' % version
    parser=get_parser(version)
    root=etree.fromstring(xml,parser)
    
    if not id is None: root.id=id
    if not engine is None: root.engine=engine
    root.temperatureUnit=temperatureUnit
    root.lengthUnit=lengthUnit
    root.areaUnit=areaUnit
    root.volumeUnit=volumeUnit
    root.useSIUnitsForResults=useSIUnitsForResults
    if not SurfaceReferenceLocation is None: root.SurfaceReferenceLocation=SurfaceReferenceLocation
    
    return root
    

    
class gbElement(etree.ElementBase):
    """The default element class for the gbxml parser.
    
    """
    
    def __repr__(self):
        """The repr for the class
        
        :returns: A different value is returned depending on if this is the 
        gbElement class or a subclass, and/or if the element has an 'id' attribute.
        :rtype: str

        """
        try:
            id_=self.get_attribute('id')
            id_st=' (id="%s")' % id_
        except KeyError:
            id_st=''
            
        if self.__class__.__name__=='gbElement':
            return '<%s %s%s>' % (self.__class__.__name__,
                                  self.nntag,
                                  id_st)
        else:
            return '<%s%s>' % (self.__class__.__name__,
                               id_st)
    
        
    def add_child(self,child_nntag,**kwargs):
        """Adds a new child element to the element.
        
        :param child_nntag: The 'no namespace' tag of the child element.
        :type child_nntag: str
        :param kwargs: Attributes to be set for the child element.
        
        :returns: The newly created child element.
        :rtype: (subclass of) gbElement
        
        """
        return gbxml_functions.add_child_to_gbxml_element(
            self,
            child_nntag,
            self.xsd_schema,
            **kwargs)
        
        
    @property
    def get_attributes(self):
        """The attributes of the element.
        
        :returns: A dictionary of attributes where the attribute values
            have been converted to the correct python types according to the 
            schema.
        
        :rtype: dict
        
        """
        
        return gbxml_functions.get_attributes_of_gbxml_element(self,
                                                               self.xsd_schema)
        
       
    
    @property
    def id(self):
        """The id of the element.
        
        :raises KeyError: If, on retrieval, the 'id' attribute is not present in the element.
        
        :rtype: str
        
        """
        return self.get_attribute('id')
            
    
    @id.setter
    def id(self,value):
        ""
        self.set_attribute('id',value)
        
    
    def get_attribute(self,attribute_name):
        """Returns the attribute value as a python type.
        
        :param attribute_name: The name of the attribute.
        :param attribute_name: str
        
        :raises KeyError: If the attribute is not present in the element.
        
        :rtype: bool, str, float
        
        """
        return gbxml_functions.get_attribute_of_gbxml_element(self,
                                                              attribute_name,
                                                              self.xsd_schema)
    
    
    def get_child(self,
                  child_nntag,
                  child_id=None):
        """Returns a child element with specified tag.
        
        If child_id is not supplied, then the first child element found is returned.
        
        :param child_nntag: The 'no namespace' tag of the child element.
        :type child_nntag: str
        :param child_id: Optional, the 'id' attribute of the child element.
        :type child_id: str
        
        :raises ??: If the child element is not present.
        
        :rtype: (subclass of) gbElement 
        
        """
        return gbxml_functions.get_child_of_gbxml_element(
            self,
            child_nntag,
            child_id=child_id)
        
        
    def get_children(self,child_nntag):
        """Returns child elements with a specified tag.
        
        :param child_nntag: The 'no e coercednamespace' tag of the child elements.
        :type child_nntag: str
        
        :rtype: list ??

        """
        return gbCollection(
            *gbxml_functions.get_children_of_gbxml_element(
                self,
                child_nntag
                )
            )
    
    
    def set_attribute(self,attribute_name,value):
        """Sets an attribute value of the element.
        
        Attribute will be created if it does not already exist.
        Attribute value is modified if attribute does already exist.
        Value is coerced to the correct python type if needed.
        
        :param attribute_name: The name of the attribute.
        :param attribute_name: str
        :param value: The new value for the attribute.
        :type value: bool, str, float
        
        :raises KeyError: If attribute does not exist in the schema.
        :raises ValueError: If attribute has enumerations, and 'value' does not
            match one of the enumeration options.
        
        :rtype: The (coerced) value assigned to the attribute.
        
        """
        
        return gbxml_functions.set_attribute_on_gbxml_element(self,
                                                              attribute_name,
                                                              value,
                                                              self.xsd_schema)
        
        
    @property
    def nntag(self):
        """Returns the tag without the namespace ('no namespace tag')
        
        Example:
            
        >>> print(gbXML.tag)
        {http://www.gbxml.org/schema}gbXML
        >>> print(gbXML.nntag)
        gbXML
        
        :rtype: str
        
        """
        return xml_functions.nntag(self)
    
    
    @property
    def ns(self):
        """The namespace dictionary for xpath calls.
        
        :rtype: dict
        
        """
        return gbxml_functions.ns
    
    
    def tostring(self):
        """Returns a string of the xml of the element.
        
        
        :rtype: str
        
        """
        return etree.tostring(copy(self), pretty_print=True).decode()
    
    
    @property
    def xsd_schema(self):
        ""
        return self._xsd_schema
    
    
    @property
    def value(self):
        """The value of the element text as a python type.
        
        :rtype: str, float
        
        """
        xsd_type=gbxml_xsd_functions.get_xsd_type_of_text_of_xsd_element(
            self.nntag,
            self.xsd_schema
            )
        python_type=xml_functions.xsd_type_to_python_type(xsd_type)
        
        return python_type(self.text)
    
    
    @value.setter
    def value(self,value):
        ""
        self.text=str(value)
    
    
    
class gbCollection(collections.abc.Sequence):
    """
    """
    
    def __getattr__(self,key):
        ""
        #print('__getattr__', key)
        
        result=[]
        
        for x in self:
            y=getattr(x,key)
            if isinstance(y,gbCollection):
                result.extend(y)
            else:
                result.append(y)
                
                
        if len(result)==0:
            
            return []
                
        elif isinstance(result[0],gbElement):  # if result is a collection of elements
            
            return gbCollection(*result)
        
        elif inspect.ismethod(result[0]):  # if result is a collection of methods
            
            def boundmethods(*args,**kwargs):
                y=[x(*args,**kwargs) for x in result]
                if isinstance(y[0],gbElement):
                    return gbCollection(*y)
                else:
                    return tuple(y)
                
            return boundmethods
        
        else:
            
            return tuple(result)
    
    
    def __getitem__(self,index):
        ""
        if isinstance(index, slice):
            indices = range(*index.indices(len(self._items)))
            return gbCollection(*[self._items[i] for i in indices])
        else:
            return self._items[index]
    
   
    def __init__(self,*items):
        ""
        
        self._items=tuple(items)
        

    def __len__(self):
        ""
        return len(self._items)
    
    
    def __repr__(self):
        ""
        return '%s(%s)' % (self.__class__.__name__,
                           ', '.join([str(c) for c in self]))
        
    
    
class Campus():
    ""
    
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               surface_kwargs=None):
        """
        """
        #print(self)
        for su in self.Surfaces:
            try:
                ax=su.render(ax=ax)
            except Exception as err:
                print(su)
                print(type(err))
                
        return ax
    
    
    
class CartesianPoint():
    ""
    
    def create_Coordinates(self,*coordinates):
        """Creates Coordinate child elements and sets their value.
        
        :param coordinates: The values of the x,y,(z) coordinates as an argument list.
        :type coordinates: int, float
        
        :returns: The newly creeated Coordinate elements.
        :rtype: list(Coordinate)
        
        """
        return gbCollection(
            *gbxml_functions.add_Coordinates_to_CartesianPoint(
                gbxml_element=self,
                xsd_schema=self.xsd_schema,
                *coordinates
                )
            )
            
            
    def get_coordinates(self):
        """Returns the values of the Coordinate child elements.
        
        :rtype: tuple(float)
        
        """
        
        return gbxml_functions.get_Coordinate_values_from_CartesianPoint(
            self,
            self.xsd_schema
            )
   
    
class Opening():
    ""
    
    
    def get_shell(self):
        """Returns a Polygon of the outer polyloop of the opening.
        
        The following sources are tried in order:
            - PlanarGeometry
            - RectangularGeometry/PolyLoop
            - RectangularGeoemetry... from height and width
            
        :rtype: tuple(tuple(float))
            
        """
        return gbxml_functions.get_shell_of_Opening(self,
                                                    self.xsd_schema)
        
        
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               surface_kwargs=None):
        ""
        x=dict(color='green')
        if outline_kwargs is None:
            outline_kwargs=x
        else:
            for k,v in x.items:
                if not k in outline_kwargs:
                    outline_kwargs[k]=v
            
        x=dict(color='green')
        if surface_kwargs is None:
            surface_kwargs=x
        else:
            for k,v in x.items:
                if not k in surface_kwargs:
                    surface_kwargs[k]=v
        
        ax=render_functions.render_polygon_3d(
            polygon=(self.get_shell(),[]),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            surface_kwargs=surface_kwargs
            )
        
        return ax
        
    
    
    
    

class PlanarGeometry():
    ""
    
    def get_coordinates(self):
        """Returns the coordinates of the polyloop child element.
        
        :returns: Point_coordinates where each point_coordinate is a tuple of 
            the (x,y,(z)) coordinates of a CartesianPoint.
        :rtype: tuple(tuple(float))
        
        """
        return gbxml_functions.get_coordinate_values_from_PlanarGeometry(
            self,
            self.xsd_schema
            )
        
    
    def get_shell(self):
        """Returns a Polygon of the polyloop child element.
        
        :rtype: tuple
        
        """
        return gbxml_functions.get_shell_of_PlanarGeometry(
            self,
            self.xsd_schema
            )
    
    
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               surface_kwargs=None):
        ""
        ax=self.PolyLoop.render(
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            surface_kwargs=surface_kwargs
            )
        
        return ax
    

    
class PolyLoop():
    ""
    
    def create_CartesianPoints(self,*points_coordinates):
        """Creates CartesianPoint child elements with Coordinate subelements.
        
        :param points_coordinates: An argument list of tuple where each tuple is 
            the (x,y,(z)) coordinates of a CartesianPoint.
        :type points_coordinates: tuple
        
        :returns: The newly creeated CartesianPoint elements.
        :rtype: list(CartesianPoints)
        
        """
        for point_coordinates in points_coordinates:
            self.add_CartesianPoint().create_Coordinates(*point_coordinates)
        return self.CartesianPoints
            
            
    def get_coordinates(self):
        """Returns the coordinates of the CartesianPoint child elements.
        
        :returns: Point_coordinates where each point_coordinate is a tuple of 
            the (x,y,(z)) coordinates of a CartesianPoint.
        :rtype: tuple(tuple(float))
        
        """
        return gbxml_functions.get_coordinate_values_from_PolyLoop(
            self,
            self.xsd_schema
            )
        
    
    def get_shell(self):
        """
        """
        return gbxml_functions.get_shell_of_PolyLoop(
            self,
            self.xsd_schema
            )
        
    
    
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               surface_kwargs=None):
        ""
        ax=render_functions.render_polygon_3d(
            polygon=(self.get_shell(),[]),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            surface_kwargs=surface_kwargs
            )
        
        return ax
    

class RectangularGeometry():
    """
    """
    
    def get_shell(self):
        """Returns the coordinates of the rectangular geometry.
        
        The following sources are tried in order:
            - RectangularGeometry/PolyLoop
            - RectangularGeoemetry... from height and width
            
        :rtype: tuple(tuple(float))
            
        """
        return gbxml_functions.get_shell_of_RectangularGeometry(
            self,
            self.xsd_schema
            )
    
    
    def get_shell_from_height_and_width(self):
        """
        """
        return gbxml_functions.get_shell_from_height_and_width_of_RectangularGeometry(
            self,
            self.xsd_schema
            )
    
    
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               surface_kwargs=None):
        ""
        
        ax=render_functions.render_polygon_3d(
            polygon=(self.get_shell(),[]),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            surface_kwargs=surface_kwargs
            )
        
        return ax
            

    
class Surface():
    ""


    def copy_opening(self,
                     opening,
                     tolerance=0.01):
        """
        """
        
        return gbxml_functions.copy_Opening_to_Surface(
            opening,
            self,
            self.xsd_schema,
            tolerance=tolerance
            )


    def get_holes(self):
        """
        """
        return gbxml_functions.get_holes_of_Surface(self,
                                                    self.xsd_schema)
        

    def get_shell(self):
        """Returns a Polygon of the outer polyloop of the opening.
        
        The following sources are tried in order:
            - PlanarGeometry
            - RectangularGeometry/PolyLoop
            - RectangularGeoemetry... from height and width
            
        :rtype: tuple(tuple(float))
            
        """
        return gbxml_functions.get_shell_of_Surface(self,
                                                    self.xsd_schema)


    def get_Spaces(self):
        """Returns the space elements adjacent to the surface.
        
        """
        return gbxml_functions.get_Spaces_of_Surface(self)
        
    
    def get_polygon(self):
        """Returns a Polygon of the outer polyloop of the surface.
        
        The following sources are tried in order:
            - PlanarGeometry
            - RectangularGeometry/PolyLoop
            - RectangularGeoemetry... from height and width
            
        :rtype: tuple(tuple(float))
            
        """
        return gbxml_functions.get_polygon_of_Surface(self,
                                                      self.xsd_schema)
    
    
    
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               surface_kwargs=None):
        ""
        
        ax=render_functions.render_polygon_3d(
            polygon=self.get_polygon(),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            surface_kwargs=surface_kwargs
            )
        
        for op in self.Openings:
            ax=op.render(ax=ax)
        
        return ax
    
    
    

    
    
    
    
