from enum import Enum
from functools import reduce
from typing import Callable, List, Optional, Union
from numpy.typing import NDArray
from numpy import ndarray


def log_as_table(values: Union[list, dict], ignore_keys: list = []):
    """
    Log a list of values to display as a table.
    Can either use a single dictionary, represented using id/value pair,
    or a list of dictionaries using their keys as columns.

    Parameters
    ----------
    values : list | dict
        Values to display as a table.
    """
    return (
        ";".join(
            [f"id:{k}_value:{v}" for k, v in values.items() if k not in ignore_keys]
            if isinstance(values, dict)
            else [
                (
                    "_".join(
                        [f"{k}:{v}" for k, v in value.items() if k not in ignore_keys]
                    )
                    if isinstance(value, dict)
                    else str(value)
                )
                for value in values
            ]
        )
        or "None"
    )


def log_blank_nodes_id(blank_nodes: List[dict]):
    """
    Log a list of blank node ids to display as a table.

    Parameters
    ----------
    values : list
        List of blank nodes, like Product, Input, Measurement, etc.
    """
    return (
        ";".join(
            [
                p.get("term", {}).get("@id")
                for p in blank_nodes
                if p.get("term", {}).get("@id")
            ]
        )
        or "None"
    )


_INVALID_CHARS = {"_", ":", ",", "="}
_REPLACEMENT_CHAR = "-"


def format_str(value: Optional[str], default: str = "None") -> str:
    """Format a string for logging in a table. Remove all characters used to render the table on the front end."""
    return (
        reduce(
            lambda x, char: x.replace(char, _REPLACEMENT_CHAR),
            _INVALID_CHARS,
            str(value),
        )
        if value
        else default
    )


def format_bool(value: Optional[bool], default: str = "None") -> str:
    return str(value) if isinstance(value, bool) else default


def format_float(
    value: Union[int, float, None],
    unit: str = "",
    default: str = "None",
    ndigits: int = 3,
) -> str:
    return (
        " ".join(
            string
            for string in [f"{round(value, ndigits)}", format_str(unit, "")]
            if string
        )
        if isinstance(value, (float, int))
        else default
    )


def format_int(
    value: Union[int, float, None], unit: str = "", default: str = "None"
) -> str:
    return format_float(value, unit=unit, default=default, ndigits=None)


def _format_nd_array(
    value: Optional[NDArray], unit: str = "", default: str = "None", ndigits: int = 3
) -> str:
    return (
        " ".join(
            string
            for string in [
                f"{format_float(value.mean(), ndigits=ndigits)} ± {format_float(value.std(), ndigits=ndigits)}",
                format_str(unit, ""),
            ]
            if string
        )
        if isinstance(value, ndarray)
        else default
    )


TYPE_TO_FORMAT_FUNC = {ndarray: _format_nd_array, (float, int): format_float}


def format_nd_array(
    value: Optional[NDArray], unit: str = "", default: str = "None", ndigits: int = 3
) -> str:
    """
    Format a numpy array for logging in a table.

    Values that are floats and ints are logged using `format_float`.
    """
    format_func = next(
        (
            func
            for type_, func in TYPE_TO_FORMAT_FUNC.items()
            if isinstance(value, type_)
        ),
        None,
    )
    return (
        format_func(value, unit=unit, default=default, ndigits=ndigits)
        if format_func
        else default
    )


def format_decimal_percentage(
    value: Optional[float], unit: str = "pct", default: str = "None", ndigits: int = 3
) -> str:
    """Format a decimal percentage (0-1) as a percentage (0-100%) for logging in a table."""
    return (
        format_float(value * 100, unit=unit, ndigits=ndigits)
        if isinstance(value, (float, int))
        else default
    )


def format_enum(value: Optional[Enum], default: str = "None") -> str:
    """Format an enum for logging in a table."""
    return format_str(value.value) if isinstance(value, Enum) else default


def format_conditional_message(
    value: bool, on_true: str = "True", on_false: str = "False"
) -> str:
    return format_str(on_true if bool(value) else on_false)


def format_func_name(value: Optional[Callable], default: str = "None") -> str:
    return format_str(value.__name__) if callable(value) else default
