from abc import ABC, abstractmethod
from dataclasses import dataclass
import logging
from typing import Iterator, List, Literal, Optional, Type, Union

from physiq_cloud.series_frame import fb_builder
from physiq_cloud.series_frame.fb_wrapper import FlatBufferWrapper
from physiq_cloud.time import Instant, TimeUnit
from physiqfb.SeriesFrame import SeriesFrame
from physiqfb.SeriesFrameHolderList import SeriesFrameHolderList
from pydantic import BaseModel

logger = logging.getLogger(__name__)


class SamplingSetInfo(BaseModel):
    id: int
    alias: str
    timestamped: bool
    # FIXME: not in the INFO.yaml, but isn't this just the `frame_size_micros`
    # divided by the number of values in the channel data? We could either get
    # it from the first frame, or look it up in the API/repo.
    # frequency: int


class SftInfo(BaseModel):
    alias: Optional[str]
    frame_size_micros: int
    max_frame_size_bytes: int
    sampling_sets: List[SamplingSetInfo]


@dataclass(frozen=True)
class SeriesFramePackage:
    frame: FlatBufferWrapper[SeriesFrame]
    sft_info: SftInfo

    def relative_instant(self,  offset=0):
        return Instant.from_unix(
            TimeUnit.MICROSECONDS,
            (self.frame.fb.FrameId() + offset)
            * self.sft_info.frame_size_micros,
        )

    @property
    def start(self):
        return self.relative_instant(0)

    @property
    def end(self):
        return self.relative_instant(1)


class ASeriesFrameEmitter(ABC):
    """
    Abstract class defining the characteristics of a series frame emitter,
    which is designed to be subclassed by all generators and filters.

    A "generator" is a type of emitter that produces frames from "nothing";
    i.e., by some external process, such as reading a file, an API, etc.; or
    by generating frames algorithmically.

    A "filter" is any emitter that takes another emitter and affects its
    :meth:`frames` in some arbitrary way. This terminology is borrowed from
    the digital signal processing domain, and doesn't imply that the signal
    will be reduced or shortened, etc. In fact the signal coming from the
    injected emitter might be lengthened or amplified by a filter to any
    extent, including the maximum or infinity, etc.

    This class defines an abstract property called `frames` that should
    trigger the pipeline to begin and return an `Iterator` of
    :class:`SeriesFramePackage`.

    These emitters are designed to be chained together with filters. E.g., a
    typical scenario would be to start with some generator, and chain it
    together with one or more filters:

    ```
    my_generator = SomeGenerator()
    my_first_filter = SomeFilter(reader=my_generator)
    my_second_filter = SomeOtherFilter(reader=my_first_filter)
    for frames in my_second_filter.frames: ...
    ``` 

    Alternatively, the `__or__` method has been overloaded so that you can
    also do something equivalent to the above like this:

    ```
    for frame in (
        SomeGenerator()
        | SomeFilter
        | SomeOtherFilter
        ): ...
    ```

    NOTE: Accessing `frames` may trigger side effects; it starts a chain of
    calls to the reader accessors in the pipeline, which restarts the pipeline
    anew each time. Use :meth:`cursor` to access the existing `Iterator` if
    you need multiple accesses.

    i.e., calling
    ```
    next(reader.cursor)
    ```
    is different than
    ```
    next(reader.frames())
    ```
    or
    ```
    next(reader.stream())
    ```
    in that calling `stream` is like making a database query (that returns and
    caches a cursor), and `cursor` is just providing access to the cached
    cursor, while calling `frames` directly provides an Iterator without
    caching a cursor.

    That said, :meth:`cursor` is empty until :meth:`stream` is called for the
    first time. (It requires the initial "query" that :meth:`frames` provides.)
    """

    def __init__(self, sft_info: Optional[SftInfo] = None) -> None:
        """
        :param:`sft_info` - an :class:`SftInfo` describing the series frame
        type this emitter emits.
        """
        super().__init__()
        self._sft_info = sft_info

    @property
    def sft_info(self):
        if not self._sft_info:
            raise RuntimeError(
                "Illegal state: `_sft_info` is None."
            )
        return self._sft_info

    @sft_info.setter
    def sft_info(self, val):
        self._sft_info = val

    def __iter__(self): return self.stream()

    @abstractmethod
    def frames(self) -> Iterator[SeriesFramePackage]: ...

    @property
    def cursor(self):
        """
        An `Iterator` representing the current pipeline in progress.
        """
        return self._cursor

    def stream(self):
        """
        Start the pipeline and return an :class:`Iterator` of
        :class:`SeriesFramePackage`.

        Each time :meth:`frames` is accessed, a new `Iterator` is instantiated by
        accessing the `reader`'s `frames`, which may trigger side effects. It
        will also start the process pipeline over, so if you need to iterate
        over the frames with multiple calls within a subclass of this, you should
        use :meth:`cursor` instead, which returns the same object returned from this
        method, without restarting the pipeline.
        """
        self._cursor = self.frames()
        return self.cursor

    def __or__(
        self,
        right: Union["SeriesFrameFilter", Type["SeriesFrameFilter"]],
    ) -> "SeriesFrameFilter":
        if callable(right):
            right = right()

        right.reader = self
        right.sft_info = self.sft_info
        return right

    def sfhls(
        self,
        max_bytelen: int,
    ) -> Iterator[FlatBufferWrapper[SeriesFrameHolderList]]:
        """
        Repackage input frames as a series frame holder list (sfhl) iterator.
        :param:`max_bytelen` - the max byte length allowed per sfhl.
        """
        chunk = list()
        chunk_bytelen = 0

        for pkg in self.frames():
            if chunk_bytelen + len(pkg.frame.bytes) > max_bytelen:
                if not chunk:
                    break
                yield sfhl_from_frames(chunk)
                chunk_bytelen = chruncate_frames(chunk)

            chunk_bytelen += append_frame(chunk, pkg.frame)

        if chunk:
            yield sfhl_from_frames(chunk)
            chunk_bytelen = chruncate_frames(chunk)


class SeriesFrameFilter(ASeriesFrameEmitter):
    """
    Class defining the characteristics of a filter, which is a kind of emitter.

    The difference between a filter and an emitter is:

    1. an emitter doesn't know how to provide `frames` (no concrete
    implementation)
    2. a filter assumes it will read `frames` from another injected emitter
    (called a `reader`), and by default simply returns those `frames`
    unadulterated.
    """

    def __init__(
        self,
        reader: Optional[ASeriesFrameEmitter] = None,
        **kwargs
    ) -> None:
        """
        :param:`reader` - input stream this filter reads.
        """
        super().__init__(**kwargs)
        self._reader = reader
        self._cursor: Iterator[SeriesFramePackage] = iter(())

    @property
    def reader(self) -> ASeriesFrameEmitter:
        """
        The input stream this filter reads.
        """
        if self._reader is None:
            raise RuntimeError("Illegal state: `reader` is None.")
        return self._reader

    @reader.setter
    def reader(self, val):
        self._reader = val

    def frames(self):
        """
        This has to exist because it's subclassing an abstract class that
        declares it, but a concrete implementation of this would be confusing.
        Generally speaking, implementations should read the `self.reader` and
        do something to modify the frames.
        """
        raise NotImplementedError()


def sfhl_from_frames(
    frames: Iterator[FlatBufferWrapper[SeriesFrame]]
) -> FlatBufferWrapper[SeriesFrameHolderList]:
    builder = fb_builder.Builder(0)
    builder.Finish(
        fb_builder.CreateSeriesFrameHolderList(
            builder=builder,
            frames=[
                fb_builder.CreateSeriesFrameHolder(
                    builder=builder,
                    data=frame.bytes,
                )
                for frame in frames
            ]
        )
    )

    return FlatBufferWrapper(
        _bytes=builder.Output(),
        schema=SeriesFrameHolderList,
    )


def append_frame(
    chunk: List[FlatBufferWrapper[SeriesFrame]],
    frame: FlatBufferWrapper[SeriesFrame],
) -> int:
    """
    Appends a frame to the given chunk and returns the bytelen it appended.
    """
    chunk.append(frame)
    return len(frame.bytes)


def chruncate_frames(
    chunk: List[FlatBufferWrapper[SeriesFrame]],
) -> Literal[0]:
    """
    "Truncates" a "chunk" of frames to zero. Returns bytelen (constantly `0`)
    to be consistent with :func:`append_frame`.
    """
    chunk.clear()
    return 0
