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


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

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.xsd_functions as xsd_functions
import xgbxml.gbxml_xsd_functions as gbxml_xsd_functions

from . import render_functions
from . import geometry_functions 


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 three types of additional properties and methods:
          
      1. Properties and methods inherited from the :py:class:`~xgbxml.xgbxml.gbElement` class.
      
      2. Properties and methods that are automatically generated from the 
         gbXML schema file. For example, this method generates the :code:`version` property and
         the :code:`add_Campus` method for the gbXML element.
         
      3. 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(remove_blank_text=True)
    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):
    """A base class which is inherited by all xgbxml element instances.
    
    When using either :py:func:`~xgbxml.xgbxml.get_parser` or 
    :py:func:`~xgbxml.xgbxml.create_gbXML`, all elements created will have the 
    properties and methods available in this class.
    
    """
    
    def _introspect(self):
        ""
        
        print('self.__class__',self.__class__)
        print('self.__class__.__bases__',self.__class__.__bases__)
        print(self.__class__.__bases__[1])
        print([x for x in dir(self.__class__.__bases__[1]) if not x.startswith('_')])
        
        import inspect
        for name,obj in inspect.getmembers(self.__class__.__bases__[1]):
            print(name, type(obj))
            if inspect.isfunction(obj):
                print(inspect.signature(obj))
        
        
    
    def __repr__(self):
        """The repr for the class.
        
        Determines how the instance is displayed when printed.
        
        :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,
                  value=None,
                  **kwargs):
        """Adds a new child element to the element.
        
        :param child_nntag: The 'no namespace' tag of the child element (i.e. "Campus")
        :type child_nntag: str
        :param value: The value for the element. Optional.
        :type value: str, float, bool etc.
        :param kwargs: Attributes to be set for the child element. Optional.
        
        :raises KeyError: If the child name does not exist in the schema.
        :raises: Other error may be raised if the optional value or attributes are
            not specified correctly.
        
        :returns: The newly created child element.
        :rtype: (subclass of) gbElement
        
        """
        return gbxml_functions.add_child_to_gbxml_element(
            gbxml_element=self,
            child_nntag=child_nntag,
            xsd_schema=self.xsd_schema,
            value=value,
            **kwargs)
       
    
    def get_attribute(self,attribute_name):
        """Returns the attribute value of the element.
        
        :param attribute_name: The name of the attribute.
        :param attribute_name: str
            
        :raises KeyError: If the attribute is not present in the element.
        
        :returns: The text value of the attribute converted to the python type
            of the attribute.
        :rtype: bool, str or float etc.
        
        """
        return gbxml_functions.get_attribute_of_gbxml_element(
            gbxml_element=self,
            attribute_name=attribute_name,
            xsd_schema=self.xsd_schema
            )
    
    
    @property
    def get_attributes(self):
        """Returns the attributes of the element.
        
        :param gbxml_element: A gbXML element.
        :type gbxml_element: lxml.etree._Element
        :param xsd_schema: The root node of a gbXML schema.
        :type xsd_schema: lxml.etree._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(
            gbxml_element=self,
            xsd_schema=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 KeyError: 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 all child element with specified tag.
        
        :param child_nntag: The 'no namespace' tag of the child element (i.e. "Campus")
        :type child_nntag: str
        
        :rtype: gbCollection

        """
        return gbCollection(
            *gbxml_functions.get_children_of_gbxml_element(
                self,
                child_nntag
                )
            )
    
    
    @property
    def id(self):
        """Returns / sets the 'id' attribute of the element.
        
        :param value: When setting, the value of the id attribute
        :type value: str
        
        :raises KeyError: When returning, if the 'id' attribute is not present in the element.
        :raises KeyError: When setting, if attribute name does not exist in the schema.
        :raises TypeError: When setting, if the attribute value is of a type that does not match 
            the schema.
        
        :rtype: str (when returning)
        
        """
        return self.get_attribute('id')
            
    
    @id.setter
    def id(self,value):
        """Sets the 'id' attribute of the element.

        
        """
        self.set_attribute('id',value)
        
    
    @property
    def nntag(self):
        """Returns the tag without the namespace ('no namespace tag')
        
        :rtype: str
        
        .. rubric:: Code Example
            
        .. code-block:: python
        
           >>> print(gbxml.tag)
           {http://www.gbxml.org/schema}gbXML
           >>> print(gbxml.nntag)
           gbXML
        
        
        """
        return xml_functions.nntag(self)
    
    
    @property
    def ns(self):
        """The namespace dictionary for xpath calls.
        
        :rtype: dict
        
        """
        return gbxml_functions.ns
    
    
    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.
        
        :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 name does not exist in the schema.
        :raises ValueError: If attribute has enumerations, and 'value' does not
            match one of the enumeration options.
        :raises TypeError: If the attribute value is of a type that does not match 
            the schema.
        
        """
        
        return gbxml_functions.set_attribute_on_gbxml_element(self,
                                                              attribute_name,
                                                              value,
                                                              self.xsd_schema)
        
    
    
    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):
        """Returns / set the value of the gbXML element.
        
        This is stored in the text value of the XML element.
        
        :param value: When setting, the value for the element.
        :type value: str, float, bool etc.
        
        :raises TypeError: When setting, if value is of a type that does not match 
            the schema.
        
        :returns: When returning, a value which is converted from the element text node.
        :rtype: str, int, float or book etc.
        
        """
        xsd_type=gbxml_xsd_functions.get_xsd_type_of_text_of_xsd_element(
            self.nntag,
            self.xsd_schema
            )
        python_type=xsd_functions.xsd_type_to_python_type(xsd_type)
        
        return python_type(self.text)
    
    
    @value.setter
    def value(self,value):
        """Sets the value of the element.
        
        
        
        """
        
        gbxml_functions.set_value_of_gbxml_element(
            gbxml_element=self,
            value=value,
            xsd_schema=self.xsd_schema
            )
        
    
    
class gbCollection(collections.abc.Sequence):
    """This class is a collection of xgbxml elements.
    
    It acts like a list, but is immutable so cannot be changed or updated. 
    However the items in the list can be changed.
    
    Instances of this class occur when a list of xgbxml elements is returned 
    using the :py:func:`get_children` method or a method that is based on 
    :py:func:`get_children`.
    
    For example, the code below returns a :code:`gbCollection` instance of 
    Building elements:

    .. code-block:: python
        
       from lxml import etree
       import xgbxml
    
       parser=xgbxml.get_parser()  # default is gbXML version 6.01
    
       tree=etree.parse('gbXMLStandard.xml', parser)
       gbxml=tree.getroot()
       buildings=gbxml.Campus.Buildings
    
       print(buildings)
       # prints "gbCollection(<Building (id="aim0013")>)"
    
       print(type(buildings))
       # prints "<class 'xgbxml.xgbxml.gbCollection'>"
       
    
    """
    
    def __getattr__(self,key):
        """This method catches any unknown attribute calls on a gbCollection instance.
        
        This allows gbCollection instances to propogate methods and property calls
        onto the elements stored in the collection.
        
        :param key: The key passes to __getattr__
        :param key: str
        
        This can be used to access all child elements of the elements in a gbCollection.
        For example, the code below shows a quick access approach to query 
        all the openings in a gbXML file.
        
        .. code-block::  python
        
           from lxml import etree
           import xgbxml

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

           tree=etree.parse('gbXMLStandard.xml', parser)
           gbxml=tree.getroot()
           openings=gbxml.Campus.Surfaces.Openings

           print(len(openings))
           # prints "138"
           
           print(type(openings))
           # prints "<class 'xgbxml.xgbxml.gbCollection'>"

        This can also be used to access the properties of the elements of a gbCollection.
        For example, the code below shows a quick-access approach to listing
        all the openingTypes attributes of the openings in a gbXL file.
        
        .. code-block:: python
        
           from lxml import etree
           import xgbxml

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

           tree=etree.parse('gbXMLStandard.xml', parser)
           gbxml=tree.getroot()
           opening_types=gbxml.Campus.Surfaces.Openings.openingType

           print(len(opening_types))
           # prints "138"

           print(type(opening_types))
           # prints "<class 'tuple'>"

           print(opening_types)
           # prints "('NonSlidingDoor', 'NonSlidingDoor', 'NonSlidingDoor', ...)"
        
        The approach also works with method calls. For example, the code below 
        creates a list of the areas of all openings in a gbXML file.
        
        .. code-block:: python
        
           from lxml import etree
           import xgbxml

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

           tree=etree.parse('gbXMLStandard.xml', parser)
           gbxml=tree.getroot()
           surface_areas=gbxml.Campus.Surfaces.Openings.get_area()

           print(len(surface_areas))
           # prints "138"

           print(type(surface_areas))
           # prints "<class 'tuple'>"

           print(surface_areas)
           # prints "(21.0, 21.000000056466668, 21.0, ...)"
            
            
        """
        #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):
        """
        
        :param index: The index value passed to __getitem__
        :type index: int
        
        """
        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, 
               surface_outline_kwargs=None,
               surface_fill_kwargs=None,
               opening_outline_kwargs=None,
               opening_fill_kwargs=None):
        """Renders the Campus in 3D using matplotlib.
        
        :param ax: A matplotlib 3D Axes instance. Optional, if not supplied
            then an axis is created and returned.
        :type ax:  matplotlib.axes._subplots.Axes3DSubplot
        :param set_lims: If True, then the x, y and z axis limits are set
            automatically based on the geometry being rendered.
        :type set_lims: bool
        :param surface_outline_kwargs: matplotlib keywork arguments for formatting
            the outlines of surfaces (passed to ax.plot method).
        :param surface_fill_kwargs: matplotlib keywork arguments for formatting the 
            fill of surfaces (passed to Poly3DCollection method).
        :param opening_outline_kwargs: matplotlib keywork arguments for formatting
            the outlines of openings (passed to ax.plot method).
        :param opening_fill_kwargs: matplotlib keywork arguments for formatting the 
            fill of openings (passed to Poly3DCollection method).
            
        :returns: The axis instance.
        :rtype: matplotlib.axes._subplots.Axes3DSubplot
        
        """
        #print(self)
        for su in self.Surfaces:
            #try:
            ax=su.render(ax=ax,
                         set_lims=set_lims,
                         surface_outline_kwargs=surface_outline_kwargs,
                         surface_fill_kwargs=surface_fill_kwargs,
                         opening_outline_kwargs=opening_outline_kwargs,
                         opening_fill_kwargs=opening_fill_kwargs
                         )
            #except TypeError 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.
            For example: ((0,0,0),(1,0,0),(0,1,0)) 
        :type coordinates: tuple otr list etc.
        
        :returns: The newly creeated Coordinate elements.
        :rtype: gbCollection
        
        """
        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_area(self):
        """Calculates the area of the opening.
        
        :returns: The area of the Opening.
        :rtype: float
        
        """
        shell=self.get_shell()
        holes=[]
        return geometry_functions.polygon_area_3d(shell,holes)
    
    
    def get_shell(self):
        """Returns the shell 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,
               fill_kwargs=None):
        """
        Renders the Opening in 3D using matplotlib.
        
        :param ax: A matplotlib 3D Axes instance. Optional, if not supplied
            then an axis is created and returned.
        :type ax:  matplotlib.axes._subplots.Axes3DSubplot
        :param set_lims: If True, then the x, y and z axis limits are set
            automatically based on the geometry being rendered.
        :type set_lims: bool
        :param outline_kwargs: matplotlib keywork arguments for formatting
            the outlines of the openings (passed to ax.plot method).
        :param surface_kwargs: matplotlib keywork arguments for formatting the 
            surface rendering (passed to Poly3DCollection method).
            
        :returns: The axis instance.
        :rtype: matplotlib.axes._subplots.Axes3DSubplot
        
        """
        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 fill_kwargs is None:
            fill_kwargs=x
        else:
            for k,v in x.items:
                if not k in fill_kwargs:
                    fill_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,
            fill_kwargs=fill_kwargs
            )
        
        return ax
        

class PlanarGeometry():
    ""
    
    def get_area(self):
        """Returns the area of the polygon described by the PlanarGeometry.
        
        :rtype: float
        
        """
        shell=self.get_shell()
        holes=[]
        return geometry_functions.polygon_area_3d(shell,holes)
    
    
    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 the shell of the polyloop child element.
        
        :rtype: tuple(tuple(float))
        
        """
        return gbxml_functions.get_shell_of_PlanarGeometry(
            self,
            self.xsd_schema
            )
    
    
    def render(self,
               ax=None, 
               set_lims=True, 
               outline_kwargs=None,
               fill_kwargs=None):
        """Renders the PlanarGeometry in 3D using matplotlib.
        
        :param ax: A matplotlib 3D Axes instance. Optional, if not supplied
            then an axis is created and returned.
        :type ax:  matplotlib.axes._subplots.Axes3DSubplot
        :param set_lims: If True, then the x, y and z axis limits are set
            automatically based on the geometry being rendered.
        :type set_lims: bool
        :param outline_kwargs: matplotlib keywork arguments for formatting
            the outlines (passed to ax.plot method).
        :param fill_kwargs: matplotlib keywork arguments for formatting the 
            fill (passed to Poly3DCollection method).
        
        :returns: The axis instance.
        :rtype: matplotlib.axes._subplots.Axes3DSubplot
        
        """
        ax=self.PolyLoop.render(
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            fill_kwargs=fill_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_area(self):
        """
        
        :returns: The area of the PolyLoop
        :rtype: float
        
        """
        shell=self.get_shell()
        holes=[]
        return geometry_functions.polygon_area_3d(shell,holes)
    
            
    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,
               fill_kwargs=None):
        """Renders the PolyLoop in 3D using matplotlib.
        
        :param ax: A matplotlib 3D Axes instance. Optional, if not supplied
            then an axis is created and returned.
        :type ax:  matplotlib.axes._subplots.Axes3DSubplot
        :param set_lims: If True, then the x, y and z axis limits are set
            automatically based on the geometry being rendered.
        :type set_lims: bool
        :param outline_kwargs: matplotlib keywork arguments for formatting
            the outlines (passed to ax.plot method).
        :param fill_kwargs: matplotlib keywork arguments for formatting the 
            fill (passed to Poly3DCollection method).
        
        :returns: The axis instance.
        :rtype: matplotlib.axes._subplots.Axes3DSubplot
        
        """
        ax=render_functions.render_polygon_3d(
            polygon=(self.get_shell(),[]),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            fill_kwargs=fill_kwargs
            )
        
        return ax
    

class RectangularGeometry():
    """
    """
    
    def get_shell(self):
        """Returns the shell 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):
    #     """Returns the shell coordinates of the rectangular geometry.
        
        
        
    #     :rtype: tuple(tuple(float))
            
    #     """
    #     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,
               fill_kwargs=None):
        """Renders the RectangularGeometry in 3D using matplotlib.
        
        :param ax: A matplotlib 3D Axes instance. Optional, if not supplied
            then an axis is created and returned.
        :type ax:  matplotlib.axes._subplots.Axes3DSubplot
        :param set_lims: If True, then the x, y and z axis limits are set
            automatically based on the geometry being rendered.
        :type set_lims: bool
        :param outline_kwargs: matplotlib keywork arguments for formatting
            the outlines (passed to ax.plot method).
        :param fill_kwargs: matplotlib keywork arguments for formatting the 
            fill (passed to Poly3DCollection method).
        
        :returns: The axis instance.
        :rtype: matplotlib.axes._subplots.Axes3DSubplot
        
        """
        
        ax=render_functions.render_polygon_3d(
            polygon=(self.get_shell(),[]),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=outline_kwargs,
            fill_kwargs=fill_kwargs
            )
        
        return ax
            

    
class Surface():
    ""


    def copy_opening(self,
                     opening,
                     tolerance=0.01):
        """Makes a copy of an Opening and places it on the Surface.
        
        :param opening: The opening to be copied.
        :type opening: xgbxml.xgbxml.Opening
        :param tolerance: The distance which an opening can be 'snapped' to a surface in meters.
        :type tolerance: fload
        
        :returns: The newly created Opening.
        :rtype: xgbxml.xgbxml.Opening
        
        """
        
        return gbxml_functions.copy_Opening_to_Surface(
            opening,
            self,
            self.xsd_schema,
            tolerance=tolerance
            )


    def get_area(self):
        """Calculates the area of the surface (does not include the area of any openings)
        
        :returns: The area of the Surface (the shell area minus the Opening areas)
        :rtype: float
        
        """
        shell=self.get_shell()
        holes=self.get_holes()
        return geometry_functions.polygon_area_3d(shell,holes)


    def get_holes(self):
        """Returns the coordinates of the holes of the surface.
        
        :rtype: list(tuple(tuple(float)))
        
        """
        return gbxml_functions.get_holes_of_Surface(self,
                                                    self.xsd_schema)
        

    def get_shell(self):
        """Returns the shell coordinates 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.
        
        :returns: The Spaces as given by the AdjacentSpaceId elements.
        :rtype: gbCollection
        
        """
        return gbCollection(
            *gbxml_functions.get_Spaces_of_Surface(self)
            )
        
    
    def get_polygon(self):
        """Returns the polygon coordinates of the outer polyloop of the surface.
        
        The following sources are tried in order:
            - PlanarGeometry
            - RectangularGeometry/PolyLoop
            - RectangularGeoemetry... from height and width
            
        :returns: (shell_coordinates, list of hole coordinates)
        :rtype: ( tuple(tuple(float)), list(tuple(tuple(float))) )
            
        """
        return gbxml_functions.get_polygon_of_Surface(self,
                                                      self.xsd_schema)
    
    
    
    def render(self,
               ax=None, 
               set_lims=True, 
               surface_outline_kwargs=None,
               surface_fill_kwargs=None,
               opening_outline_kwargs=None,
               opening_fill_kwargs=None):
        """Renders the Surface in 3D using matplotlib.
        
        :param ax: A matplotlib 3D Axes instance. Optional, if not supplied
            then an axis is created and returned.
        :type ax:  matplotlib.axes._subplots.Axes3DSubplot
        :param set_lims: If True, then the x, y and z axis limits are set
            automatically based on the geometry being rendered.
        :type set_lims: bool
        :param surface_outline_kwargs: matplotlib keywork arguments for formatting
            the outlines of surfaces (passed to ax.plot method).
        :param surface_fill_kwargs: matplotlib keywork arguments for formatting the 
            fill of surfaces (passed to Poly3DCollection method).
        :param opening_outline_kwargs: matplotlib keywork arguments for formatting
            the outlines of openings (passed to ax.plot method).
        :param opening_fill_kwargs: matplotlib keywork arguments for formatting the 
            fill of openings (passed to Poly3DCollection method).
            
        :returns: The axis instance.
        :rtype: matplotlib.axes._subplots.Axes3DSubplot
        
        """
        
        ax=render_functions.render_polygon_3d(
            polygon=self.get_polygon(),
            polygon_triangles=None,
            ax=ax,
            set_lims=set_lims,
            outline_kwargs=surface_outline_kwargs,
            fill_kwargs=surface_fill_kwargs
            )
        
        for op in self.Openings:
            ax=op.render(ax=ax,
                         set_lims=set_lims,
                         outline_kwargs=opening_outline_kwargs,
                         fill_kwargs=opening_fill_kwargs)
        
        return ax
    
    
    

    
    
    
    
