"""
This module contains functions to parse LCMLogs to a nav_structtype object.

Functions:
    dict_to_struct: Convert a dictionary to a nav_structtype object.
    struct_to_dict: Convert a nav_structtype object to a dictionary.
    filter_channels_from_data: Filter channels from an existing nav_structtype object based on channel patterns.
    read_log: Read a LCMLog file and return a nav_structtype object with optional channel filtering.

Authors: Sebastián Rodríguez-Martínez, Giancarlo Troni and Bastián Muñoz
Contact: srodriguez@mbari.org
"""

import os.path
import pickle
import re
from pathlib import Path
from typing import Any, Dict, List

import numpy as np  # noqa: F401

from navlib.lcmlog.log_to_smat import parse_and_save


class nav_structtype:
    """
    Base class to store data as a structure with numpy arrays.
    """

    pass


def dict_to_struct(ddata: Dict[str, Any]) -> nav_structtype:
    """
    nav_dict_to_struct takes as input a simple or nested dictionary with
    data and returns an object of the class nav_structtype with the data

    Args:
        ddata (Dict[str, Any]): Dictionary with data

    Returns:
        nav_structtype: Object with data as a structure with numpy arrays
    """
    # Create new object
    sdata = nav_structtype()

    # Iterate each element of the dictionary
    for k, v in ddata.items():
        # Fix dictionary key name (remove '.')
        k = k.replace(".", "_")
        k = k.replace("$", "S")

        # Recursive function to deal with nested dictionaries
        if isinstance(v, dict):
            exec(compile("sdata." + k + " = dict_to_struct(v)", "<string>", "exec"))
        else:
            try:
                exec(compile("sdata." + k + " = np.array(v)", "<string>", "exec"))
            except ValueError:
                print(
                    f"Warning: Non homogeneous array {k} cannot be converted to numpy array."
                )
                exec(compile("sdata." + k + " = v", "<string>", "exec"))
    return sdata


def struct_to_dict(sdata: nav_structtype) -> Dict[str, Any]:
    """
    Convert a nav_structtype object to a dictionary.

    Args:
        sdata (nav_structtype): Object with data as a structure with numpy arrays

    Returns:
        Dict[str, Any]: Dictionary with data
    """
    result = {}

    for key, value in sdata.__dict__.items():
        if isinstance(value, nav_structtype):
            # Recursively convert nested nav_structtype objects
            result[key] = struct_to_dict(value)
        else:
            # Keep the value as is (including numpy arrays)
            result[key] = value

    return result


def filter_channels_from_data(
    data: nav_structtype, channels_to_process: str = ".*", channels_to_ignore: str = ""
) -> nav_structtype:
    """
    Filter channels from an existing nav_structtype object based on channel patterns.

    Args:
        data (nav_structtype): Object with data as a structure with numpy arrays
        channels_to_process (str): Regular expression to filter the channels to process. Default is ".*" (all channels).
        channels_to_ignore (str): Regular expression to filter the channels to ignore. Default is "" (no channels ignored).

    Returns:
        nav_structtype: Filtered object with data as a structure with numpy arrays
    """
    if channels_to_process == ".*" and channels_to_ignore == "":
        # No filtering needed
        return data

    # Create regular expressions for channel filtering
    channels_to_process_re = re.compile(channels_to_process)
    channels_to_ignore_re = (
        re.compile(channels_to_ignore) if channels_to_ignore else None
    )

    # Create new filtered object
    filtered_data = nav_structtype()

    # Iterate through all attributes (channels) in the data
    for channel_name in data.__dict__.keys():
        # Check if channel should be processed
        if channels_to_process_re.match(channel_name):
            # Check if channel should not be ignored
            if channels_to_ignore_re is None or not channels_to_ignore_re.match(
                channel_name
            ):
                # Keep this channel
                setattr(filtered_data, channel_name, getattr(data, channel_name))

    return filtered_data


def read_log(
    file_name: str,
    verbose=True,
    channels_to_process: str = ".*",
    channels_to_ignore: str = "",
    lcm_packages: List[str] = [],
) -> nav_structtype:
    """
    nav_read_log gets a file name and returns the data as a nav_structtype
    object.
    If the file_name does not correspond to a .pkl file the transforms a lcm_log
    to a pickle file.

    Args:
        file_name (str): File name of the log file
        verbose (bool): Verbose flag. Default is True.
        channels_to_process (str): Regular expression to filter the channels to process. Default is ".*" (all channels).
        channels_to_ignore (str): Regular expression to filter the channels to ignore. Default is "" (no channels ignored).
        lcm_packages (List[str]): List of directories to search for LCM types in addition to the directories in the
            python path.

    Returns:
        nav_structtype: Object with data as a structure with numpy arrays
    """
    fname = Path(file_name)
    parent, stem = Path(fname.parent), Path(fname.stem)
    pkl_file_exists = os.path.isfile(str(parent / stem) + ".pkl")

    # Check if file is not already parsed to a pickle file
    if not pkl_file_exists:
        # If it is not a file, replace "." and "-" by "_"
        stem_alt = str(stem).replace(".", "_").replace("-", "_")
        try:
            if not os.path.isfile("/".join([str(parent), ".".join([stem_alt, "pkl"])])):
                if verbose:
                    print("Pickle file not found, looking for lcmlog with same stem")
                parse_and_save(
                    str(fname),
                    output_file_name=str(parent / stem) + ".pkl",
                    channels_to_process=channels_to_process,
                    channels_to_ignore=channels_to_ignore,
                    verbose=verbose,
                    lcm_packages=lcm_packages,
                )
                if verbose:
                    print(f"LCMLogs {str(fname)} parsed to .pkl format")
                fname_alt = str(parent / stem)
        except FileNotFoundError:
            print(f"{fname} not found neither as lcmlog nor as .pkl file.")
            return
    else:
        fname_alt = str(parent / stem)

    # Load Pickle file
    print(f"Loading pickle logfile:  {fname_alt}.pkl")
    pkl_file = open(fname_alt + ".pkl", "rb")
    ddata = pickle.load(pkl_file)

    # Convert dictionary to a structure with numpy arrays
    data = dict_to_struct(ddata)

    # Apply channel filtering if loading from existing pickle file
    # (when parsing from LCM, filtering is already applied in parse_and_save)
    if pkl_file_exists and (channels_to_process != ".*" or channels_to_ignore != ""):
        if verbose:
            print("Applying channel filtering to loaded data")
        data = filter_channels_from_data(data, channels_to_process, channels_to_ignore)

    if verbose:
        print("LCMLog successfully transformed to nav_structtype object")
    return data
