"""
The ``sde`` module contains functions to fit the single diode equation.

Function names should follow the pattern "fit_" + fitting method.

"""

import numpy as np

from pvlib.ivtools.utils import _schumaker_qspline


# set constant for numpy.linalg.lstsq parameter rcond
# rcond=-1 for numpy<1.14, rcond=None for numpy>=1.14
# TODO remove after minimum numpy version >= 1.14
minor = int(np.__version__.split('.')[1])
if minor < 14:
    RCOND = -1
else:
    RCOND = None


def fit_sandia_simple(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None,
                      vlim=0.2, ilim=0.1):
    r"""
    Fits the single diode equation (SDE) to an IV curve.

    Parameters
    ----------
    voltage : ndarray
        1D array of `float` type containing voltage at each point on the IV
        curve, increasing from 0 to ``v_oc`` inclusive. [V]

    current : ndarray
        1D array of `float` type containing current at each point on the IV
        curve, from ``i_sc`` to 0 inclusive. [A]

    v_oc : float, default None
        Open circuit voltage. If not provided, ``v_oc`` is taken as the
        last point in the ``voltage`` array. [V]

    i_sc : float, default None
        Short circuit current. If not provided, ``i_sc`` is taken as the
        first point in the ``current`` array. [A]

    v_mp_i_mp : tuple of float, default None
        Voltage, current at maximum power point. If not provided, the maximum
        power point is found at the maximum of ``voltage`` \times ``current``.
        [V], [A]

    vlim : float, default 0.2
        Defines portion of IV curve where the exponential term in the single
        diode equation can be neglected, i.e.
        ``voltage`` <= ``vlim`` x ``v_oc``. [V]

    ilim : float, default 0.1
        Defines portion of the IV curve where the exponential term in the
        single diode equation is significant, approximately defined by
        ``current`` < (1 - ``ilim``) x ``i_sc``. [A]

    Returns
    -------
    photocurrent : float
        photocurrent [A]
    saturation_current : float
        dark (saturation) current [A]
    resistance_series : float
        series resistance [ohm]
    resistance_shunt : float
        shunt (parallel) resistance [ohm]
    nNsVth : float
        product of thermal voltage ``Vth`` [V], diode ideality factor
        ``n``, and number of series cells ``Ns``. [V]

    Raises
    ------
    RuntimeError if parameter extraction is not successful.

    Notes
    -----
    Inputs ``voltage``, ``current``, ``v_oc``, ``i_sc`` and ``v_mp_i_mp`` are
    assumed to be from a single IV curve at constant irradiance and cell
    temperature.

    :py:func:`fit_sandia_simple` obtains values for the five parameters for
    the single diode equation [1]_:

    .. math::

        I = I_{L} - I_{0} (\exp \frac{V + I R_{s}}{nNsVth} - 1)
        - \frac{V + I R_{s}}{R_{sh}}

    See :py:func:`pvlib.pvsystem.singlediode` for definition of the parameters.

    The extraction method [2]_ proceeds in six steps.

    1. In the single diode equation, replace :math:`R_{sh} = 1/G_{p}` and
       re-arrange

    .. math::

        I = \frac{I_{L}}{1 + G_{p} R_{s}} - \frac{G_{p} V}{1 + G_{p} R_{s}}
        - \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nN_sV_{th}})
        - 1)

    2. The linear portion of the IV curve is defined as
       :math:`V \le vlim \times v_{oc}`. Over this portion of the IV curve,

    .. math::

        \frac{I_{0}}{1 + G_{p} R_{s}} (\exp(\frac{V + I R_{s}}{nN_sV_{th}})
        - 1) \approx 0

    3. Fit the linear portion of the IV curve with a line.

    .. math::

        I &\approx \frac{I_{L}}{1 + G_{p} R_{s}}
        - \frac{G_{p}}{1 + G_{p}R_{s}} V
        &= \beta_{0} + \beta_{1} V

    4. The exponential portion of the IV curve is defined by
       :math:`\beta_{0} + \beta_{1} \times V - I > ilim \times i_{sc}`.
       Over this portion of the curve,
       :math:`\exp((V + IR_s)/{nN_sV_{th}}) \gg 1` so that

    .. math::

        \exp(\frac{V + I R_{s}}{nN_sV_{th}}) - 1 \approx
        \exp(\frac{V + I R_{s}}{nN_sV_{th}})

    5. Fit the exponential portion of the IV curve.

    .. math::

        \log(\beta_{0} - \beta_{1} V - I)
        &\approx \log(\frac{I_{0}}{1 + G_{p} R_{s}} + \frac{V}{nN_sV_{th}}
        + \frac{I R_{s}}{nN_sV_{th}}) \\
        &= \beta_{2} + \beta_{3} V + \beta_{4} I

    6. Calculate values for ``IL, I0, Rs, Rsh,`` and ``nNsVth`` from the
       regression coefficents :math:`\beta_{0}, \beta_{1}, \beta_{3}` and
       :math:`\beta_{4}`.


    References
    ----------
    .. [1] S.R. Wenham, M.A. Green, M.E. Watt, "Applied Photovoltaics" ISBN
       0 86758 909 4
    .. [2] C. B. Jones, C. W. Hansen, "Single Diode Parameter Extraction from
       In-Field Photovoltaic I-V Curves on a Single Board Computer", 46th IEEE
       Photovoltaic Specialist Conference, Chicago, IL, 2019
    """

    # If not provided, extract v_oc, i_sc, v_mp and i_mp from the IV curve data
    if v_oc is None:
        v_oc = voltage[-1]
    if i_sc is None:
        i_sc = current[0]
    if v_mp_i_mp is not None:
        v_mp, i_mp = v_mp_i_mp
    else:
        v_mp, i_mp = _find_mp(voltage, current)

    # Find beta0 and beta1 from linear portion of the IV curve
    beta0, beta1 = _sandia_beta0_beta1(voltage, current, vlim, v_oc)

    # Find beta3 and beta4 from the exponential portion of the IV curve
    beta3, beta4 = _sandia_beta3_beta4(voltage, current, beta0, beta1, ilim,
                                       i_sc)

    # calculate single diode parameters from regression coefficients
    return _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc)


def _find_mp(voltage, current):
    """
    Finds voltage and current at maximum power point.

    Parameters
    ----------
    voltage : ndarray
        1D array containing voltage at each point on the IV curve, increasing
        from 0 to v_oc inclusive, of `float` type. [V]

    current : ndarray
        1D array containing current at each point on the IV curve, decreasing
        from i_sc to 0 inclusive, of `float` type. [A]

    Returns
    -------
    v_mp, i_mp : tuple
        voltage ``v_mp`` and current ``i_mp`` at the maximum power point. [V],
        [A]
    """
    p = voltage * current
    idx = np.argmax(p)
    return voltage[idx], current[idx]


def _sandia_beta0_beta1(v, i, vlim, v_oc):
    # Used by fit_sandia_simple.
    # Get intercept and slope of linear portion of IV curve.
    # Start with V =< vlim * v_oc, extend by adding points until slope is
    # negative (downward).
    beta0 = np.nan
    beta1 = np.nan
    first_idx = np.searchsorted(v, vlim * v_oc)
    for idx in range(first_idx, len(v)):
        coef = np.polyfit(v[:idx], i[:idx], deg=1)
        if coef[0] < 0:
            # intercept term
            beta0 = coef[1].item()
            # sign change of slope to get positive parameter value
            beta1 = -coef[0].item()
            break
    if any(np.isnan([beta0, beta1])):
        raise RuntimeError("Parameter extraction failed: beta0={}, beta1={}"
                           .format(beta0, beta1))
    else:
        return beta0, beta1


def _sandia_beta3_beta4(voltage, current, beta0, beta1, ilim, i_sc):
    # Used by fit_sde_sandia.
    # Subtract the IV curve from the linear fit.
    y = beta0 - beta1 * voltage - current
    x = np.array([np.ones_like(voltage), voltage, current]).T
    # Select points where y > ilim * i_sc to regress log(y) onto x
    idx = (y > ilim * i_sc)
    result = np.linalg.lstsq(x[idx], np.log(y[idx]), rcond=RCOND)
    coef = result[0]
    beta3 = coef[1].item()
    beta4 = coef[2].item()
    if any(np.isnan([beta3, beta4])):
        raise RuntimeError("Parameter extraction failed: beta3={}, beta4={}"
                           .format(beta3, beta4))
    else:
        return beta3, beta4


def _sandia_simple_params(beta0, beta1, beta3, beta4, v_mp, i_mp, v_oc):
    # Used by fit_sandia_simple.
    nNsVth = 1.0 / beta3
    rs = beta4 / beta3
    gsh = beta1 / (1.0 - rs * beta1)
    rsh = 1.0 / gsh
    iph = (1 + gsh * rs) * beta0
    # calculate I0
    io_vmp = _calc_I0(v_mp, i_mp, iph, gsh, rs, nNsVth)
    io_voc = _calc_I0(v_oc, 0, iph, gsh, rs, nNsVth)
    if any(np.isnan([io_vmp, io_voc])) or ((io_vmp <= 0) and (io_voc <= 0)):
        raise RuntimeError("Parameter extraction failed: I0 is undetermined.")
    elif (io_vmp > 0) and (io_voc > 0):
        io = 0.5 * (io_vmp + io_voc)
    elif (io_vmp > 0):
        io = io_vmp
    else:  # io_voc > 0
        io = io_voc
    return iph, io, rs, rsh, nNsVth


def _calc_I0(voltage, current, iph, gsh, rs, nNsVth):
    return (iph - current - gsh * (voltage + rs * current)) / \
        np.expm1((voltage + rs * current) / nNsVth)


def _fit_sandia_cocontent(voltage, current, nsvth):
    """
    Regression technique to fit the single diode equation to data for a single
    IV curve.

    In general, not reliable for estimating parameters other than Rsh.

    Parameters
    ----------
    voltage : numeric
        voltage for the IV curve in increasing order, the first value must be
        0, the last value is taken as ``Voc``. [V]
    current : numeric
        current for the IV curve corresponding to ``voltage``, the first value
        is taken as ``Isc``, the last value must be 0. [A]
    nsvth : numeric
        the thermal voltage for the module, equal to ``Ns`` (number of cells in
        series) times ``Vth`` (thermal voltage per cell). [V]

    Returns
    -------
    iph : numeric
        photocurrent [A]
    io : numeric
        dark current [A]
    rs : numeric
        shunt resistance [ohm]
    rsh : numeric
        series resistance [ohm]
    n : numeric
        diode (ideality) factor [unitless]

    Raises
    ------
    ValueError if ``voltage`` and ``current`` are different lengths.
    ValueError if ``len(voltage)`` < 6

    Notes
    -----
    Ported from PVLib Matlab [1]_. This function uses a regression technique
    described in [2]_ to fit the single diode equation to data for a single IV
    curve. The method extends ideas in [3]_ where the co-content is regressed
    onto predictors involving voltage and current. Here, a principal component
    transformation of ``(voltage, current)`` prior to regression to reduce the
    effects of colinearity between voltage and current.
    Although values for each of the five parameters are returned,
    testing has shown that values for ``Rs``, ``Io`` and ``n`` may be negative
    or imaginary even for IV curve data without obvious flaws.

    References
    ----------
    .. [1] PVLib MATLAB https://github.com/sandialabs/MATLAB_PV_LIB
    .. [2] C. Hansen, "Parameter Estimation for Single Diode Models of
       Photovoltaic Modules", Sandia National Laboratories Report SAND2015-2065
    .. [3] A. Ortiz-Conde, F. Garci'a Sa'nchez, J. Murci, "New method to
       extract the model parameters of solar cells from the explicit analytic
       solutions of their illuminated I-V characteristics", Solar Energy
       Materials and Solar Cells 90, pp 352 - 361, 2006.
    """

    if len(current) != len(voltage):
        raise ValueError("voltage and current should have the same "
                         "length")
    if len(voltage) < 6:
        raise ValueError("at least 6 voltage points are required; ~50 are "
                         "recommended")
    isc = current[0]  # short circuit current
    voc = voltage[-1]  # open circuit voltage

    # Fit quadratic spline to IV curve in order to compute the co-content
    # (i.e., integral of Isc - I over V) more accurately

    [t, c, yhat, kflag] = _schumaker_qspline(voltage, current)

    # Calculate co-content integral
    cci = _cocontent(t, c, isc, kflag)

    # Regress co-content onto voltage and current predictors
    beta = _cocontent_regress(voltage, current, voc, isc, cci)

    # Extract five parameter values from regression coefficients.
    # Equation 11, [3]
    betagp = beta[3] * 2.

    # Equation 12, [3]
    betars = (np.sqrt(1. + 16. * beta[3] * beta[4]) - 1.) / (4. * beta[3])

    # Equation 13, [3]
    betan = (beta[0] * (np.sqrt(1. + 16. * beta[3] * beta[4]) - 1.) + 4. *
             beta[1] * beta[3]) / (4. * beta[3] * nsvth)

    # Single diode equation at Voc, approximating Iph + Io by Isc
    betaio = (isc - voc * betagp) / (np.exp(voc / (betan * nsvth)))

    # Single diode equation at Isc, using Rsh, Rs, n and Io that were
    # determined above
    betaiph = isc - betaio + betaio * np.exp(isc / (betan * nsvth)) + \
        isc * betars * betagp

    iph = betaiph
    io = betaio
    rsh = 1 / betagp
    rs = betars
    n = betan

    return iph, io, rs, rsh, n


def _cocontent(v, c, isc, kflag):
    # Used by fit_sandia_cocontent
    # calculate co-content integral by numerical integration of
    # i = (Isc - I) over v
    # Here, i = Isc - I is assumed to be represented by the quadratic spline
    # with coefficients in input c, at the discrete sequence of knots in v
    xn = len(v)
    delx = v[1:] - v[:-1]
    tmp = np.array([1. / 3., .5, 1.])
    ss = np.tile(tmp, [xn - 1, 1])
    cc = c * ss  # cast coefficients to a convenient shape
    # compute integral on each interval
    tmpint = np.sum(cc * np.array([delx ** 3, delx ** 2, delx]).T, 1)
    tmpint = np.append(0., tmpint)

    # compute co-content = Int_0^V (Isc - I) dV
    scc = np.zeros(xn)
    # Use trapezoid rule for the first 5 intervals due to spline being
    # unreliable near the left endpoint
    scc[0:5] = isc * v[0:5] - np.cumsum(tmpint[0:5])  # by spline
    scc[5:(xn - 5)] = isc * (v[5:(xn - 5)] - v[4]) - \
        np.cumsum(tmpint[5:(xn - 5)]) + scc[4]

    # Use trapezoid rule for the last 5 intervals due to spline being
    # unreliable near the right endpoint
    scc[(xn - 5):xn] = isc * (v[(xn - 5):xn] - v[xn - 6]) - \
        np.cumsum(tmpint[(xn - 5):xn]) + scc[xn - 6]

    # For estimating diode equation parameters only use original data points,
    # not at any knots added by the quadratic spline fit
    cci = scc[~kflag.astype(bool)]
    return cci


def _cocontent_regress(v, i, voc, isc, cci):
    # Used by fit_sandia_content
    # For the method coded here see Appendix C of [2] SAND2015-2065
    # predictor variables for regression of CC
    x = np.vstack((v, isc - i, v * (isc - i), v * v, (i - isc) ** 2)).T

    # define principal components transformation to shift, scale and rotate
    # V and I before the regression.
    tmpx = x[:, 0:2]
    tmpx_length = tmpx.shape[0]

    tmpx_mean = np.mean(tmpx, axis=0)
    tmpx_std = np.std(tmpx, axis=0, ddof=1)
    tmpx_zscore = (tmpx - np.tile(tmpx_mean, [tmpx_length, 1])) / \
        np.tile(tmpx_std, [tmpx_length, 1])

    tmpx_d, tmpx_v = np.linalg.eig(np.cov(tmpx_zscore.T))

    idx = np.argsort(tmpx_d)[::-1]

    ev1 = tmpx_v[:, idx[0]]

    # Second component set to be orthogonal and rotated counterclockwise by 90.
    ev2 = np.dot(np.array([[0., -1.], [1., 0.]]), ev1)
    r = np.array([ev1, ev2])  # principal components transformation

    s = np.dot(tmpx_zscore, r)
    # [V, I] shift and scaled by zscore, rotated by r

    scc = cci - np.mean(cci, axis=0)  # center co-content values
    col1 = np.ones(len(scc))

    # predictors. Shifting makes a constant term necessary in the regression
    # model
    sx = np.vstack((s[:, 0], s[:, 1], s[:, 0] * s[:, 1], s[:, 0] * s[:, 0],
                    s[:, 1] * s[:, 1], col1)).T

    gamma = np.linalg.lstsq(sx, scc, rcond=RCOND)[0]
    # coefficients from regression in rotated coordinates

    # Principle components transformation steps
    # Matrix which relates principal components transformation R to the mapping
    # between [V' I' V'I' V'^2 I'^2] and sx, where prime ' indicates shifted
    # and scaled data. Used to translate from regression coefficients in
    # rotated coordinates to coefficients in initial V, I coordinates.
    mb = np.array([[r[0, 0], r[1, 0], 0., 0., 0.], [r[0, 1], r[1, 1], 0., 0.,
                                                    0.],
                   [0., 0., r[0, 0] * r[1, 1] + r[0, 1] * r[1, 0], 2. *
                    r[0, 0] * r[0, 1], 2. * r[1, 0] * r[1, 1]],
                   [0., 0., r[0, 0] * r[1, 0], r[0, 0] ** 2., r[1, 0] ** 2.],
                   [0., 0., r[0, 1] * r[1, 1], r[0, 1] ** 2., r[1, 1] ** 2.]])

    # matrix which is used to undo effect of shifting and scaling on regression
    # coefficients.
    ma = np.array([[np.std(v, ddof=1), 0., np.std(v, ddof=1) *
                    np.mean(isc - i), 2. * np.std(v, ddof=1) * np.mean(v),
                   0.], [0., np.std(isc - i, ddof=1), np.std(isc - i, ddof=1)
                         * np.mean(v), 0.,
                   2. * np.std(isc - i, ddof=1) * np.mean(isc - i)],
                   [0., 0., np.std(v, ddof=1) * np.std(isc - i, ddof=1), 0.,
                    0.],
                   [0., 0., 0., np.std(v, ddof=1) ** 2., 0.],
                   [0., 0., 0., 0., np.std(isc - i, ddof=1) ** 2.]])

    # translate from coefficients in rotated space (gamma) to coefficients in
    # original coordinates (beta)
    beta = np.linalg.lstsq(np.dot(mb, ma), gamma[0:5], rcond=RCOND)[0]
    return beta
