# Authors: The MNE-Python contributors.
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

import math

import numpy as np
from scipy.signal import lfilter

from .._fiff.pick import pick_info
from ..cov import Covariance, compute_whitener
from ..epochs import BaseEpochs
from ..evoked import Evoked
from ..forward import apply_forward
from ..io import BaseRaw
from ..utils import _check_preload, _validate_type, check_random_state, logger, verbose


@verbose
def simulate_evoked(
    fwd,
    stc,
    info,
    cov=None,
    nave=30,
    iir_filter=None,
    random_state=None,
    use_cps=True,
    verbose=None,
):
    """Generate noisy evoked data.

    .. note:: No projections from ``info`` will be present in the
              output ``evoked``. You can use e.g.
              :func:`evoked.add_proj <mne.Evoked.add_proj>` or
              :func:`evoked.set_eeg_reference <mne.Evoked.set_eeg_reference>`
              to add them afterward as necessary.

    Parameters
    ----------
    fwd : instance of Forward
        A forward solution.
    stc : SourceEstimate object
        The source time courses.
    %(info_not_none)s Used to generate the evoked.
    cov : Covariance object | None
        The noise covariance. If None, no noise is added.
    nave : int
        Number of averaged epochs (defaults to 30).

        .. versionadded:: 0.15.0
    iir_filter : None | array
        IIR filter coefficients (denominator) e.g. [1, -1, 0.2].
    %(random_state)s
    %(use_cps)s

        .. versionadded:: 0.15
    %(verbose)s

    Returns
    -------
    evoked : Evoked object
        The simulated evoked data.

    See Also
    --------
    simulate_raw
    simulate_stc
    simulate_sparse_stc

    Notes
    -----
    To make the equivalence between snr and nave, when the snr is given
    instead of nave::

        nave = (1 / 10 ** ((actual_snr - snr)) / 20) ** 2

    where actual_snr is the snr to the generated noise before scaling.

    .. versionadded:: 0.10.0
    """
    evoked = apply_forward(fwd, stc, info, use_cps=use_cps)
    if cov is None:
        return evoked

    if nave < np.inf:
        noise = _simulate_noise_evoked(evoked, cov, iir_filter, random_state)
        evoked.data += noise.data / math.sqrt(nave)
        evoked.nave = np.int64(nave)
    if cov.get("projs", None):
        evoked.add_proj(cov["projs"]).apply_proj()
    return evoked


def _simulate_noise_evoked(evoked, cov, iir_filter, random_state):
    noise = evoked.copy()
    noise.data[:] = 0
    return _add_noise(noise, cov, iir_filter, random_state, allow_subselection=False)


@verbose
def add_noise(inst, cov, iir_filter=None, random_state=None, verbose=None):
    """Create noise as a multivariate Gaussian.

    The spatial covariance of the noise is given from the cov matrix.

    Parameters
    ----------
    inst : instance of Evoked, Epochs, or Raw
        Instance to which to add noise.
    cov : instance of Covariance
        The noise covariance.
    iir_filter : None | array-like
        IIR filter coefficients (denominator).
    %(random_state)s
    %(verbose)s

    Returns
    -------
    inst : same type as the input data
        The instance, modified to have additional noise.

    Notes
    -----
    Only channels in both ``inst.info['ch_names']`` and
    ``cov['names']`` will have noise added to them.

    This function operates inplace on ``inst``.

    .. versionadded:: 0.18.0
    """
    # We always allow subselection here
    return _add_noise(inst, cov, iir_filter, random_state)


def _add_noise(inst, cov, iir_filter, random_state, allow_subselection=True):
    """Add noise, possibly with channel subselection."""
    _validate_type(cov, Covariance, "cov")
    _validate_type(
        inst, (BaseRaw, BaseEpochs, Evoked), "inst", "Raw, Epochs, or Evoked"
    )
    _check_preload(inst, "Adding noise")
    data = inst._data
    assert data.ndim in (2, 3)
    if data.ndim == 2:
        data = data[np.newaxis]
    # Subselect if necessary
    info = inst.info
    info._check_consistency()
    picks = gen_picks = slice(None)
    if allow_subselection:
        use_chs = list(set(info["ch_names"]) & set(cov["names"]))
        picks = np.where(np.isin(info["ch_names"], use_chs))[0]
        logger.info(
            "Adding noise to %d/%d channels (%d channels in cov)",
            len(picks),
            len(info["chs"]),
            len(cov["names"]),
        )
        info = pick_info(inst.info, picks)
        info._check_consistency()

        gen_picks = np.arange(info["nchan"])
    for epoch in data:
        epoch[picks] += _generate_noise(
            info, cov, iir_filter, random_state, epoch.shape[1], picks=gen_picks
        )[0]
    return inst


def _generate_noise(
    info, cov, iir_filter, random_state, n_samples, zi=None, picks=None
):
    """Create spatially colored and temporally IIR-filtered noise."""
    rng = check_random_state(random_state)
    _, _, colorer = compute_whitener(
        cov, info, pca=True, return_colorer=True, picks=picks, verbose=False
    )
    noise = np.dot(colorer, rng.standard_normal((colorer.shape[1], n_samples)))
    if iir_filter is not None:
        if zi is None:
            zi = np.zeros((len(colorer), len(iir_filter) - 1))
        noise, zf = lfilter([1], iir_filter, noise, axis=-1, zi=zi)
    else:
        zf = None
    return noise, zf
