"""Module with tools to recursively compare nested objects
BEWARE of stack overflows in recursions"""
import numpy as np
import hdfdict as h5d
import h5py


__all__ = ["h5_same",
           "dict_same",
           "h5dict_safe"]

DATA_TYPES = (np.int_, np.int32, np.uint64, np.float_, np.complex_, float, int, complex)

DESC_TYPES = (np.bytes_, np.string_, str, bytes, np.bool_, bool)

def h5dict_safe(file: str) -> dict:
    """
    h5 files safe loader with hdfdict.
    :param file: str ou os.Path menant au h5 à lire
    """
    with h5py.File(file) as fin:
        try:
            return h5d.load(fin, lazy=False)
        except TypeError:
            return h5d.load(fin)


def h5_same(source_file: str, target_file: str, **kwargs):
    """
    *Main call function to test two h5 files.*

    :param source: Path to the source file to compare
    :param target: Path to the target file to compare
    :returns: **True** if files are identical, **False** otherwise
    """
    source_dict = h5dict_safe(source_file)
    target_dict = h5dict_safe(target_file)
    try:
        return dict_same(source_dict, target_dict, **kwargs)
    except ValueError as err:
        raise ValueError(f"Source file is {source_file}\nTarget file is {target_file}\n" + str(err))

def dict_same(source_dict: dict, target_dict: dict, **kwargs):
    """
    *Main call function to test two dictionaries.*

    :param source_dict: first dict to compare
    :param target_dict: second dict to compare
    :returns: **True** if files are identical, raises an error otherwise
    """
    is_identical, log = _same_nob(source_dict, target_dict, **kwargs)
    if not is_identical:
        raise ValueError(log)
    return True


def _same_nob(src: dict, tgt: dict, path="/") -> bool:
    """Compare NOb identified as same, numpy arrays included, recursively"""
    #pylint: disable=too-many-locals,too-many-branches,too-many-statements
    log = str()
    log += ("------------------------------\n")
    log += ("Examining " + path + "\n")
    output = True

    common = dict()

    #compare keys
    for key in src:
        if not key in tgt:
            output = False
            log += f"Attribute {key} only in source h5 file.\n"
        else:
            common[key] = src[key]
    for key in tgt:
        if not key in src:
            output = False
            log += f"Attribute {key} only in target h5 file.\n"

    #compare whats inside
    for key in common:
        log += ("\t" + key + "\n")
        tgt_t = type(tgt[key])
        src_t = type(src[key])
        if not src_t == tgt_t:
            output = False
            log += f"**  Different attribute types: {src_t} and {tgt_t} (DIFF_OBJECTS)\n"
            continue    # different hdf5 types -- don't try to compare further

        # handle datasets first
        if isinstance(tgt[key], (DESC_TYPES, DATA_TYPES)):
            if not src[key] == tgt[key]:
                output = False
                log += f"** Different values for elements {key}: {src[key]} and {tgt[key]}\n"

        elif isinstance(tgt[key], (list, tuple, np.ndarray)):
            if np.shape(tgt[key]) != np.shape(src[key]):
                output = False
                log += f" ** Different shapes for arrays {key}"
            elif isinstance(tgt[key][0], (np.ndarray, DATA_TYPES)):
                if not np.allclose(src[key], tgt[key]):
                    output = False
                    log += f"** Different values in arrays {key}\n"
            elif isinstance(tgt[key][0], DESC_TYPES):
                if not np.all(src[key] == tgt[key]):
                    output = False
                    log += f"** Different values in arrays {key}: {src[key]} and {tgt[key]}\n"
            else:
                raise NotImplementedError(f"Attribute's values type: {type(tgt[key][0])}")

        #handle nob recursively
        elif isinstance(tgt[key], dict):
            subout, sublog = _same_nob(src[key], tgt[key], path=path + key + "/")
            log += sublog
            if subout is False:
                output = subout
        else:
            raise NotImplementedError(f"Attribute type: {type(tgt[key])}")

    return output, log
