from dataclasses import dataclass
from enum import Enum
from typing import Dict, List, Optional, Tuple
import numpy as np
# Define the public API of this module
__all__ = [
    "BlockInfo",
    "FieldInfo",
    "UintFieldInfo",
    "ScalarFieldInfo",
    "TimeFieldInfo",
    "SubtypeTimeFieldInfo",
    "EnumFieldInfo",
    "BitOutFieldInfo",
    "BitMuxFieldInfo",
    "PosMuxFieldInfo",
    "TableFieldDetails",
    "TableFieldInfo",
    "PosOutFieldInfo",
    "ExtOutFieldInfo",
    "ExtOutBitsFieldInfo",
    "Changes",
    "EndReason",
    "FieldCapture",
    "Data",
    "ReadyData",
    "StartData",
    "FrameData",
    "EndData",
]
# Control
[docs]
@dataclass
class BlockInfo:
    """Block number and description as exposed by the TCP server
    Attributes:
        number: The index of this block
        description: The description for this block"""
    number: int = 0
    description: Optional[str] = None 
[docs]
@dataclass
class FieldInfo:
    """Field type, subtype, description and labels as exposed by TCP server:
    https://pandablocks-server.readthedocs.io/en/latest/fields.html#field-types
    Note that many fields will use a more specialised subclass of FieldInfo for
    their additional attributes.
    Attributes:
        type: Field type, like "param", "bit_out", "pos_mux", etc.
        subtype: Some types have subtype, like "uint", "scalar", "lut", etc.
        description: A description of the field
        labels: A list of the valid values for the field when there is a defined list
            of valid values, e.g. those with sub-type "enum"
    """
    type: str
    subtype: Optional[str]
    description: Optional[str] 
[docs]
@dataclass
class UintFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "param","read", or "write" and subtype
    "uint"""
    max_val: int 
[docs]
@dataclass
class ScalarFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "param","read", or "write" and subtype
    "scalar"""
    units: str
    scale: float
    offset: float 
[docs]
@dataclass
class TimeFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "time"""
    units_labels: List[str]
    min_val: float 
[docs]
@dataclass
class SubtypeTimeFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "param","read", or "write" and subtype
    "time"""
    units_labels: List[str] 
[docs]
@dataclass
class EnumFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "param","read", or "write" and subtype
    "enum"""
    labels: List[str] 
[docs]
@dataclass
class BitOutFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "bit_out"""
    capture_word: str
    offset: int 
[docs]
@dataclass
class BitMuxFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "bit_mux"""
    max_delay: int
    labels: List[str] 
[docs]
@dataclass
class PosMuxFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "pos_mux"""
    labels: List[str] 
[docs]
@dataclass
class TableFieldDetails:
    """Info for each field in a table"""
    subtype: str
    bit_low: int
    bit_high: int
    description: Optional[str] = None
    labels: Optional[List[str]] = None 
[docs]
@dataclass
class TableFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "table"""
    max_length: int
    fields: Dict[str, TableFieldDetails]
    row_words: int 
[docs]
@dataclass
class PosOutFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "pos_out"""
    capture_labels: List[str] 
[docs]
@dataclass
class ExtOutFieldInfo(FieldInfo):
    """Extended `FieldInfo` for fields with type "ext_out" and subtypes "timestamp"
    or "samples"""
    capture_labels: List[str] 
[docs]
@dataclass
class ExtOutBitsFieldInfo(ExtOutFieldInfo):
    """Extended `ExtOutFieldInfo` for fields with type "ext_out" and subtype "bits"""
    bits: List[str] 
[docs]
@dataclass
class Changes:
    """The changes returned from a ``*CHANGES`` command"""
    #: Map field -> value for single-line values that were returned
    values: Dict[str, str]
    #: The fields that were present but without value
    no_value: List[str]
    #: The fields that were in error
    in_error: List[str]
    #: Map field -> value for multi-line values that were returned
    multiline_values: Dict[str, List[str]] 
# Data
[docs]
class EndReason(Enum):
    """The reason that a PCAP acquisition completed"""
    #: Experiment completed by falling edge of ``PCAP.ENABLE```
    OK = "Ok"
    #: Client disconnect detected
    EARLY_DISCONNECT = "Early disconnect"
    #: Client not taking data quickly or network congestion, internal buffer overflow.
    #: In raw unscaled mode (i.e., no server-side scaling), the most recent
    #: `FrameData` is likely corrupted.
    DATA_OVERRUN = "Data overrun"
    #: Triggers too fast for configured data capture
    FRAMING_ERROR = "Framing error"
    #: Probable CPU overload on PandA, should not occur
    DRIVER_DATA_OVERRUN = "Driver data overrun"
    #: Data capture too fast for memory bandwidth
    DMA_DATA_ERROR = "DMA data error"
    # Reasons below this point are not from the server, they are generated in code
    #: An unknown exception occurred during HDF5 file processing
    UNKNOWN_EXCEPTION = "Unknown exception"
    #: StartData packets did not match when trying to continue printing to a file
    START_DATA_MISMATCH = "Start Data mismatched"
    #: Experiment manually completed by ``DATA:CAPTURE``
    MANUALLY_STOPPED = "Manually stopped"
    #: Experiment manually completed by ``*PCAP.DISARM=`` command
    DISARMED = "Disarmed" 
[docs]
@dataclass
class FieldCapture:
    """Information about a field that is being captured
    Attributes:
        name: Name of captured field
        type: Numpy data type of the field as transmitted
        capture: Value of CAPTURE field used to enable this field
        scale: Scaling factor, default 1.0
        offset: Offset, default 0.0
        units: Units string, default ""
    """
    name: str
    type: np.dtype
    capture: str
    scale: float = 1.0
    offset: float = 0.0
    units: str = "" 
[docs]
class Data:
    """Baseclass for all responses yielded by a `DataConnection`""" 
[docs]
@dataclass
class ReadyData(Data):
    """Yielded once when the connection is established and ready to take data""" 
[docs]
@dataclass
class StartData(Data):
    """Yielded when a new PCAP acquisition starts.
    Attributes:
        fields: Information about each captured field as a `FieldCapture` object
        missed: Number of samples missed by late data port connection
        process: Data processing option, only "Scaled" or "Raw" are requested
        format: Data delivery formatting, only "Framed" is requested
        sample_bytes: Number of bytes in one sample
    """
    fields: List[FieldCapture]
    missed: int
    process: str
    format: str
    sample_bytes: int 
[docs]
@dataclass
class FrameData(Data):
    """Yielded when a new data frame is flushed.
    Attributes:
        data: A numpy `Structured Array <structured_arrays>`
    Data is structured into complete columns. Each column name is
    ``<name>.<capture>`` from the corresponding `FieldInfo`. Data
    can be accessed with these column names. For example::
        # Table view with 2 captured fields
        >>> import numpy
        >>> data = numpy.array([(0, 10),
        ...       (1, 11),
        ...       (2, 12)],
        ...      dtype=[('COUNTER1.OUT.Value', '<f8'), ('COUNTER2.OUT.Value', '<f8')])
        >>> fdata = FrameData(data)
        >>> fdata.data[0] # Row view
        (0., 10.)
        >>> fdata.column_names # Column names
        ('COUNTER1.OUT.Value', 'COUNTER2.OUT.Value')
        >>> fdata.data['COUNTER1.OUT.Value'] # Column view
        array([0., 1., 2.])
    """
    data: np.ndarray
    @property
    def column_names(self) -> Tuple[str, ...]:
        """Return all the column names"""
        names = self.data.dtype.names
        assert names, f"No column names for {self.data.dtype}"
        return names 
[docs]
@dataclass
class EndData(Data):
    """Yielded when a PCAP acquisition ends.
    Attributes:
        samples: The total number of samples (rows) that were yielded
        reason: The `EndReason` for the end of acquisition
    """
    samples: int
    reason: EndReason