import pandas as pd
import numpy as np

import pytest
from .conftest import DATA_DIR, assert_series_equal
from numpy.testing import assert_allclose

from pvlib import temperature, tools
from pvlib._deprecation import pvlibDeprecationWarning

import re


@pytest.fixture
def sapm_default():
    return temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
        'open_rack_glass_glass']


def test_sapm_cell(sapm_default):
    default = temperature.sapm_cell(900, 20, 5, sapm_default['a'],
                                    sapm_default['b'], sapm_default['deltaT'])
    assert_allclose(default, 43.509, 3)


def test_sapm_module(sapm_default):
    default = temperature.sapm_module(900, 20, 5, sapm_default['a'],
                                      sapm_default['b'])
    assert_allclose(default, 40.809, 3)


def test_sapm_cell_from_module(sapm_default):
    default = temperature.sapm_cell_from_module(50, 900,
                                                sapm_default['deltaT'])
    assert_allclose(default, 50 + 900 / 1000 * sapm_default['deltaT'])


def test_sapm_ndarray(sapm_default):
    temps = np.array([0, 10, 5])
    irrads = np.array([0, 500, 0])
    winds = np.array([10, 5, 0])
    cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'],
                                       sapm_default['b'],
                                       sapm_default['deltaT'])
    module_temps = temperature.sapm_module(irrads, temps, winds,
                                           sapm_default['a'],
                                           sapm_default['b'])
    expected_cell = np.array([0., 23.06066166, 5.])
    expected_module = np.array([0., 21.56066166, 5.])
    assert_allclose(expected_cell, cell_temps, 3)
    assert_allclose(expected_module, module_temps, 3)


def test_sapm_series(sapm_default):
    times = pd.date_range(start='2015-01-01', end='2015-01-02', freq='12H')
    temps = pd.Series([0, 10, 5], index=times)
    irrads = pd.Series([0, 500, 0], index=times)
    winds = pd.Series([10, 5, 0], index=times)
    cell_temps = temperature.sapm_cell(irrads, temps, winds, sapm_default['a'],
                                       sapm_default['b'],
                                       sapm_default['deltaT'])
    module_temps = temperature.sapm_module(irrads, temps, winds,
                                           sapm_default['a'],
                                           sapm_default['b'])
    expected_cell = pd.Series([0., 23.06066166, 5.], index=times)
    expected_module = pd.Series([0., 21.56066166, 5.], index=times)
    assert_series_equal(expected_cell, cell_temps)
    assert_series_equal(expected_module, module_temps)


def test_pvsyst_cell_default():
    result = temperature.pvsyst_cell(900, 20, 5)
    assert_allclose(result, 45.137, 0.001)


def test_pvsyst_cell_kwargs():
    result = temperature.pvsyst_cell(900, 20, wind_speed=5.0, u_c=23.5,
                                     u_v=6.25, module_efficiency=0.1)
    assert_allclose(result, 33.315, 0.001)


def test_pvsyst_cell_ndarray():
    temps = np.array([0, 10, 5])
    irrads = np.array([0, 500, 0])
    winds = np.array([10, 5, 0])
    result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds)
    expected = np.array([0.0, 23.96551, 5.0])
    assert_allclose(expected, result, 3)


def test_pvsyst_cell_series():
    times = pd.date_range(start="2015-01-01", end="2015-01-02", freq="12H")
    temps = pd.Series([0, 10, 5], index=times)
    irrads = pd.Series([0, 500, 0], index=times)
    winds = pd.Series([10, 5, 0], index=times)

    result = temperature.pvsyst_cell(irrads, temps, wind_speed=winds)
    expected = pd.Series([0.0, 23.96551, 5.0], index=times)
    assert_series_equal(expected, result)


def test_pvsyst_cell_eta_m_deprecated():
    with pytest.warns(pvlibDeprecationWarning):
        result = temperature.pvsyst_cell(900, 20, wind_speed=5.0, u_c=23.5,
                                         u_v=6.25, eta_m=0.1)
        assert_allclose(result, 33.315, 0.001)


def test_faiman_default():
    result = temperature.faiman(900, 20, 5)
    assert_allclose(result, 35.203, atol=0.001)


def test_faiman_kwargs():
    result = temperature.faiman(900, 20, wind_speed=5.0, u0=22.0, u1=6.)
    assert_allclose(result, 37.308, atol=0.001)


def test_faiman_list():
    temps = [0, 10, 5]
    irrads = [0, 500, 0]
    winds = [10, 5, 0]
    result = temperature.faiman(irrads, temps, wind_speed=winds)
    expected = np.array([0.0, 18.446, 5.0])
    assert_allclose(expected, result, atol=0.001)


def test_faiman_ndarray():
    temps = np.array([0, 10, 5])
    irrads = np.array([0, 500, 0])
    winds = np.array([10, 5, 0])
    result = temperature.faiman(irrads, temps, wind_speed=winds)
    expected = np.array([0.0, 18.446, 5.0])
    assert_allclose(expected, result, atol=0.001)


def test_faiman_rad_no_ir():
    expected = temperature.faiman(900, 20, 5)
    result = temperature.faiman_rad(900, 20, 5)
    assert_allclose(result, expected)


def test_faiman_rad_ir():
    ir_down = np.array([0, 100, 200, 315.6574, 400])
    expected = [-11.111, -7.591, -4.071, -0.000, 2.969]
    result = temperature.faiman_rad(0, 0, 0, ir_down)
    assert_allclose(result, expected, atol=0.001)

    sky_view = np.array([1.0, 0.5, 0.0])
    expected = [-4.071, -2.036, 0.000]
    result = temperature.faiman_rad(0, 0, 0, ir_down=200,
                                    sky_view=sky_view)
    assert_allclose(result, expected, atol=0.001)

    emissivity = np.array([1.0, 0.88, 0.5, 0.0])
    expected = [-4.626, -4.071, -2.313, 0.000]
    result = temperature.faiman_rad(0, 0, 0, ir_down=200,
                                    emissivity=emissivity)
    assert_allclose(result, expected, atol=0.001)


def test_ross():
    result = temperature.ross(np.array([1000., 600., 1000.]),
                              np.array([20., 40., 60.]),
                              np.array([40., 100., 20.]))
    expected = np.array([45., 100., 60.])
    assert_allclose(expected, result)


def test_faiman_series():
    times = pd.date_range(start="2015-01-01", end="2015-01-02", freq="12H")
    temps = pd.Series([0, 10, 5], index=times)
    irrads = pd.Series([0, 500, 0], index=times)
    winds = pd.Series([10, 5, 0], index=times)

    result = temperature.faiman(irrads, temps, wind_speed=winds)
    expected = pd.Series([0.0, 18.446, 5.0], index=times)
    assert_series_equal(expected, result)


def test__temperature_model_params():
    params = temperature._temperature_model_params('sapm',
                                                   'open_rack_glass_glass')
    assert params == temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
        'open_rack_glass_glass']
    with pytest.raises(KeyError):
        temperature._temperature_model_params('sapm', 'not_a_parameter_set')


def _read_pvwatts_8760(filename):
    df = pd.read_csv(filename,
                     skiprows=17,  # ignore location/simulation metadata
                     skipfooter=1,  # ignore "Totals" row
                     engine='python')
    df['Year'] = 2019
    df.index = pd.to_datetime(df[['Year', 'Month', 'Day', 'Hour']])
    return df


@pytest.mark.parametrize('filename,inoct', [
    ('pvwatts_8760_rackmount.csv', 45),
    ('pvwatts_8760_roofmount.csv', 49),
])
def test_fuentes(filename, inoct):
    # Test against data exported from pvwatts.nrel.gov
    data = _read_pvwatts_8760(DATA_DIR / filename)
    data = data.iloc[:24*7, :]  # just use one week
    inputs = {
        'poa_global': data['Plane of Array Irradiance (W/m^2)'],
        'temp_air': data['Ambient Temperature (C)'],
        'wind_speed': data['Wind Speed (m/s)'],
        'noct_installed': inoct,
    }
    expected_tcell = data['Cell Temperature (C)']
    expected_tcell.name = 'tmod'
    actual_tcell = temperature.fuentes(**inputs)
    # the SSC implementation of PVWatts diverges from the Fuentes model at
    # at night by setting Tcell=Tamb when POA=0. This not only means that
    # nighttime values are slightly different (Fuentes models cooling to sky
    # at night), but because of the thermal inertia, there is a transient
    # error after dawn as well. Test each case separately:
    is_night = inputs['poa_global'] == 0
    is_dawn = is_night.shift(1) & ~is_night
    is_daytime = (inputs['poa_global'] > 0) & ~is_dawn
    # the accuracy is probably higher than 3 digits here, but the PVWatts
    # export data has low precision so can only test up to 3 digits
    assert_series_equal(expected_tcell[is_daytime].round(3),
                        actual_tcell[is_daytime].round(3))
    # use lower precision for dawn times to accommodate the dawn transient
    error = actual_tcell[is_dawn] - expected_tcell[is_dawn]
    assert (error.abs() < 0.1).all()
    # sanity check on night values -- Fuentes not much lower than PVWatts
    night_difference = expected_tcell[is_night] - actual_tcell[is_night]
    assert night_difference.max() < 6
    assert night_difference.min() > 0


@pytest.mark.parametrize('tz', [None, 'Etc/GMT+5'])
def test_fuentes_timezone(tz):
    index = pd.date_range('2019-01-01', freq='h', periods=3, tz=tz)

    df = pd.DataFrame({'poa_global': 1000, 'temp_air': 20, 'wind_speed': 1},
                      index)

    out = temperature.fuentes(df['poa_global'], df['temp_air'],
                              df['wind_speed'], noct_installed=45)

    assert_series_equal(out, pd.Series([47.85, 50.85, 50.85], index=index,
                                       name='tmod'))


def test_noct_sam():
    poa_global, temp_air, wind_speed, noct, module_efficiency = (
        1000., 25., 1., 45., 0.2)
    expected = 55.230790492
    result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct,
                                  module_efficiency)
    assert_allclose(result, expected)
    # test with different types
    result = temperature.noct_sam(np.array(poa_global), np.array(temp_air),
                                  np.array(wind_speed), np.array(noct),
                                  np.array(module_efficiency))
    assert_allclose(result, expected)
    dr = pd.date_range(start='2020-01-01 12:00:00', end='2020-01-01 13:00:00',
                       freq='1H')
    result = temperature.noct_sam(pd.Series(index=dr, data=poa_global),
                                  pd.Series(index=dr, data=temp_air),
                                  pd.Series(index=dr, data=wind_speed),
                                  pd.Series(index=dr, data=noct),
                                  module_efficiency)
    assert_series_equal(result, pd.Series(index=dr, data=expected))


def test_noct_sam_against_sam():
    # test is constructed to reproduce output from SAM v2020.11.29.
    # SAM calculation is the default Detailed PV System model (CEC diode model,
    # NOCT cell temperature model), with the only change being the soiling
    # loss is set to 0. Weather input is TMY3 for Phoenix AZ.
    # Values are taken from the Jan 1 12:00:00 timestamp.
    poa_total, temp_air, wind_speed, noct, module_efficiency = (
        860.673, 25, 3, 46.4, 0.20551)
    poa_total_after_refl = 851.458  # from SAM output
    # compute effective irradiance
    # spectral loss coefficients fixed in lib_cec6par.cpp
    a = np.flipud([0.918093, 0.086257, -0.024459, 0.002816, -0.000126])
    # reproduce SAM air mass calculation
    zen = 56.4284
    elev = 358
    air_mass = 1. / (tools.cosd(zen) + 0.5057 * (96.080 - zen)**-1.634)
    air_mass *= np.exp(-0.0001184 * elev)
    f1 = np.polyval(a, air_mass)
    effective_irradiance = f1 * poa_total_after_refl
    transmittance_absorptance = 0.9
    array_height = 1
    mount_standoff = 4.0
    result = temperature.noct_sam(poa_total, temp_air, wind_speed, noct,
                                  module_efficiency, effective_irradiance,
                                  transmittance_absorptance, array_height,
                                  mount_standoff)
    expected = 43.0655
    # rtol from limited SAM output precision
    assert_allclose(result, expected, rtol=1e-5)


def test_noct_sam_options():
    poa_global, temp_air, wind_speed, noct, module_efficiency = (
        1000., 25., 1., 45., 0.2)
    effective_irradiance = 1100.
    transmittance_absorptance = 0.8
    array_height = 2
    mount_standoff = 2.0
    result = temperature.noct_sam(poa_global, temp_air, wind_speed, noct,
                                  module_efficiency, effective_irradiance,
                                  transmittance_absorptance, array_height,
                                  mount_standoff)
    expected = 60.477703576
    assert_allclose(result, expected)


def test_noct_sam_errors():
    with pytest.raises(ValueError):
        temperature.noct_sam(1000., 25., 1., 34., 0.2, array_height=3)


def test_prilliman():
    # test against values calculated using pvl_MAmodel_2, see pvlib #1081
    times = pd.date_range('2019-01-01', freq='5min', periods=8)
    cell_temperature = pd.Series([0, 1, 3, 6, 10, 15, 21, 27], index=times)
    wind_speed = pd.Series([0, 1, 2, 3, 2, 1, 2, 3])

    # default coeffs
    expected = pd.Series([0, 0, 0.7047457, 2.21176412, 4.45584299, 7.63635512,
                          12.26808265, 18.00305776], index=times)
    actual = temperature.prilliman(cell_temperature, wind_speed, unit_mass=10)
    assert_series_equal(expected, actual)

    # custom coeffs
    coefficients = [0.0046, 4.5537e-4, -2.2586e-4, -1.5661e-5]
    expected = pd.Series([0, 0, 0.70716941, 2.2199537, 4.47537694, 7.6676931,
                          12.30423167, 18.04215198], index=times)
    actual = temperature.prilliman(cell_temperature, wind_speed, unit_mass=10,
                                   coefficients=coefficients)
    assert_series_equal(expected, actual)

    # even very short inputs < 20 minutes total still work
    times = pd.date_range('2019-01-01', freq='1min', periods=8)
    cell_temperature = pd.Series([0, 1, 3, 6, 10, 15, 21, 27], index=times)
    wind_speed = pd.Series([0, 1, 2, 3, 2, 1, 2, 3])
    expected = pd.Series([0, 0, 0.53557976, 1.49270094, 2.85940173,
                          4.63914366, 7.09641845, 10.24899272], index=times)
    actual = temperature.prilliman(cell_temperature, wind_speed, unit_mass=12)
    assert_series_equal(expected, actual)


def test_prilliman_coarse():
    # if the input series time step is >= 20 min, input is returned unchanged,
    # and a warning is emitted
    times = pd.date_range('2019-01-01', freq='30min', periods=3)
    cell_temperature = pd.Series([0, 1, 3], index=times)
    wind_speed = pd.Series([0, 1, 2])
    msg = re.escape("temperature.prilliman only applies smoothing when the "
                    "sampling interval is shorter than 20 minutes (input "
                    "sampling interval: 30.0 minutes); returning "
                    "input temperature series unchanged")
    with pytest.warns(UserWarning, match=msg):
        actual = temperature.prilliman(cell_temperature, wind_speed)
    assert_series_equal(cell_temperature, actual)


def test_prilliman_nans():
    # nans in inputs are handled appropriately; nans in input tcell
    # are ignored but nans in wind speed cause nan in output
    times = pd.date_range('2019-01-01', freq='1min', periods=8)
    cell_temperature = pd.Series([0, 1, 3, 6, 10, np.nan, 21, 27], index=times)
    wind_speed = pd.Series([0, 1, 2, 3, 2, 1, np.nan, 3])
    actual = temperature.prilliman(cell_temperature, wind_speed)
    expected = pd.Series([True, True, True, True, True, True, False, True],
                         index=times)
    assert_series_equal(actual.notnull(), expected)

    # check that nan temperatures do not mess up the weighted average;
    # the original implementation did not set weight=0 for nan values,
    # so the numerator of the weighted average ignored nans but the
    # denominator (total weight) still included the weight for the nan.
    cell_temperature = pd.Series([1, 1, 1, 1, 1, np.nan, 1, 1], index=times)
    wind_speed = pd.Series(1, index=times)
    actual = temperature.prilliman(cell_temperature, wind_speed)
    # original implementation would return some values < 1 here
    expected = pd.Series(1., index=times)
    assert_series_equal(actual, expected)


def test_glm_conversions():
    # it is easiest and sufficient to test conversion from  & to the same model
    glm = temperature.GenericLinearModel(module_efficiency=0.1,
                                         absorptance=0.9)

    inp = {'u0': 25.0, 'u1': 6.84}
    glm.use_faiman(**inp)
    out = glm.to_faiman()
    for k, v in inp.items():
        assert np.isclose(out[k], v)

    inp = {'u_c': 25, 'u_v': 4}
    glm.use_pvsyst(**inp)
    out = glm.to_pvsyst()
    for k, v in inp.items():
        assert np.isclose(out[k], v)

    # test with optional parameters
    inp = {'u_c': 25, 'u_v': 4,
           'module_efficiency': 0.15,
           'alpha_absorption': 0.95}
    glm.use_pvsyst(**inp)
    out = glm.to_pvsyst()
    for k, v in inp.items():
        assert np.isclose(out[k], v)

    inp = {'noct': 47}
    glm.use_noct_sam(**inp)
    out = glm.to_noct_sam()
    for k, v in inp.items():
        assert np.isclose(out[k], v)

    # test with optional parameters
    inp = {'noct': 47,
           'module_efficiency': 0.15,
           'transmittance_absorptance': 0.95}
    glm.use_noct_sam(**inp)
    out = glm.to_noct_sam()
    for k, v in inp.items():
        assert np.isclose(out[k], v)

    inp = {'a': -3.5, 'b': -0.1}
    glm.use_sapm(**inp)
    out = glm.to_sapm()
    for k, v in inp.items():
        assert np.isclose(out[k], v)


def test_glm_simulations():

    glm = temperature.GenericLinearModel(module_efficiency=0.1,
                                         absorptance=0.9)
    wind = np.array([1.4, 1/.51, 5.4])
    weather = (800, 20, wind)

    inp = {'u0': 20.0, 'u1': 5.0}
    glm.use_faiman(**inp)
    out = glm(*weather)
    expected = temperature.faiman(*weather, **inp)
    assert np.allclose(out, expected)

    out = glm(*weather)
    assert np.allclose(out, expected)

    out = glm(*weather, module_efficiency=0.1)
    assert np.allclose(out, expected)

    inp = glm.get_generic_linear()
    out = temperature.generic_linear(*weather, **inp)
    assert np.allclose(out, expected)


def test_glm_repr():

    glm = temperature.GenericLinearModel(module_efficiency=0.1,
                                         absorptance=0.9)
    inp = {'u0': 20.0, 'u1': 5.0}
    glm.use_faiman(**inp)
    expected = ("GenericLinearModel: {"
                "'u_const': 16.0, "
                "'du_wind': 4.0, "
                "'eta': 0.1, "
                "'alpha': 0.9}")

    assert glm.__repr__() == expected
