import json
from dataclasses import asdict, dataclass, is_dataclass
from functools import partial
from pprint import pformat
from typing import Any, Callable

import numpy
from rich.console import Console
from rich.table import Table

from .block import SeriesBlock
from .channel import Channel


def _dict_factory_nonone(obj: Any):
    """dict factory with items with None value ignored"""
    return {key: value for key, value in obj if value is not None}


def _obj_encode(obj: Any, *, abrv: bool = False) -> str:
    if is_dataclass(obj) and not isinstance(obj, type):
        return asdict(obj, dict_factory=_dict_factory_nonone)
    if isinstance(obj, numpy.dtype):
        return obj.name
    if isinstance(obj, (numpy.ndarray, numpy.ma.MaskedArray)):
        if abrv:
            # https://numpy.org/doc/stable/reference/generated/numpy.array2string.html
            data = numpy.array2string(
                obj,
                max_line_width=1000000,
                separator=", ",
                threshold=16,
                formatter={
                    "all": str,
                },
            )
            return f"array(data={data}, dtype={obj.dtype})"
        return obj.tolist()
    return f"??{type(obj)}??"


@dataclass
class Formatter:
    """class for storing an object formatter function"""

    name: str
    desc: str
    func: Callable

    def __call__(self, *args, **kwargs):
        """format object"""
        return self.func(*args, **kwargs)


FORMATTERS = [
    Formatter(
        "str",
        "str",
        str,
    ),
    Formatter(
        "repr",
        "repr",
        repr,
    ),
    Formatter(
        "pprint",
        "pprint",
        lambda obj: pformat(_obj_encode(obj)),
    ),
    Formatter(
        "json",
        "JSON, one entry per line",
        partial(json.dumps, default=_obj_encode),
    ),
    Formatter(
        "json-pp",
        "JSON, human-readable formatting",
        partial(json.dumps, default=_obj_encode, indent=4),
    ),
    Formatter(
        "json-abrv",
        "json-pp but with arrays abreviated by their repr (non-valid JSON)",
        partial(
            json.dumps,
            default=partial(_obj_encode, abrv=True),
            indent=4,
        ),
    ),
]


def formatter(obj: Any, format: str = "str") -> str:  # noqa: A002
    """format an object for printing to the screen

    See FORMATTERS for available formats.  If format is not specified
    the str representation of the object is returned.

    """
    for formatter in FORMATTERS:
        if formatter.name == format:
            return formatter(obj)
    else:
        msg = f"Unknown format: {format}"
        raise ValueError(msg)


##########
# rich Table formatting


def format_channels_rich(channels: list[Channel]) -> Table:
    """format a list of channels into a rich Table"""
    table = None
    for channel in channels:
        cdict = {k: str(v) for k, v in channel.as_dict().items()}
        if not table:
            table = Table(
                *cdict,
                box=None,
            )
        table.add_row(*cdict.values())
    assert table
    return table


def format_block_rich(block: SeriesBlock) -> Table:
    """format a SeriesBlock into a rich Table"""
    fields = [
        "channel",
        "dtype",
        "sample_rate",
        "samples",
        "has gap",
        "data (abrv)",
    ]
    header = f"time: {block.time_ns:_} ns, duration: {block.duration_ns:_} ns"
    table = Table(
        *fields,
        title=header,
        title_justify="left",
        title_style="bold",
        box=None,
    )
    for name, series in sorted(block.items()):
        cells = [
            name,
            str(series.data_type),
            str(series.sample_rate),
            str(len(series.data)),
            str(series.has_nulls),
            numpy.array2string(
                series.data,
                max_line_width=10000,
                separator=", ",
                threshold=5,
                edgeitems=1,
                formatter={
                    "all": str,
                },
            ),
        ]
        table.add_row(*cells)
    return table


def print_rich(obj):
    """print a rich object to the screen"""
    Console().print(obj)
