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

import numpy as np

from .._fiff.pick import _picks_to_idx, pick_info
from .._fiff.proj import Projection
from ..utils import verbose
from .maxwell import _prep_mf_coils, _sss_basis


@verbose
def compute_proj_hfc(
    info, order=1, picks="meg", exclude="bads", *, accuracy="accurate", verbose=None
):
    """Generate projectors to perform homogeneous/harmonic correction to data.

    Remove environmental fields from magnetometer data by assuming it is
    explained as a homogeneous :footcite:`TierneyEtAl2021` or harmonic field
    :footcite:`TierneyEtAl2022`. Useful for arrays of OPMs.

    Parameters
    ----------
    %(info)s
    order : int
        The order of the spherical harmonic basis set to use. Set to 1 to use
        only the homogeneous field component (default), 2 to add gradients, 3
        to add quadrature terms, etc.
    picks : str | array_like | slice | None
        Channels to include. Default of ``'meg'`` (same as None) will select
        all non-reference MEG channels. Use ``('meg', 'ref_meg')`` to include
        reference sensors as well.
    exclude : list | 'bads'
        List of channels to exclude from HFC, only used when picking
        based on types (e.g., exclude="bads" when picks="meg").
        Specify ``'bads'`` (the default) to exclude all channels marked as bad.
    accuracy : str
        Can be ``"point"``, ``"normal"`` or ``"accurate"`` (default), defines
        which level of coil definition accuracy is used to generate model.
    %(verbose)s

    Returns
    -------
    %(projs)s

    See Also
    --------
    mne.io.Raw.add_proj
    mne.io.Raw.apply_proj

    Notes
    -----
    To apply the projectors to a dataset, use
    ``inst.add_proj(projs).apply_proj()``.

    .. versionadded:: 1.4

    References
    ----------
    .. footbibliography::
    """
    picks = _picks_to_idx(info, picks, none="meg", exclude=exclude, with_ref_meg=False)
    info = pick_info(info, picks)
    del picks
    exp = dict(origin=(0.0, 0.0, 0.0), int_order=0, ext_order=order)
    coils = _prep_mf_coils(info, ignore_ref=False, accuracy=accuracy)
    n_chs = len(coils[5])
    if n_chs != info["nchan"]:
        raise ValueError(
            f"Only {n_chs}/{info['nchan']} picks could be interpreted as MEG channels."
        )
    S = _sss_basis(exp, coils)
    del coils
    bad_chans = [
        info["ch_names"][pick] for pick in np.where((~np.isfinite(S)).any(axis=1))[0]
    ]
    if bad_chans:
        raise ValueError(
            "The following channel(s) generate non-finite projectors:\n"
            f"    {bad_chans}\nPlease exclude from picks!"
        )
    S /= np.linalg.norm(S, axis=0)
    labels = _label_basis(order)
    assert len(labels) == S.shape[1]
    projs = []
    for label, vec in zip(labels, S.T):
        proj_data = dict(
            col_names=info["ch_names"],
            row_names=None,
            data=vec[np.newaxis, :],
            ncol=info["nchan"],
            nrow=1,
        )
        projs.append(Projection(active=False, data=proj_data, desc=label))
    return projs


def _label_basis(order):
    """Give basis vectors names for Projection() class."""
    return [
        f"HFC: l={L} m={m}"
        for L in np.arange(1, order + 1)
        for m in np.arange(-1 * L, L + 1)
    ]
