"""
Surface module
SPDX - License - Identifier: LGPL - 3.0 - or -later
Copyright © 2022 Concordia CERC group
Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
"""

from __future__ import annotations
import uuid
import numpy as np
from typing import List, Union
from hub.city_model_structure.attributes.polygon import Polygon
from hub.city_model_structure.attributes.plane import Plane
from hub.city_model_structure.attributes.point import Point
from hub.city_model_structure.greenery.vegetation import Vegetation
from hub.city_model_structure.building_demand.thermal_boundary import ThermalBoundary
import hub.helpers.constants as cte


class Surface:
  """
  Surface class
  """

  def __init__(self, solid_polygon, perimeter_polygon, holes_polygons=None, name=None, surface_type=None):
    self._type = surface_type
    self._name = name
    self._id = None
    self._azimuth = None
    self._inclination = None
    self._area = None
    self._lower_corner = None
    self._upper_corner = None
    self._shared_surfaces = []
    self._global_irradiance = dict()
    self._perimeter_polygon = perimeter_polygon
    self._holes_polygons = holes_polygons
    self._solid_polygon = solid_polygon
    self._short_wave_reflectance = None
    self._long_wave_emittance = None
    self._inverse = None
    self._associated_thermal_boundaries = []
    self._vegetation = None

  @property
  def name(self):
    """
    Get the surface name
    :return: str
    """
    if self._name is None:
      self._name = str(uuid.uuid4())
    return self._name

  @property
  def id(self):
    """
    Get the surface id
    :return: str
    """
    if self._id is None:
      raise ValueError('Undefined surface id')
    return self._id

  @id.setter
  def id(self, value):
    """
    Set the surface id
    :param value: str
    """
    if value is not None:
      self._id = str(value)

  # todo: implement share surfaces
  @property
  def share_surfaces(self):
    """
    Raises not implemented error
    """
    raise NotImplementedError

  def _max_coord(self, axis):
    if axis == 'x':
      axis = 0
    elif axis == 'y':
      axis = 1
    else:
      axis = 2
    max_coordinate = ''
    for point in self.perimeter_polygon.coordinates:
      if max_coordinate == '':
        max_coordinate = point[axis]
      elif max_coordinate < point[axis]:
        max_coordinate = point[axis]
    return max_coordinate

  def _min_coord(self, axis):
    if axis == 'x':
      axis = 0
    elif axis == 'y':
      axis = 1
    else:
      axis = 2
    min_coordinate = ''
    for point in self.perimeter_polygon.coordinates:
      if min_coordinate == '':
        min_coordinate = point[axis]
      elif min_coordinate > point[axis]:
        min_coordinate = point[axis]
    return min_coordinate

  @property
  def lower_corner(self):
    """
    Get surface's lower corner [x, y, z]
    :return: [float]
    """
    if self._lower_corner is None:
      self._lower_corner = [self._min_coord('x'), self._min_coord('y'), self._min_coord('z')]
    return self._lower_corner

  @property
  def upper_corner(self):
    """
    Get surface's upper corner [x, y, z]
    :return: [float]
    """
    if self._upper_corner is None:
      self._upper_corner = [self._max_coord('x'), self._max_coord('y'), self._max_coord('z')]
    return self._upper_corner

  @property
  def perimeter_area(self):
    """
    Get perimeter surface area in square meters (opaque + transparent)
    :return: float
    """
    self._area = self.perimeter_polygon.area
    return self._area

  @property
  def azimuth(self):
    """
    Get surface azimuth in radians
    :return: float
    """
    if self._azimuth is None:
      normal = self.perimeter_polygon.normal
      self._azimuth = np.arctan2(normal[1], normal[0])
    return self._azimuth

  @property
  def inclination(self):
    """
    Get surface inclination in radians
    :return: float
    """
    if self._inclination is None:
      self._inclination = np.arccos(self.perimeter_polygon.normal[2])
    return self._inclination

  @property
  def type(self):
    """
    Get surface type Ground, Wall or Roof
    :return: str
    """

    # todo: there are more types: internal wall, internal floor... this method must be redefined
    if self._type is None:
      grad = np.rad2deg(self.inclination)
      if grad >= 170:
        self._type = 'Ground'
      elif 80 <= grad <= 100:
        self._type = 'Wall'
      else:
        self._type = 'Roof'
    return self._type

  @property
  def global_irradiance(self) -> dict:
    """
    Get global irradiance on surface in Wh/m2
    :return: dict{DataFrame(float)}
    """
    return self._global_irradiance

  @global_irradiance.setter
  def global_irradiance(self, value):
    """
    Set global irradiance on surface in Wh/m2
    :param value: dict{DataFrame(float)}
    """
    self._global_irradiance = value

  @property
  def perimeter_polygon(self) -> Polygon:
    """
    Get a polygon surface defined by the perimeter, merging solid and holes
    :return: Polygon
    """
    return self._perimeter_polygon

  @property
  def solid_polygon(self) -> Polygon:
    """
    Get the solid surface
    :return: Polygon
    """
    return self._solid_polygon

  @solid_polygon.setter
  def solid_polygon(self, value):
    """
    Set the solid surface
    :return: Polygon
    """
    self._solid_polygon = value

  @property
  def holes_polygons(self) -> Union[List[Polygon], None]:
    """
    Get hole surfaces, a list of hole polygons found in the surface
    :return: None, [] or [Polygon]
    None -> not known whether holes exist in reality or not due to low level of detail of input data
    [] -> no holes in the surface
    [Polygon] -> one or more holes in the surface
    """
    return self._holes_polygons

  @holes_polygons.setter
  def holes_polygons(self, value):
    """
    Set the hole surfaces
    :param value: [Polygon]
    """
    self._holes_polygons = value

  @property
  def short_wave_reflectance(self):
    """
    Get the short wave reflectance, this includes all solar spectrum, visible and not visible
    The absorptance as an opaque surface, can be calculated as 1-short_wave_reflectance
    :return: float
    """
    return self._short_wave_reflectance

  @short_wave_reflectance.setter
  def short_wave_reflectance(self, value):
    """
    Set the short wave reflectance, this includes all solar spectrum, visible and not visible
    The absorptance as an opaque surface, can be calculated as 1-short_wave_reflectance
    :param value: float
    """
    self._short_wave_reflectance = value

  @property
  def long_wave_emittance(self):
    """
    Get the long wave emittance af the surface
    The thermal absorptance can be calculated as 1-long_wave_emittance
    :return: float
    """
    return self._long_wave_emittance

  @long_wave_emittance.setter
  def long_wave_emittance(self, value):
    """
    Set the long wave emittance af the surface
    The thermal absorptance can be calculated as 1-long_wave_emittance
    :param value: float
    """
    self._long_wave_emittance = value

  @property
  def inverse(self) -> Surface:
    """
    Get the inverse surface (the same surface pointing backwards)
    :return: Surface
    """
    if self._inverse is None:
      new_solid_polygon = Polygon(self.solid_polygon.inverse)
      new_perimeter_polygon = Polygon(self.perimeter_polygon.inverse)
      new_holes_polygons = []
      if self.holes_polygons is not None:
        for hole in self.holes_polygons:
          new_holes_polygons.append(Polygon(hole.inverse))
      else:
        new_holes_polygons = None
      self._inverse = Surface(new_solid_polygon, new_perimeter_polygon, new_holes_polygons, cte.VIRTUAL_INTERNAL)
    return self._inverse

  def shared_surfaces(self):
    """
    Raises not implemented error
    """
    # todo: check https://trimsh.org/trimesh.collision.html as an option to implement this method
    raise NotImplementedError

  def divide(self, z):
    """
    Divides a surface at Z plane
    :return: Surface, Surface, Any
    """
    # todo: check return types
    # todo: recheck this method for LoD3 (windows)
    origin = Point([0, 0, z])
    normal = np.array([0, 0, 1])
    plane = Plane(normal=normal, origin=origin)
    polygon = self.perimeter_polygon
    part_1, part_2, intersection = polygon.divide(plane)
    surface_child = Surface(part_1, part_1, name=self.name, surface_type=self.type)
    rest_surface = Surface(part_2, part_2, name=self.name, surface_type=self.type)
    return surface_child, rest_surface, intersection

  @property
  def vegetation(self) -> Union[None, Vegetation]:
    """
    Get the vegetation construction at the external surface of the thermal boundary
    :return: None or Vegetation
    """
    return self._vegetation

  @vegetation.setter
  def vegetation(self, value):
    """
    Set the vegetation construction at the external surface of the thermal boundary
    :param value: Vegetation
    """
    self._vegetation = value

  @property
  def associated_thermal_boundaries(self) -> Union[None, List[ThermalBoundary]]:
    """
    Get the list of thermal boundaries that has this surface as external face
    :return: None or [ThermalBoundary]
    """
    return self._associated_thermal_boundaries

  @associated_thermal_boundaries.setter
  def associated_thermal_boundaries(self, value):
    """
    Set the list of thermal boundaries that has this surface as external face
    :param value: None or [ThermalBoundary]
    """
    self._associated_thermal_boundaries = value
