"""
This module contains functions for soiling models
"""

import datetime
import numpy as np
import pandas as pd
from scipy.special import erf

from pvlib.tools import cosd


def hsu(rainfall, cleaning_threshold, tilt, pm2_5, pm10,
        depo_veloc=None, rain_accum_period=pd.Timedelta('1h')):
    """
    Calculates soiling ratio given particulate and rain data using the
    Fixed Velocity model from Humboldt State University (HSU).

    The HSU soiling model [1]_ returns the soiling ratio, a value between zero
    and one which is equivalent to (1 - transmission loss). Therefore a soiling
    ratio of 1.0 is equivalent to zero transmission loss.

    Parameters
    ----------

    rainfall : Series
        Rain accumulated in each time period. [mm]

    cleaning_threshold : float
        Amount of rain in an accumulation period needed to clean the PV
        modules. [mm]

    tilt : float
        Tilt of the PV panels from horizontal. [degree]

    pm2_5 : numeric
        Concentration of airborne particulate matter (PM) with
        aerodynamic diameter less than 2.5 microns. [g/m^3]

    pm10 : numeric
        Concentration of airborne particulate matter (PM) with
        aerodynamicdiameter less than 10 microns. [g/m^3]

    depo_veloc : dict, default {'2_5': 0.0009, '10': 0.004}
        Deposition or settling velocity of particulates. [m/s]

    rain_accum_period : Timedelta, default 1 hour
        Period for accumulating rainfall to check against `cleaning_threshold`
        It is recommended that `rain_accum_period` be between 1 hour and
        24 hours.

    Returns
    -------
    soiling_ratio : Series
        Values between 0 and 1. Equal to 1 - transmission loss.

    References
    -----------
    .. [1] M. Coello and L. Boyle, "Simple Model For Predicting Time Series
       Soiling of Photovoltaic Panels," in IEEE Journal of Photovoltaics.
       doi: 10.1109/JPHOTOV.2019.2919628
    .. [2] Atmospheric Chemistry and Physics: From Air Pollution to Climate
       Change. J. Seinfeld and S. Pandis. Wiley and Sons 2001.

    """
    # never use mutable input arguments
    if depo_veloc is None:
        depo_veloc = {'2_5': 0.0009, '10': 0.004}

    # accumulate rainfall into periods for comparison with threshold
    accum_rain = rainfall.rolling(rain_accum_period, closed='right').sum()
    # cleaning is True for intervals with rainfall greater than threshold
    cleaning_times = accum_rain.index[accum_rain >= cleaning_threshold]

    # determine the time intervals in seconds (dt_sec)
    dt = rainfall.index
    # subtract shifted values from original and convert to seconds
    dt_diff = (dt[1:] - dt[:-1]).total_seconds()
    # ensure same number of elements in the array, assuming that the interval
    # prior to the first value is equal in length to the first interval
    dt_sec = np.append(dt_diff[0], dt_diff).astype('float64')

    horiz_mass_rate = (
        pm2_5 * depo_veloc['2_5'] + np.maximum(pm10 - pm2_5, 0.)
        * depo_veloc['10']) * dt_sec
    tilted_mass_rate = horiz_mass_rate * cosd(tilt)  # assuming no rain

    # tms -> tilt_mass_rate
    tms_cumsum = np.cumsum(tilted_mass_rate * np.ones(rainfall.shape))

    mass_no_cleaning = pd.Series(index=rainfall.index, data=tms_cumsum)
    # specify dtype so pandas doesn't assume object
    mass_removed = pd.Series(index=rainfall.index, dtype='float64')
    mass_removed[0] = 0.
    mass_removed[cleaning_times] = mass_no_cleaning[cleaning_times]
    accum_mass = mass_no_cleaning - mass_removed.ffill()

    soiling_ratio = 1 - 0.3437 * erf(0.17 * accum_mass**0.8473)

    return soiling_ratio


def kimber(rainfall, cleaning_threshold=6, soiling_loss_rate=0.0015,
           grace_period=14, max_soiling=0.3, manual_wash_dates=None,
           initial_soiling=0, rain_accum_period=24):
    """
    Calculates fraction of energy lost due to soiling given rainfall data and
    daily loss rate using the Kimber model.

    Kimber soiling model [1]_ assumes soiling builds up at a daily rate unless
    the daily rainfall is greater than a threshold. The model also assumes that
    if daily rainfall has exceeded the threshold within a grace period, then
    the ground is too damp to cause soiling build-up. The model also assumes
    there is a maximum soiling build-up. Scheduled manual washes and rain
    events are assumed to reset soiling to zero.

    Parameters
    ----------
    rainfall: pandas.Series
        Accumulated rainfall at the end of each time period. [mm]
    cleaning_threshold: float, default 6
        Amount of daily rainfall required to clean the panels. [mm]
    soiling_loss_rate: float, default 0.0015
        Fraction of energy lost due to one day of soiling. [unitless]
    grace_period : int, default 14
        Number of days after a rainfall event when it's assumed the ground is
        damp, and so it's assumed there is no soiling. [days]
    max_soiling : float, default 0.3
        Maximum fraction of energy lost due to soiling. Soiling will build up
        until this value. [unitless]
    manual_wash_dates : sequence or None, default None
        List or tuple of dates as Python ``datetime.date`` when the panels were
        washed manually. Note there is no grace period after a manual wash, so
        soiling begins to build up immediately.
    initial_soiling : float, default 0
        Initial fraction of energy lost due to soiling at time zero in the
        `rainfall` series input. [unitless]
    rain_accum_period : int, default 24
        Period for accumulating rainfall to check against `cleaning_threshold`.
        The Kimber model defines this period as one day. [hours]

    Returns
    -------
    pandas.Series
        fraction of energy lost due to soiling, has same intervals as input

    Notes
    -----
    The soiling loss rate depends on both the geographical region and the
    soiling environment type. Rates measured by Kimber [1]_ are summarized in
    the following table:

    ===================  =======  =========  ======================
    Region/Environment   Rural    Suburban   Urban/Highway/Airport
    ===================  =======  =========  ======================
    Central Valley       0.0011   0.0019     0.0020
    Northern CA          0.0011   0.0010     0.0016
    Southern CA          0        0.0016     0.0019
    Desert               0.0030   0.0030     0.0030
    ===================  =======  =========  ======================

    Rainfall thresholds and grace periods may also vary by region. Please
    consult [1]_ for more information.

    References
    ----------
    .. [1] "The Effect of Soiling on Large Grid-Connected Photovoltaic Systems
       in California and the Southwest Region of the United States," Adrianne
       Kimber, et al., IEEE 4th World Conference on Photovoltaic Energy
       Conference, 2006, :doi:`10.1109/WCPEC.2006.279690`
    """
    # convert rain_accum_period to timedelta
    rain_accum_period = datetime.timedelta(hours=rain_accum_period)

    # convert grace_period to timedelta
    grace_period = datetime.timedelta(days=grace_period)

    # get indices as numpy datetime64, calculate timestep as numpy timedelta64,
    # and convert timestep to fraction of days
    rain_index_vals = rainfall.index.values
    timestep_interval = (rain_index_vals[1] - rain_index_vals[0])
    day_fraction = timestep_interval / np.timedelta64(24, 'h')

    # accumulate rainfall
    accumulated_rainfall = rainfall.rolling(
        rain_accum_period, closed='right').sum()

    # soiling rate
    soiling = np.ones_like(rainfall.values) * soiling_loss_rate * day_fraction
    soiling[0] = initial_soiling
    soiling = np.cumsum(soiling)
    soiling = pd.Series(soiling, index=rainfall.index, name='soiling')

    # rainfall events that clean the panels
    rain_events = accumulated_rainfall > cleaning_threshold

    # grace periods windows during which ground is assumed damp, so no soiling
    grace_windows = rain_events.rolling(grace_period, closed='right').sum() > 0

    # clean panels by subtracting soiling for indices in grace period windows
    cleaning = pd.Series(float('NaN'), index=rainfall.index)
    cleaning.iloc[0] = 0.0
    cleaning[grace_windows] = soiling[grace_windows]

    # manual wash dates
    if manual_wash_dates is not None:
        rain_tz = rainfall.index.tz
        # convert manual wash dates to datetime index in the timezone of rain
        manual_wash_dates = pd.DatetimeIndex(manual_wash_dates, tz=rain_tz)
        cleaning[manual_wash_dates] = soiling[manual_wash_dates]

    # remove soiling by foward filling cleaning where NaN
    soiling -= cleaning.ffill()

    # check if soiling has reached the maximum
    return soiling.where(soiling < max_soiling, max_soiling)
