"""Solar model calculation the radiations on a given unit surface with a specific direction.

Author: stephane.ploix@grenoble-inp.fr
"""

from math import pi, cos, sin, acos, asin, atan, sqrt, exp, log, atan2
from pytz import utc
import matplotlib.pylab
from buildingenergy import timemg

DIRECTION = {'SOUTH': 0, 'EAST': -90, 'WEST': 90, 'NORTH': 180}
SLOPE = {'HORIZONTAL': 0, 'VERTICAL': 90} # solar captor angle with meaning facing the ground


class SolarModel():
    """Model using the position of the sun and weather data to compute the resulting solar radiations on a directed ground surface."""

    def __init__(self, site_weather_data):
        """Create a site solar model.

        :param site_weather_data: object generated by an OpenWeatherMapJsonReader, that contains site data like location, altitude,... (see openweather.SiteWeatherData)
        :type site_weather_data: openweather.SiteWeatherData
        """
        self.time_zone = site_weather_data.timezone
        self.latitude_in_rad = site_weather_data.latitude_in_deg / 180 * pi
        self.longitude_in_rad = site_weather_data.longitude_in_deg / 180 * pi
        self.sea_level_in_meters = site_weather_data.sea_level_in_meter
        self.albedo = site_weather_data.albedo
        self._site_weather_data = site_weather_data

    @property
    def site_data(self):
        """Return site data.

        :return: see openweather.SiteWeatherData
        """
        return self._site_weather_data

    def solar_beam_transfer(self, phis_with_nebulosity, altitude_in_deg, temperature, humidity, pollution):
        """Return solar beam transfer.

        :param phis_with_nebulosity: solar power with nebolusity (cloudiness)
        :type phis_with_nebulosity: float
        :param altitude_in_deg: altitude in degrees from the tangente plan with a South direction
        :type altitude_in_deg: float
        :param temperature: outdoor temperature
        :type temperature: float
        :param humidity: outdoor humidity
        :type humidity: float
        :param pollution: pollution coefficient between 0 and 1
        :type pollution: float
        :return: phi_direct_atmosphere and transmissivity
        :rtype: list[float]
        """
        altitude_in_rad = altitude_in_deg / 180 * pi
        atmospheric_pressure = 101325 * (1 - 2.257e-5 * self.sea_level_in_meters) ** 5.256
        air_mass = atmospheric_pressure / (101325 * sin(altitude_in_rad) + 15198.75 * (3.885 + altitude_in_rad) ** (-1.253))
        if 0 < altitude_in_rad < pi:
            transmissivity = 0.6 ** ((sqrt(1229 + (614 * sin(altitude_in_rad)) ** 2) - 614 * sin(altitude_in_rad)) * ((288 - 0.0065 * self.sea_level_in_meters) / 288) ** 5.256)
            Erayleigh = 1 / (0.9 * air_mass + 9.4)
            Pv = 2.165 * (1.098 + temperature / 100) ** 8.02 * humidity
            lLinke = 2.4 + 14.6 * pollution + 0.4 * (1 + 2 * pollution) * log(Pv)
            phi_direct_atmosphere = transmissivity * phis_with_nebulosity * exp(-air_mass * Erayleigh * lLinke)
            return phi_direct_atmosphere, transmissivity
        return 0, 0

    def solar_time(self, administrative_datetime):
        """Calculate the solar time from administrative.

        :param administrative_datetime: the date time
        :type administrative_datetime: datetime.datetime
        :return: elevation_in_rad, azimuth_in_rad, hour_angle_in_rad, latitude_in_rad, declination_in_rad
        :rtype: tuple[float]
        """
        utc_datetime = administrative_datetime.astimezone(utc)
        utc_timetuple = utc_datetime.timetuple()
        day_in_year = utc_timetuple.tm_yday
        hour_in_day = utc_timetuple.tm_hour
        minute_in_hour = utc_timetuple.tm_min
        seconde_in_minute = utc_timetuple.tm_sec
        standard_time_in_seconds = hour_in_day * 3600 + minute_in_hour * 60 + seconde_in_minute + self.longitude_in_rad / pi * 180 * 4 * 60
        coefsin1 = 0.001868
        coefcos1 = 0.032077
        angle1 = atan(coefsin1 / coefcos1)
        deltaday1 = angle1 / (2 * pi) * 365
        coef1 = sqrt(coefcos1 ** 2 + coefsin1 ** 2)
        coefsin2 = 0.014615
        coefcos2 = 0.04089
        angle2 = atan(coefsin2 / coefcos2)
        coef2 = sqrt(coefcos2 ** 2 + coefsin2 ** 2)
        deltaday2 = angle2 / (4 * pi) * 365
        solartime_in_secondes = standard_time_in_seconds + 229.2 * 60 * (0.000075 + coef1 * sin(2 * pi * (deltaday1 - day_in_year) / 365) - coef2 * sin(4 * pi * (deltaday2 + day_in_year) / 365))
        return day_in_year, solartime_in_secondes

    def solar_angle(self, administrative_datetime):
        """Return to the basic calculation of solar angles.

        :param administrative_datetime: administrative time
        :type administrative_datetime: datetime.datetime
        :return: the altitude of the sun in the sky from the horizontal plan to the sun altitude, the azimut is the angle with south direction in the horizontal plan
        :rtype: tuple[float]
        """
        day_in_year, solartime_in_secondes = self.solar_time(administrative_datetime)
        declination_in_rad = 23.45 * pi / 180 * sin(2 * pi * (285 + day_in_year) / 365)
        hour_angle_in_rad = pi / 12 * (solartime_in_secondes / 3600 - 12)
        altitude_in_rad = asin(sin(declination_in_rad) * sin(self.latitude_in_rad) + cos(declination_in_rad) * cos(self.latitude_in_rad) * cos(hour_angle_in_rad))
        cos_azimuth = (cos(declination_in_rad) * cos(hour_angle_in_rad) * sin(self.latitude_in_rad) - sin(declination_in_rad) * cos(self.latitude_in_rad)) / cos(altitude_in_rad)
        sin_azimuth = cos(declination_in_rad) * sin(hour_angle_in_rad) / cos(altitude_in_rad)
        azimuth_in_rad = atan2(sin_azimuth, cos_azimuth)
        return altitude_in_rad/pi*180, azimuth_in_rad/pi*180

    def solar_irradiation(self, exposure_in_deg, slope_in_deg, administrative_datetime, temperature, humidity, nebulosity_in_percentage, pollution = 0.1):
        """Compute the solar power on a 1m2 flat surface.

        :param exposure_in_deg: angle of the surface with the north. O means north oriented, -90 means West, 90 East and pi South oriented
        :type exposure_in_deg: float
        :param slope_in_deg: angle in radians between the ground and the back of the exposed surface. 0 means horizontal directed to the sky zenith and 90 means vertical
        :type slope_in_deg: float
        :param administrative_datetime: hour in the day
        :type administrative_datetime: datetime.datetime
        :param temperature: outdoor temperature
        :type temperature: float
        :param humidity: outdoor humidity
        :type humidity: float
        :param nebulosity_in_percentage: cloudiness ie percentage of the sky covered by clouds
        :type nebulosity_in_percentage: float
        :param pollution: pollution rate
        :type pollution: float
        :return: phi_total, phi_direct_collected, phi_diffuse, phi_reflected
        :rtype: list[float]
        """
        day_in_year, _ = self.solar_time(administrative_datetime)
        altitude_in_deg, azimuth_in_deg = self.solar_angle(administrative_datetime)
        altitude_in_rad, azimuth_in_rad = altitude_in_deg / 180 * pi, azimuth_in_deg / 180 * pi
        slope_in_rad, exposure_in_rad = slope_in_deg / 180 * pi, exposure_in_deg / 180 * pi
        phis = 1367 * (1 + 0.033 * cos(2 * pi * day_in_year / 365))
        phis_with_nebulosity = (1 - 0.75 * (nebulosity_in_percentage/100) ** 3.4) * phis
        phi_direct_atmosphere, transmitivity = self.solar_beam_transfer(phis_with_nebulosity, altitude_in_deg, temperature, humidity, pollution)
        incidence_in_rad = acos(cos(altitude_in_rad) * sin(slope_in_rad) * cos(azimuth_in_rad + exposure_in_rad) + sin(altitude_in_rad) * cos(slope_in_rad))

        if 0 < altitude_in_rad:
            phi_direct_collected = incidence_in_rad * phi_direct_atmosphere
            phi_diffuse = max(0, phis_with_nebulosity * (0.271 - 0.294 * transmitivity) * sin(altitude_in_rad))
            phi_reflected = max(0, self.albedo * phis_with_nebulosity * (0.271 + 0.706 * transmitivity) * sin(altitude_in_rad) * cos(slope_in_rad / 2) ** 2)
        else:
            phi_direct_collected = 0
            phi_diffuse = 0
            phi_reflected = 0
        return phi_direct_collected + phi_diffuse + phi_reflected, phi_direct_collected, phi_diffuse, phi_reflected

    def solar_irradiations(self, slope_in_deg=90, exposure_in_deg=0):
        """Compute total, direct, diffuse and reflected solar irradiation on a directed unit surface for the dates corresponding to dates in site_weather_data.

        :param slope_in_deg: slope of surface i.e. angle between a tangential plan and the surface (0° means facing ground, 90° vertical and 180° facing the sky)
        :type slope_in_deg: float
        :param exposure_in_deg: angle between the south direction and the normal to the surface projected in the tangential plan (0° means south directed, -90° east and 90° west)
        :type exposure_in_deg: float
        :return: dictionnary of list of powers with keywords 'total', 'direct', 'diffuse' and 'reflected'
        :rtype: dict[str,list[float]]
        """
        phis = {'total':[], 'direct':[], 'diffuse': [], 'reflected': []}
        for i in range(len(self._site_weather_data.get('datetime'))):
            irradiations = self.solar_irradiation(exposure_in_deg=180-exposure_in_deg, slope_in_deg=slope_in_deg, administrative_datetime=self._site_weather_data.get('datetime')[i], temperature=self._site_weather_data.get('temperature')[i], humidity=self._site_weather_data.get('humidity')[i], nebulosity_in_percentage=self._site_weather_data.get('cloudiness')[i], pollution=self._site_weather_data.pollution)
            phis['total'].append(irradiations[0])
            phis['direct'].append(irradiations[1])
            phis['diffuse'].append(irradiations[2])
            phis['reflected'].append(irradiations[3])
        return phis

    def plot_heliodon(self, year, name=''):
        """Plot heliodon at current location.

        :param year: year to be displayed in figure
        :type year: int
        :param name: file_name to be displayed in figure, defaul to ''
        :type name: str
        """
        days = ['21/%i/%i' % (i, year) for i in range(1, 13, 1)]
        legends = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
        hours_elevation = dict()
        hours_azimuth = dict()
        matplotlib.pylab.figure()
        for d in range(len(days)):
            altitudes_in_deg, azimuths_in_deg = [], []
            for h in range(0,24,1):
                for m in range(0,60,10):
                    altitude_in_deg, azimuth_in_deg = self.solar_angle(timemg.stringdate_to_datetime(days[d] + ' %i:%i:0' % (h, m)))
                    if altitude_in_deg > 0:
                        altitudes_in_deg.append(altitude_in_deg)
                        azimuths_in_deg.append(azimuth_in_deg)
                        if m==0 and h not in hours_elevation:
                            hours_elevation[h] = [altitude_in_deg]
                            hours_azimuth[h] = [azimuth_in_deg]
                        elif m==0:
                            hours_elevation[h].append(altitude_in_deg)
                            hours_azimuth[h].append(azimuth_in_deg)
                matplotlib.pylab.plot(azimuths_in_deg, altitudes_in_deg)
            i_position = (d%6)*len(altitudes_in_deg)//6
            matplotlib.pylab.annotate(legends[d],(azimuths_in_deg[i_position], altitudes_in_deg[i_position]))
        matplotlib.pylab.title('heliodon %s (21th of each month)'%name)
        for h in hours_elevation:
            matplotlib.pylab.plot(hours_azimuth[h], hours_elevation[h], ':k')
            matplotlib.pylab.annotate(h, (hours_azimuth[h][len(hours_azimuth[h])//2], hours_elevation[h][len(hours_azimuth[h])//2]))
        matplotlib.pylab.axis('tight')
        matplotlib.pylab.grid()

    def plot_angles(self):
        """Plot solar angles for the dates corresponding to dates in site_weather_data."""
        matplotlib.pylab.figure()
        altitudes, azimuths, hour_angles, latitudes, declinations = [], [], [], [], []
        datetimes = self.site_data.get('datetime')
        for dt in datetimes:
            angles = self.solar_angle(dt)
            altitudes.append(angles[0])
            azimuths.append(angles[1])
        matplotlib.pylab.plot(datetimes, altitudes, datetimes, azimuths)
        matplotlib.pylab.legend(('altitude in deg', 'azimuth in deg'))
        matplotlib.pylab.axis('tight')
        matplotlib.pylab.grid()

    def plot_solar_cardinal_irradiations(self):
        """Plot total solar irradiation on all cardinal direction and an horizontal one, for the dates corresponding to dates in site_weather_data."""
        matplotlib.pylab.figure()
        directions = ('SOUTH', 'EAST', 'WEST', 'NORTH')
        phis = self.solar_irradiations(exposure_in_deg=DIRECTION['SOUTH'], slope_in_deg=180)['total']
        print('energy HORIZONTAL:', sum(phis)/1000, 'kWh/m2')
        matplotlib.pylab.plot(self._site_weather_data.get('datetime'), phis)
        for d in range(4):
            phis = self.solar_irradiations( exposure_in_deg=DIRECTION[directions[d]], slope_in_deg=90)['total']
            print('energy', directions[d], ':', sum(phis)/1000, 'kWh/m2')
            matplotlib.pylab.plot(self._site_weather_data.get('datetime'), phis)
        matplotlib.pylab.legend(('HORIZONTAL', 'SOUTH', 'EAST', 'WEST', 'NORTH'))
        matplotlib.pylab.ylabel('Watt')
        matplotlib.pylab.axis('tight')
        matplotlib.pylab.grid()


def regular_angle_to_decimal_angle_converter(decimals, minutes, seconds):
    """Convert decimals, minutes, seconds to float value.

    :param decimals: number of degrees as an integer
    :type decimals: int
    :param minutes: number of minutes
    :type minutes: int
    :param seconds: number of seconds
    :type seconds: int
    :return: angle in decimal format
    :rtype: float
    """
    return decimals+minutes/60+seconds/3600
