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

import numpy as np

from ..fixes import _csc_array_cast
from ..utils import _check_fname, warn
from .constants import FIFF
from .open import fiff_open, read_tag
from .tag import _float_item, _int_item, find_tag
from .tree import dir_tree_find
from .write import (
    _safe_name_list,
    end_block,
    start_block,
    write_float,
    write_float_matrix,
    write_float_sparse,
    write_id,
    write_int,
    write_int_matrix,
    write_name_list_sanitized,
    write_string,
)

_proc_keys = [
    "parent_file_id",
    "block_id",
    "parent_block_id",
    "date",
    "experimenter",
    "creator",
]
_proc_ids = [
    FIFF.FIFF_PARENT_FILE_ID,
    FIFF.FIFF_BLOCK_ID,
    FIFF.FIFF_PARENT_BLOCK_ID,
    FIFF.FIFF_MEAS_DATE,
    FIFF.FIFF_EXPERIMENTER,
    FIFF.FIFF_CREATOR,
]
_proc_writers = [write_id, write_id, write_id, write_int, write_string, write_string]
_proc_casters = [dict, dict, dict, np.array, str, str]


def _read_proc_history(fid, tree):
    """Read processing history from fiff file.

    This function reads the SSS info, the CTC correction and the
    calibaraions from the SSS processing logs inside af a raw file
    (C.f. Maxfilter v2.2 manual (October 2010), page 21)::

        104 = {                 900 = proc. history
          104 = {               901 = proc. record
            103 = block ID
            204 = date
            212 = scientist
            113 = creator program
            104 = {             502 = SSS info
              264 = SSS task
              263 = SSS coord frame
              265 = SSS origin
              266 = SSS ins.order
              267 = SSS outs.order
              268 = SSS nr chnls
              269 = SSS components
              278 = SSS nfree
              243 = HPI g limit    0.98
              244 = HPI dist limit 0.005
            105 = }             502 = SSS info
            104 = {             504 = MaxST info
              264 = SSS task
              272 = SSST subspace correlation
              279 = SSST buffer length
            105 = }
            104 = {             501 = CTC correction
              103 = block ID
              204 = date
              113 = creator program
              800 = CTC matrix
              3417 = proj item chs
            105 = }             501 = CTC correction
            104 = {             503 = SSS finecalib.
              270 = SSS cal chnls
              271 = SSS cal coeff
            105 = }             503 = SSS finecalib.
          105 = }               901 = proc. record
        105 = }                 900 = proc. history
    """
    proc_history = dir_tree_find(tree, FIFF.FIFFB_PROCESSING_HISTORY)
    out = list()
    if len(proc_history) > 0:
        proc_history = proc_history[0]
        proc_records = dir_tree_find(proc_history, FIFF.FIFFB_PROCESSING_RECORD)
        for proc_record in proc_records:
            record = dict()
            for i_ent in range(proc_record["nent"]):
                kind = proc_record["directory"][i_ent].kind
                pos = proc_record["directory"][i_ent].pos
                for key, id_, cast in zip(_proc_keys, _proc_ids, _proc_casters):
                    if kind == id_:
                        tag = read_tag(fid, pos)
                        record[key] = cast(tag.data)
                        break
                else:
                    warn(f"Unknown processing history item {kind}")
            record["max_info"] = _read_maxfilter_record(fid, proc_record)
            iass = dir_tree_find(proc_record, FIFF.FIFFB_IAS)
            if len(iass) > 0:
                # XXX should eventually populate this
                ss = [dict() for _ in range(len(iass))]
                record["ias"] = ss
            if len(record["max_info"]) > 0:
                out.append(record)
    return out


def _write_proc_history(fid, info):
    """Write processing history to file."""
    if len(info["proc_history"]) > 0:
        start_block(fid, FIFF.FIFFB_PROCESSING_HISTORY)
        for record in info["proc_history"]:
            start_block(fid, FIFF.FIFFB_PROCESSING_RECORD)
            for key, id_, writer in zip(_proc_keys, _proc_ids, _proc_writers):
                if key in record:
                    writer(fid, id_, record[key])
            _write_maxfilter_record(fid, record["max_info"])
            if "ias" in record:
                for _ in record["ias"]:
                    start_block(fid, FIFF.FIFFB_IAS)
                    # XXX should eventually populate this
                    end_block(fid, FIFF.FIFFB_IAS)
            end_block(fid, FIFF.FIFFB_PROCESSING_RECORD)
        end_block(fid, FIFF.FIFFB_PROCESSING_HISTORY)


_sss_info_keys = (
    "job",
    "frame",
    "origin",
    "in_order",
    "out_order",
    "nchan",
    "components",
    "nfree",
    "hpi_g_limit",
    "hpi_dist_limit",
)
_sss_info_ids = (
    FIFF.FIFF_SSS_JOB,
    FIFF.FIFF_SSS_FRAME,
    FIFF.FIFF_SSS_ORIGIN,
    FIFF.FIFF_SSS_ORD_IN,
    FIFF.FIFF_SSS_ORD_OUT,
    FIFF.FIFF_SSS_NMAG,
    FIFF.FIFF_SSS_COMPONENTS,
    FIFF.FIFF_SSS_NFREE,
    FIFF.FIFF_HPI_FIT_GOOD_LIMIT,
    FIFF.FIFF_HPI_FIT_DIST_LIMIT,
)
_sss_info_writers = (
    write_int,
    write_int,
    write_float,
    write_int,
    write_int,
    write_int,
    write_int,
    write_int,
    write_float,
    write_float,
)
_sss_info_casters = (
    _int_item,
    _int_item,
    np.array,
    _int_item,
    _int_item,
    _int_item,
    np.array,
    _int_item,
    _float_item,
    _float_item,
)

_max_st_keys = ("job", "subspcorr", "buflen")
_max_st_ids = (FIFF.FIFF_SSS_JOB, FIFF.FIFF_SSS_ST_CORR, FIFF.FIFF_SSS_ST_LENGTH)
_max_st_writers = (write_int, write_float, write_float)
_max_st_casters = (_int_item, _float_item, _float_item)

_sss_ctc_keys = ("block_id", "date", "creator", "decoupler")
_sss_ctc_ids = (
    FIFF.FIFF_BLOCK_ID,
    FIFF.FIFF_MEAS_DATE,
    FIFF.FIFF_CREATOR,
    FIFF.FIFF_DECOUPLER_MATRIX,
)
_sss_ctc_writers = (write_id, write_int, write_string, write_float_sparse)
_sss_ctc_casters = (dict, np.array, str, _csc_array_cast)

_sss_cal_keys = ("cal_chans", "cal_corrs")
_sss_cal_ids = (FIFF.FIFF_SSS_CAL_CHANS, FIFF.FIFF_SSS_CAL_CORRS)
_sss_cal_writers = (write_int_matrix, write_float_matrix)
_sss_cal_casters = (np.array, np.array)


def _read_ctc(fname):
    """Read cross-talk correction matrix."""
    fname = _check_fname(fname, overwrite="read", must_exist=True)
    f, tree, _ = fiff_open(fname)
    with f as fid:
        sss_ctc = _read_maxfilter_record(fid, tree)["sss_ctc"]
        bad_str = f"Invalid cross-talk FIF: {fname}"
        if len(sss_ctc) == 0:
            raise ValueError(bad_str)
        node = dir_tree_find(tree, FIFF.FIFFB_DATA_CORRECTION)[0]
        comment = find_tag(fid, node, FIFF.FIFF_COMMENT).data
        if comment != "cross-talk compensation matrix":
            raise ValueError(bad_str)
        sss_ctc["creator"] = find_tag(fid, node, FIFF.FIFF_CREATOR).data
        sss_ctc["date"] = find_tag(fid, node, FIFF.FIFF_MEAS_DATE).data
    return sss_ctc


def _read_maxfilter_record(fid, tree):
    """Read maxfilter processing record from file."""
    sss_info_block = dir_tree_find(tree, FIFF.FIFFB_SSS_INFO)  # 502
    sss_info = dict()
    if len(sss_info_block) > 0:
        sss_info_block = sss_info_block[0]
        for i_ent in range(sss_info_block["nent"]):
            kind = sss_info_block["directory"][i_ent].kind
            pos = sss_info_block["directory"][i_ent].pos
            for key, id_, cast in zip(_sss_info_keys, _sss_info_ids, _sss_info_casters):
                if kind == id_:
                    tag = read_tag(fid, pos)
                    sss_info[key] = cast(tag.data)
                    break

    max_st_block = dir_tree_find(tree, FIFF.FIFFB_SSS_ST_INFO)  # 504
    max_st = dict()
    if len(max_st_block) > 0:
        max_st_block = max_st_block[0]
        for i_ent in range(max_st_block["nent"]):
            kind = max_st_block["directory"][i_ent].kind
            pos = max_st_block["directory"][i_ent].pos
            for key, id_, cast in zip(_max_st_keys, _max_st_ids, _max_st_casters):
                if kind == id_:
                    tag = read_tag(fid, pos)
                    max_st[key] = cast(tag.data)
                    break

    sss_ctc_block = dir_tree_find(tree, FIFF.FIFFB_CHANNEL_DECOUPLER)  # 501
    sss_ctc = dict()
    if len(sss_ctc_block) > 0:
        sss_ctc_block = sss_ctc_block[0]
        for i_ent in range(sss_ctc_block["nent"]):
            kind = sss_ctc_block["directory"][i_ent].kind
            pos = sss_ctc_block["directory"][i_ent].pos
            for key, id_, cast in zip(_sss_ctc_keys, _sss_ctc_ids, _sss_ctc_casters):
                if kind == id_:
                    tag = read_tag(fid, pos)
                    sss_ctc[key] = cast(tag.data)
                    break
            else:
                if kind == FIFF.FIFF_PROJ_ITEM_CH_NAME_LIST:
                    tag = read_tag(fid, pos)
                    chs = _safe_name_list(tag.data, "read", "proj_items_chs")
                    # This list can null chars in the last entry, e.g.:
                    # [..., 'MEG2642', 'MEG2643', 'MEG2641\x00 ... \x00']
                    chs[-1] = chs[-1].split("\x00")[0]
                    sss_ctc["proj_items_chs"] = chs

    sss_cal_block = dir_tree_find(tree, FIFF.FIFFB_SSS_CAL)  # 503
    sss_cal = dict()
    if len(sss_cal_block) > 0:
        sss_cal_block = sss_cal_block[0]
        for i_ent in range(sss_cal_block["nent"]):
            kind = sss_cal_block["directory"][i_ent].kind
            pos = sss_cal_block["directory"][i_ent].pos
            for key, id_, cast in zip(_sss_cal_keys, _sss_cal_ids, _sss_cal_casters):
                if kind == id_:
                    tag = read_tag(fid, pos)
                    sss_cal[key] = cast(tag.data)
                    break

    max_info = dict(sss_info=sss_info, sss_ctc=sss_ctc, sss_cal=sss_cal, max_st=max_st)
    return max_info


def _write_maxfilter_record(fid, record):
    """Write maxfilter processing record to file."""
    sss_info = record["sss_info"]
    if len(sss_info) > 0:
        start_block(fid, FIFF.FIFFB_SSS_INFO)
        for key, id_, writer in zip(_sss_info_keys, _sss_info_ids, _sss_info_writers):
            if key in sss_info:
                writer(fid, id_, sss_info[key])
        end_block(fid, FIFF.FIFFB_SSS_INFO)

    max_st = record["max_st"]
    if len(max_st) > 0:
        start_block(fid, FIFF.FIFFB_SSS_ST_INFO)
        for key, id_, writer in zip(_max_st_keys, _max_st_ids, _max_st_writers):
            if key in max_st:
                writer(fid, id_, max_st[key])
        end_block(fid, FIFF.FIFFB_SSS_ST_INFO)

    sss_ctc = record["sss_ctc"]
    if len(sss_ctc) > 0:  # dict has entries
        start_block(fid, FIFF.FIFFB_CHANNEL_DECOUPLER)
        for key, id_, writer in zip(_sss_ctc_keys, _sss_ctc_ids, _sss_ctc_writers):
            if key in sss_ctc:
                writer(fid, id_, sss_ctc[key])
        if "proj_items_chs" in sss_ctc:
            write_name_list_sanitized(
                fid,
                FIFF.FIFF_PROJ_ITEM_CH_NAME_LIST,
                sss_ctc["proj_items_chs"],
                "proj_items_chs",
            )
        end_block(fid, FIFF.FIFFB_CHANNEL_DECOUPLER)

    sss_cal = record["sss_cal"]
    if len(sss_cal) > 0:
        start_block(fid, FIFF.FIFFB_SSS_CAL)
        for key, id_, writer in zip(_sss_cal_keys, _sss_cal_ids, _sss_cal_writers):
            if key in sss_cal:
                writer(fid, id_, sss_cal[key])
        end_block(fid, FIFF.FIFFB_SSS_CAL)
