# ------------------------------------------------------------------------------
# Copyright (c) Acoular Development Team.
# ------------------------------------------------------------------------------
"""Implements base classes for signal processing blocks in Acoular.

The classes in this module are abstract base classes that provide a common interface for all classes
that generate an output via the ``result`` generator in block-wise manner. They are not intended to
be used directly, but to be subclassed by classes that implement the actual signal processing.

.. inheritance-diagram::
                acoular.base
    :top-classes:
                acoular.base.Generator
    :parts: 1

.. autosummary::
    :toctree: generated/

    Generator
    SamplesGenerator
    SpectraGenerator
    InOut
    TimeOut
    SpectraOut
"""

from abc import abstractmethod

# acoular imports
from .internal import digest

from traits.api import (
    ABCHasStrictTraits,
    CArray,
    CInt,
    Delegate,
    Float,
    Instance,
    Property,
    cached_property,
)


class Generator(ABCHasStrictTraits):
    """Interface for any generating signal processing block.

    It provides a common interface for all classes, which generate an output via the generator
    :meth:`result` in block-wise manner. It has a common set of traits that are used by all classes
    that implement this interface. This includes the sampling frequency of the signal
    (:attr:`sample_freq`), the number of samples (:attr:`num_samples`), and the number of channels
    (:attr:`num_channels`). A private trait :attr:`digest` is used to store the internal identifier
    of the object, which is a hash of the object's attributes.
    This is used to check if the object's internal state has changed.

    """

    #: Sampling frequency of the signal, defaults to 1.0
    sample_freq = Float(1.0)

    #: Number of signal samples
    num_samples = CInt

    #: Number of channels
    num_channels = CInt

    #: A unique identifier for the generator, based on its properties. (read-only)
    digest = Property(depends_on=['sample_freq', 'num_samples', 'num_channels'])

    def _get_digest(self):
        return digest(self)

    @abstractmethod
    def result(self, num):
        """Python generator that yields the output block-wise.

        This method needs to be implemented by the derived classes.

        Parameters
        ----------
        num : int
            The size of the first dimension of the blocks to be yielded

        Yields
        ------
        numpy.ndarray
            Two-dimensional output data block of shape (num, ...)
        """


class SamplesGenerator(Generator):
    """Interface for any generating multi-channel time domain signal processing block.

    It provides a common interface for all SamplesGenerator classes, which generate an output via
    the generator :meth:`result` in block-wise manner. This class has no real functionality on its
    own and should not be used directly.

    """

    #: A unique identifier for the generator, based on its properties. (read-only)
    digest = Property(depends_on=['sample_freq', 'num_samples', 'num_channels'])

    def _get_digest(self):
        return digest(self)

    @abstractmethod
    def result(self, num):
        """Python generator that yields the output block-wise.

        Parameters
        ----------
        num : int
            This parameter defines the size of the blocks to be yielded
            (i.e. the number of samples per block)

        Yields
        ------
        numpy.ndarray
            The two-dimensional time-data block of shape (num, num_channels).
        """


class SpectraGenerator(Generator):
    """Interface for any generating multi-channel signal frequency domain processing block.

    It provides a common interface for all SpectraGenerator classes, which generate an output via
    the generator :meth:`result` in block-wise manner. This class has no real functionality on its
    own and should not be used directly.

    """

    #: Number of frequencies
    num_freqs = CInt

    #: 1-D array of frequencies
    freqs = CArray

    #: The length of the block used to calculate the spectra
    block_size = CInt

    #: A unique identifier for the generator, based on its properties. (read-only)
    digest = Property(depends_on=['sample_freq', 'num_samples', 'num_channels', 'num_freqs', 'block_size'])

    def _get_digest(self):
        return digest(self)

    @abstractmethod
    def result(self, num=1):
        """Python generator that yields the output block-wise.

        Parameters
        ----------
        num : int, optional
            Maximum number of spectral snapshots per block to yield.
            Defaults to 1.

        Yields
        ------
        numpy.ndarray
            Two-dimensional spectral data block with shape
            ``(n, num_freqs * num_channels)``, where ``n`` is the
            number of snapshots in the yielded block and is at most ``num``. The final
            block may contain fewer than ``num`` snapshots.

            The second axis stores the spectra in frequency-major order with channels
            interlaced for each frequency bin. Reshagping a block with
            ``block.reshape(n, num_freqs, num_channels)`` gives direct access to the
            data by snapshot, frequency, and channel.
        """


class TimeOut(SamplesGenerator):
    """
    Abstract base class receiving from a ``source`` and returning time domain signals.

    It provides a base class that can be used to create signal processing blocks that receive data
    from any generating :attr:`source` and generates a time signal output via the :meth:`result`
    generator in block-wise manner.
    """

    #: Data source; :class:`~acoular.base.Generator` or derived object.
    source = Instance(Generator)

    #: Sampling frequency of output signal, as given by :attr:`source`.
    sample_freq = Delegate('source')

    #: Number of channels in output, as given by :attr:`source`.
    num_channels = Delegate('source')

    #: Number of samples in output, as given by :attr:`source`.
    num_samples = Delegate('source')

    #: A unique identifier for the generator, based on its properties. (read-only)
    digest = Property(depends_on=['source.digest'])

    @cached_property
    def _get_digest(self):
        return digest(self)

    @abstractmethod
    def result(self, num):
        """Python generator that processes the source data and yields the time-signal block-wise.

        This method needs to be implemented by the derived classes.

        Parameters
        ----------
        num : int
            This parameter defines the size of the blocks to be yielded
            (i.e. the number of samples per block)

        Yields
        ------
        numpy.ndarray
            Two-dimensional output data block of shape (num, num_channels)
        """


class SpectraOut(SpectraGenerator):
    """
    Abstract base class receiving from a ``source`` and returning frequency domain signals.

    It provides a base class that can be used to create signal processing blocks that receive data
    from any generating :attr:`source` domain and generates a frequency domain output via the
    :meth:`result` generator in block-wise manner.
    """

    #: Data source; :class:`~acoular.base.Generator` or derived object.
    source = Instance(Generator)

    #: Sampling frequency of output signal, as given by :attr:`source`.
    sample_freq = Delegate('source')

    #: Number of channels in output, as given by :attr:`source`.
    num_channels = Delegate('source')

    #: Number of snapshots in output, as given by :attr:`source`.
    num_samples = Delegate('source')

    #: Number of frequencies in output, as given by :attr:`source`.
    num_freqs = Delegate('source')

    #: 1-D array of frequencies, as given by :attr:`source`.
    freqs = Delegate('source')

    #: The size of the block used to calculate the spectra
    block_size = Delegate('source')

    #: A unique identifier for the generator, based on its properties. (read-only)
    digest = Property(depends_on=['source.digest'])

    @cached_property
    def _get_digest(self):
        return digest(self)

    @abstractmethod
    def result(self, num=1):
        """Python generator that processes the source data and yields the output block-wise.

        This method needs to be implemented by the derived classes.

        Parameters
        ----------
        num : int, optional
            Maximum number of spectral snapshots per block to yield.
            Defaults to 1.

        Yields
        ------
        numpy.ndarray
            Two-dimensional spectral data block with shape
            (n, :attr:`num_freqs` ``*`` :attr:`num_channels`), where ``n`` is the
            number of snapshots in the yielded block and is at most ``num``. The final
            block may contain fewer than ``num`` snapshots.

            The second axis stores the spectra in frequency-major order with channels
            interlaced for each frequency bin. Reshaping a block with
            ``block.reshape(n, num_freqs, num_channels)`` gives direct access to the
            data by snapshot, frequency, and channel.
        """


class InOut(SamplesGenerator, SpectraGenerator):
    """
    Abstract base class receiving from a ``source`` and returning signals in the same domain.

    It provides a base class that can be used to create signal processing blocks that receive data
    from any generating :attr:`source` and generates an output via the :meth:`result` generator in
    block-wise manner.
    """

    #: Data source; :class:`~acoular.base.Generator` or derived object.
    source = Instance(Generator)

    #: Sampling frequency of output signal, as given by :attr:`source`.
    sample_freq = Delegate('source')

    #: Number of channels in output, as given by :attr:`source`.
    num_channels = Delegate('source')

    #: Number of frequencies in output, as given by :attr:`source`.
    num_freqs = Delegate('source')

    #: Number of samples / snapshots in output, as given by :attr:`source`.
    num_samples = Delegate('source')

    #: A unique identifier for the generator, based on its properties. (read-only)
    digest = Property(depends_on=['source.digest'])

    @cached_property
    def _get_digest(self):
        return digest(self)

    @abstractmethod
    def result(self, num):
        """Python generator that processes the source data and yields the output block-wise.

        This method needs to be implemented by the derived classes.

        Parameters
        ----------
        num : int
            The size of the first dimension of the blocks to be yielded

        Yields
        ------
        numpy.ndarray
            Two-dimensional output data block of shape (num, ...)
        """
