# Copyright 2023 Alireza Aghamohammadi

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import math

import numpy as np
from scipy import integrate, optimize  # type: ignore

from .atmospheric_transmission import AtmosphericTransmission
from .irradiance import SolarIrradiance
from .observer import Observer
from .sun_position import SunPosition


class IrradiationCalculator:
    r"""
    A class to find the optimal orientation and
    calculate the direct irradiation for a solar panel [1]_.

    References
    ----------
    .. [1] Aghamohammadi, A., & Foulaadvand, M. (2023).
            Efficiency comparison between tracking and
            optimally fixed flat solar collectors. Scientific Reports, 13(1).


    """

    OMEGA = 7.2722 * 1e-5

    def __init__(
        self, climate_type: str, observer_altitude: int, observer_latitude: float
    ):
        """
        To instantiate the ``IrradiationCalculator`` class,
        provide the following three parameters.

        :param climate_type: The type of climate.
        :type climate_type: str
        :param observer_altitude: The altitude of the observer in meters.
        :type observer_altitude: int
        :param observer_latitude: The latitude of the observer in degrees.
        :type observer_latitude: float
        """
        self._observer = Observer(observer_latitude=observer_latitude)
        self._sun_position = SunPosition()
        self._solar_irradiance = SolarIrradiance(self._sun_position)
        self._atmospheric_transmission = AtmosphericTransmission(
            climate_type, observer_altitude, observer_latitude
        )

    @staticmethod
    def _heaviside(x: float) -> int:
        """
        Heaviside step function.

        :param x: Input to the function.
        :type x: float
        :return: 1 if x >= 0, else 0.
        :rtype: int
        """
        return 1 if x >= 0 else 0

    def _calculate_irradiance_component(
        self, hour_angle: float, panel_orientation: float, day_of_year: int
    ) -> float:
        r"""
        Calculate a component of the irradiance integral.

        :param hour_angle: The hour angle.
        :type hour_angle: float
        :param panel_orientation: The orientation of the solar panel in radians.
        :type panel_orientation: float
        :param day_of_year: The day of the year.
        :type day_of_year: int
        :return: A component of the irradiance integral.
        :rtype: float
        """
        observer_latitude = self._observer._ensure_latitude_provided()
        solar_declination = self._sun_position.solar_declination(day_of_year)
        cos_theta = math.sin(solar_declination) * math.sin(
            observer_latitude - panel_orientation
        ) + math.cos(solar_declination) * math.cos(hour_angle) * math.cos(
            observer_latitude - panel_orientation
        )
        transmittance = self._atmospheric_transmission.calculate_transmittance(
            day_of_year, self._sun_position.solar_time(hour_angle)
        )
        irradiance = self._solar_irradiance.calculate_extraterrestrial_irradiance(
            day_of_year
        )
        return (
            irradiance
            * transmittance
            * cos_theta
            * self._heaviside(cos_theta)
            / self.OMEGA
        )

    def calculate_direct_irradiation(
        self, panel_orientation: float, day_of_year: int
    ) -> float:
        r"""
        Calculate the direct irradiation
        for a given solar panel orientation (i.e., :math:`\beta`).

        | The direct irradiation is calculated using the formula:

        .. math::
            E(n,\phi) = \frac{I(n)}{\Omega} \int_{\omega_s}^{\omega_t}
            \cos(\theta) \times H(\cos(\theta)) \times \tau_b~d\omega


        | - :math:`n` is the day of year (i.e., ``day_of_year``)

        | - :math:`\phi` is the latitude of the observer

        | - :math:`I(n)`  is the amount of
                solar energy received per unit area per second
                on day number :math:`n` of the year

        | - :math:`\Omega` = ``7.2722 * 1e-5``

        | - :math:`\theta` is incidence angle, the angle between the position vector
                of the sun and the normal vector to the solar panel.

        | - :math:`\omega_s` is the sunrise hour angle

        | - :math:`\omega_t` is the sunset hour angle

        | - :math:`H` is the Heaviside step function


        :param panel_orientation: The orientation of the solar panel in degrees.
        :type panel_orientation: float
        :param day_of_year: The day of the year.
        :type day_of_year: int
        :return: The direct irradiation in Megajoules per square meter.
        :rtype: float
        """
        sunrise_hour_angle, sunset_hour_angle = self._observer.calculate_sunrise_sunset(
            day_of_year
        )
        panel_orientation = math.radians(panel_orientation)
        irradiance_components = [
            self._calculate_irradiance_component(
                hour_angle, panel_orientation, day_of_year
            )
            for hour_angle in np.arange(sunrise_hour_angle, sunset_hour_angle, 0.01)
        ]

        # During polar night, the sun doesn't rise and both hour angles are zero.
        # This results in an empty irradiance_components list. In this case,
        # we return 0 as there is no direct irradiation.
        if not irradiance_components:
            return 0

        return integrate.simpson(irradiance_components, dx=0.01)

    def find_optimal_orientation(self, day_of_year: int) -> float:
        """
        Find the optimal orientation :math:`beta` that maximizes
        the direct irradiation.

        :param day_of_year: The day of the year.
        :type day_of_year: int
        :return: The optimal orientation (i.e., :math:`beta`) in degrees.
        :rtype: float
        """

        def neg_irradiation(beta: float):
            # We negate the irradiation because we're minimizing
            return -self.calculate_direct_irradiation(math.degrees(beta), day_of_year)

        result = optimize.minimize_scalar(
            neg_irradiation, bounds=(-math.pi / 2, math.pi / 2), method="bounded"
        )
        optimal_beta = result.x

        # Check if irradiance_components is empty
        if not self.calculate_direct_irradiation(
            math.degrees(optimal_beta), day_of_year
        ):
            observer_latitude = self._observer._ensure_latitude_provided()
            # Return 90 or -90 based on observer_latitude
            return 90 if observer_latitude >= 0 else -90

        return math.degrees(optimal_beta)
