# coding=utf-8
"""Module for parsing THERM results."""
from __future__ import division

import os
import zipfile
import json
import re
import xml.etree.ElementTree as ET

from ladybug_geometry.geometry2d import Point2D
from ladybug_geometry.geometry3d import Vector3D, Plane, Mesh3D


class THMZResult(object):
    """Object for parsing results out of simulated THMZ files.

    Args:
        file_path: Full path to a THMZ file that was simulated using THERM.

    Properties:
        * file_path
        * u_factors
        * plane
        * mesh
        * temperatures
        * heat_fluxes
        * heat_flux_magnitudes
    """

    def __init__(self, file_path):
        """Initialize THMZResult."""
        assert os.path.isfile(file_path), 'No file was found at {}'.format(file_path)
        assert file_path.lower().endswith('.thmz'), \
            '"{}" is not a THMZ file ending in .thmz.'.format(file_path)
        self._file_path = file_path

        # set up variables to track what has been loaded from the file
        self._plane_loaded = False
        self._mesh_loaded = False
        self._u_factors_loaded = False
        self._ss_mesh_results_loaded = False

        # values to be computed as soon as they are requested
        self._plane = None
        self._u_factors = None
        self._mesh = None
        self._temperatures = None
        self._heat_fluxes = None
        self._heat_flux_magnitudes = None

    @property
    def file_path(self):
        """Get the path to the .thmz file."""
        return self._file_path

    @property
    def u_factors(self):
        """Get a tuple of UFactor objects for the .

        This will be None if there is no U-Factor information in the THMZ file
        and this will be an empty tuple if the model had no U-Factor tags assigned
        to it.
        """
        if not self._u_factors_loaded:
            self._extract_u_factors()
        return self._u_factors

    @property
    def plane(self):
        """Get a ladybug-geometry Plane for the 3D plane in which the mesh exists.

        This will be the World XY plane if there is no plane information in the
        THMZ file.
        """
        if not self._plane_loaded:
            self._extract_plane()
        return self._plane

    @property
    def mesh(self):
        """Get a ladybug-geometry Mesh3D for the finite element mesh of the model.

        Will be None if the THMZ file has not been simulated and there is no Mesh
        in the file.
        """
        if not self._mesh_loaded:
            self._extract_mesh()
        return self._mesh

    @property
    def temperatures(self):
        """Get a tuple of temperatures in Celsius that correspond to the mesh vertices.

        Will be None if the THMZ file has not been simulated and there are no
        steady state results in the file.
        """
        if not self._ss_mesh_results_loaded:
            self._extract_steady_state_results()
        return self._temperatures

    @property
    def heat_fluxes(self):
        """Get a tuple of Vector3Ds that correspond to the mesh vertices for heat fluxes.

        Will be None if the THMZ file has not been simulated and there are no
        steady state results in the file.
        """
        if not self._ss_mesh_results_loaded:
            self._extract_steady_state_results()
        return self._heat_fluxes

    @property
    def heat_flux_magnitudes(self):
        """Get a tuple of heat flux values in W/m2 in that correspond to mesh vertices.

        Will be None if the THMZ file has not been simulated and there are no
        steady state results in the file.
        """
        if not self._ss_mesh_results_loaded:
            self._extract_steady_state_results()
        return self._heat_flux_magnitudes

    def _extract_u_factors(self):
        """Extract U-factor results from the THMZ file."""
        self._u_factors_loaded = True
        try:  # load the root of the SteadyStateResults.xml
            with zipfile.ZipFile(self.file_path, 'r') as archive:
                with archive.open('SteadyStateResults.xml') as f:
                    # Read content as bytes and decode to a string
                    content = f.read().decode('utf-8')
        except KeyError:  # no results in the file; it has not been simulated
            return
        # extract the temperatures and heat fluxes from the model
        xml_root = ET.fromstring(content)
        xml_case = xml_root.find('Case')
        u_factors = []
        for xml_u_fac in xml_case:
            if xml_u_fac.tag == 'U-factors':
                u_factors.append(UFactor(xml_u_fac))
        self._u_factors = tuple(u_factors)

    def _extract_plane(self):
        """Extract a Plane object from the THMZ file."""
        self._plane_loaded = True
        # load the root of the Model.xml
        with zipfile.ZipFile(self.file_path, 'r') as archive:
            with archive.open('Model.xml') as f:
                # Read content as bytes and decode to a string
                content = f.read().decode('utf-8')
        xml_root = ET.fromstring(content)
        # extract the Plane specification form the model
        xml_props = xml_root.find('Properties')
        xml_gen = xml_props.find('General')
        xml_notes = xml_gen.find('Notes')
        all_notes = xml_notes.text
        _plane_pattern = re.compile(r"Plane:\s(.*)")
        matches = _plane_pattern.findall(all_notes)
        if len(matches) > 0:
            self._plane = Plane.from_dict(json.loads(matches[0]))
        else:
            self._plane = Plane()

    def _extract_mesh(self):
        """Extract a Mesh3D object from the THMZ file."""
        self._mesh_loaded = True
        try:  # load the root of the Mesh.xml
            with zipfile.ZipFile(self.file_path, 'r') as archive:
                with archive.open('Mesh.xml') as f:
                    # Read content as bytes and decode to a string
                    content = f.read().decode('utf-8')
        except KeyError:  # no mesh in the file; it has not been simulated
            return
        # get the mesh information from the root
        xml_root = ET.fromstring(content)
        xml_case = xml_root.find('Case')
        # extract the vertices (aka. nodes) from the model
        plane = self.plane
        vertices = []
        for xml_node in xml_case.find('Nodes'):
            pt_2d = Point2D(xml_node.find('x').text, xml_node.find('y').text)
            vertices.append(plane.xy_to_xyz(pt_2d))
        # extract the faces (aka. elements) from the model
        faces = []
        for xml_face in xml_case.find('Elements'):
            face_i = []
            for e_prop in xml_face:
                if e_prop.tag.startswith('node'):
                    face_i.append(int(e_prop.text) - 1)
            faces.append(tuple(face_i))
        self._mesh = Mesh3D(vertices, faces)

    def _extract_steady_state_results(self):
        """Extract steady state results from the THMZ file."""
        self._mesh_loaded = True
        try:  # load the root of the SteadyStateMeshResults.xml
            with zipfile.ZipFile(self.file_path, 'r') as archive:
                with archive.open('SteadyStateMeshResults.xml') as f:
                    # Read content as bytes and decode to a string
                    content = f.read().decode('utf-8')
        except KeyError:  # no results in the file; it has not been simulated
            return
        # extract the temperatures and heat fluxes from the model
        xml_root = ET.fromstring(content)
        xml_case = xml_root.find('Case')
        plane = self.plane
        temperatures, heat_fluxes, flux_magnitudes = [], [], []
        for xml_node in xml_case.find('Nodes'):
            temperatures.append(float(xml_node.find('Temperature').text))
            pt_2d = Point2D(xml_node.find('X-flux').text, xml_node.find('Y-flux').text)
            pt_3d = plane.xy_to_xyz(pt_2d)
            flux_vec = Vector3D(pt_3d.x, pt_3d.y, pt_3d.z)
            heat_fluxes.append(flux_vec)
            flux_magnitudes.append(flux_vec.magnitude)
        self._temperatures = tuple(temperatures)
        self._heat_fluxes = tuple(heat_fluxes)
        self._heat_flux_magnitudes = tuple(flux_magnitudes)

    def ToString(self):
        """Overwrite .NET ToString."""
        return self.__repr__()

    def __repr__(self):
        return 'THMZ Result: {}'.format(self.file_path)


class UFactor(object):
    """Object for holding the results of an individual U-factor tag.

    Args:
        xml_element: An XML element for a U-factor result in the
            SteadyStateResults.xml file.

    Properties:
        * name
        * delta_temperature
        * heat_flux
        * total_u_factor
        * total_length
        * projected_x_u_factor
        * projected_x_length
        * projected_y_u_factor
        * projected_y_length
        * projected_in_glass_plane_u_factor
        * projected_in_glass_plane_length
        * custom_rotation_u_factor
        * custom_rotation_length
    """

    __slots__ = (
        '_name', '_delta_temperature', '_heat_flux', '_total_u_factor', '_total_length',
        '_projected_x_u_factor', '_projected_x_length',
        '_projected_y_u_factor', '_projected_y_length',
        '_projected_in_glass_plane_u_factor', '_projected_in_glass_plane_length',
        '_custom_rotation_u_factor', '_custom_rotation_length')

    def __init__(self, xml_element):
        """Initialize UFactor."""
        # get the basic properties like the name and heat flux
        self._name = xml_element.find('Tag').text
        self._delta_temperature = float(xml_element.find('DeltaT').text)
        self._heat_flux = float(xml_element.find('HeatFlux').text)

        # set defaults for the different U-factors in case they are not found
        self._total_u_factor = None
        self._total_length = 0
        self._projected_x_u_factor = None
        self._projected_x_length = 0
        self._projected_y_u_factor = None
        self._projected_y_length = 0
        self._projected_in_glass_plane_u_factor = None
        self._projected_in_glass_plane_length = 0
        self._custom_rotation_u_factor = None
        self._custom_rotation_length = 0

        # extract the different types of U-factors
        for xml_project in xml_element:
            if xml_project.tag == 'Projection':
                len_type = xml_project.find('Length-type').text
                if len_type == 'Total Length':
                    self._set_u_factors(
                        xml_project, '_total_u_factor', '_total_length')
                elif len_type == 'Projected X':
                    self._set_u_factors(
                        xml_project, '_projected_x_u_factor', '_projected_x_length')
                elif len_type == 'Projected Y':
                    self._set_u_factors(
                        xml_project, '_projected_y_u_factor', '_projected_y_length')
                elif len_type == 'Projected in glass plane':
                    self._set_u_factors(
                        xml_project, '_projected_in_glass_plane_u_factor',
                        '_projected_in_glass_plane_length')
                elif len_type == 'Custom Rotation':
                    self._set_u_factors(
                        xml_project, '_custom_rotation_u_factor',
                        '_custom_rotation_length')

    @property
    def name(self):
        """Get the name of the U-factor tag."""
        return self._name

    @property
    def delta_temperature(self):
        """Get a number for the temperature delta across the boundaries in Celsius."""
        return self._delta_temperature

    @property
    def heat_flux(self):
        """Get a number for the heat flux across the boundaries in W/m2."""
        return self._heat_flux

    @property
    def total_u_factor(self):
        """Get a number for the total U-Factor across the boundaries in W/m2-K."""
        return self._total_u_factor

    @property
    def total_length(self):
        """Get a number for the total length the boundaries in mm."""
        return self._total_length

    @property
    def projected_x_u_factor(self):
        """Get a number for the X-projected U-Factor across the boundaries in W/m2-K."""
        return self._projected_x_u_factor

    @property
    def projected_x_length(self):
        """Get a number for the X-projected length the boundaries in mm."""
        return self._projected_x_length

    @property
    def projected_y_u_factor(self):
        """Get a number for the X-projected U-Factor across the boundaries in W/m2-K."""
        return self._projected_y_u_factor

    @property
    def projected_y_length(self):
        """Get a number for the Y-projected length the boundaries in mm."""
        return self._projected_y_length

    @property
    def projected_in_glass_plane_u_factor(self):
        """Get a number for the glass plane-projected U-Factor in W/m2-K."""
        return self._projected_in_glass_plane_u_factor

    @property
    def projected_in_glass_plane_length(self):
        """Get a number for the glass plane-projected length in mm."""
        return self._projected_in_glass_plane_length

    @property
    def custom_rotation_u_factor(self):
        """Get a number for the custom rotation-projected U-Factor in W/m2-K."""
        return self._custom_rotation_u_factor

    @property
    def custom_rotation_length(self):
        """Get a number for the custom rotation-projected length in mm."""
        return self._custom_rotation_length

    def _set_u_factors(self, xml_project, u_fac_attr, len_attr):
        """Set the U-Factor properties given an XML Projection element."""
        xml_u_fac = xml_project.find('U-factor')
        if xml_u_fac is not None:
            setattr(self, u_fac_attr, float(xml_u_fac.text))
        xml_len = xml_project.find('Length')
        if xml_len is not None:
            setattr(self, len_attr, float(xml_len.text))

    def ToString(self):
        """Overwrite .NET ToString."""
        return self.__repr__()

    def __repr__(self):
        return 'UFactor: {}'.format(self.name)
