from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import abc
import collections
import typing  # NOQA: F401

import six

from nixnet import _cconsts
from nixnet import _errors
from nixnet import _py2
from nixnet import constants

__all__ = [
    'DriverVersion',
    'CanComm',
    'LinComm',
    'CanIdentifier',
    'FrameFactory',
    'Frame',
    'RawFrame',
    'CanFrame',
    'CanBusErrorFrame',
    'LinFrame',
    'LinBusErrorFrame',
    'DelayFrame',
    'LogTriggerFrame',
    'StartTriggerFrame',
    'XnetFrame',
    'PduProperties']


DriverVersion_ = collections.namedtuple(
    'DriverVersion_',
    ['major', 'minor', 'update', 'phase', 'build'])


class DriverVersion(DriverVersion_):
    """Driver Version

    The arguments align with the following fields: ``[major].[minor].[update][phase][build]``.

    Attributes:
        major (int):
        minor (int):
        update (int):
        phase (:any:`nixnet._enums.Phase`):
        build (int):
    """


CanComm_ = collections.namedtuple(
    'CanComm_',
    ['state', 'tcvr_err', 'sleep', 'last_err', 'tx_err_count', 'rx_err_count'])


class CanComm(CanComm_):
    """CAN Communication State.

    Attributes:
        state (:any:`nixnet._enums.CanCommState`): Communication State
        tcvr_err (bool): Transceiver Error.
            Transceiver error indicates whether an error condition exists on
            the physical transceiver. This is typically referred to as the
            transceiver chip NERR pin.  False indicates normal operation (no
            error), and true indicates an error.
        sleep (bool): Sleep.
            Sleep indicates whether the transceiver and communication
            controller are in their sleep state. False indicates normal
            operation (awake), and true indicates sleep.
        last_err (:any:`nixnet._enums.CanLastErr`): Last Error.
            Last error specifies the status of the last attempt to receive or
            transmit a frame
        tx_err_count (int): Transmit Error Counter.
            The transmit error counter begins at 0 when communication starts on
            the CAN interface. The counter increments when an error is detected
            for a transmitted frame and decrements when a frame transmits
            successfully. The counter increases more for an error than it is
            decreased for success. This ensures that the counter generally
            increases when a certain ratio of frames (roughly 1/8) encounter
            errors.
            When communication state transitions to Bus Off, the transmit error
            counter no longer is valid.
        rx_err_count (int): Receive Error Counter.
            The receive error counter begins at 0 when communication starts on
            the CAN interface. The counter increments when an error is detected
            for a received frame and decrements when a frame is received
            successfully. The counter increases more for an error than it is
            decreased for success. This ensures that the counter generally
            increases when a certain ratio of frames (roughly 1/8) encounter
            errors.
    """

    pass


LinComm_ = collections.namedtuple(
    'LinComm_',
    ['sleep', 'state', 'last_err', 'err_received', 'err_expected', 'err_id', 'tcvr_rdy', 'sched_index'])


class LinComm(LinComm_):
    """CAN Communication State.

    Attributes:
        sleep (bool): Sleep.
            Indicates whether the transceiver and communication
            controller are in their sleep state. False indicates normal
            operation (awake), and true indicates sleep.
        state (:any:`nixnet._enums.LinCommState`): Communication State
        last_err (:any:`nixnet._enums.LinLastErr`): Last Error.
            Last error specifies the status of the last attempt to receive or
            transmit a frame
        err_received (int): Returns the value received from the network
            when last error occurred.

            When ``last_err`` is ``READBACK``, this is the value read back.

            When ``last_err`` is ``CHECKSUM``, this is the received checksum.
        err_expected (int): Returns the value that the LIN interface
            expected to see (instead of last received).

            When ``last_err`` is ``READBACK``, this is the value transmitted.

            When ``last_err`` is ``CHECKSUM``, this is the calculated checksum.
        err_id (int): Returns the frame identifier in which the last error
            occurred.

            This is not applicable when ``last_err`` is ``NONE`` or ``UNKNOWN_ID``.
        tcvr_rdy (bool): Indicates whether the LIN transceiver is powered from
            the bus.

            True indicates the bus power exists, so it is safe to start
            communication on the LIN interface.

            If this value is false, you cannot start communication
            successfully. Wire power to the LIN transceiver and run your
            application again.
        sched_index (int): Indicates the LIN schedule that the interface
            currently is running.

            This index refers to a LIN schedule that you requested using the
            :any:`nixnet._session.base.SessionBase.change_lin_schedule` function. It
            indexes the array of schedules represented in the
            :any:`nixnet._session.intf.Interface.lin_sched_names`.

            This index applies only when the LIN interface is running as a
            master. If the LIN interface is running as a slave only, this
            element should be ignored.
        """

    pass


PduProperties_ = collections.namedtuple(
    'PDU_PROPERTIES_',
    ['pdu', 'start_bit', 'update_bit'])


class PduProperties(PduProperties_):
    """Properties that map a PDU onto a frame.

    Mapping PDUs to a frame requires setting three frame properties that are combined into this tuple.

    Attributes:
        pdu (:any:`Pdu`): Defines the sequence of values for the other two properties.
        start_bit (int): Defines the start bit of the PDU inside the frame.
        update_bit (int): Defines the update bit for the PDU inside the frame.
            If the update bit is not used, set the value to ``-1``.
    """


class CanIdentifier(object):
    """CAN frame arbitration identifier.

    Attributes:
        identifier(int): CAN frame arbitration identifier
        extended(bool): If the identifier is extended
    """

    _FRAME_ID_MASK = 0x000007FF
    _EXTENDED_FRAME_ID_MASK = 0x1FFFFFFF

    def __init__(self, identifier, extended=False):
        # type: (int, bool) -> None
        self.identifier = identifier
        self.extended = extended

    @classmethod
    def from_raw(cls, raw):
        # type: (int) -> CanIdentifier
        """Parse a raw frame identifier into a CanIdentifier

        Args:
            raw(int): A raw frame identifier

        Returns:
            CanIdentifier: parsed value

        >>> CanIdentifier.from_raw(0x1)
        CanIdentifier(0x1)
        >>> CanIdentifier.from_raw(0x20000001)
        CanIdentifier(0x1, extended=True)
        """
        extended = bool(raw & _cconsts.NX_FRAME_ID_CAN_IS_EXTENDED)
        if extended:
            identifier = raw & cls._EXTENDED_FRAME_ID_MASK
        else:
            identifier = raw & cls._FRAME_ID_MASK
        return cls(identifier, extended)

    def __int__(self):
        """Convert CanIdentifier into a raw frame identifier

        >>> hex(int(CanIdentifier(1)))
        '0x1'
        >>> hex(int(CanIdentifier(1, True)))
        '0x20000001'
        """
        identifier = self.identifier
        if self.extended:
            if identifier != (identifier & self._EXTENDED_FRAME_ID_MASK):
                _errors.check_for_error(_cconsts.NX_ERR_UNDEFINED_FRAME_ID)
            identifier |= _cconsts.NX_FRAME_ID_CAN_IS_EXTENDED
        else:
            if identifier != (identifier & self._FRAME_ID_MASK):
                _errors.check_for_error(_cconsts.NX_ERR_UNDEFINED_FRAME_ID)
        return identifier

    def __eq__(self, other):
        if isinstance(other, CanIdentifier):
            other_id = typing.cast(CanIdentifier, other)
            return all((
                self.identifier == other_id.identifier,
                self.extended == other_id.extended))
        else:
            return NotImplemented

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is NotImplemented:
            return result
        else:
            return not result

    def __repr__(self):
        """CanIdentifier debug representation.

        >>> CanIdentifier(1)
        CanIdentifier(0x1)
        >>> CanIdentifier(1, True)
        CanIdentifier(0x1, extended=True)
        """
        if self.extended:
            return "{}(0x{:x}, extended={})".format(
                type(self).__name__,
                self.identifier,
                self.extended)
        else:
            return "{}(0x{:x})".format(
                type(self).__name__,
                self.identifier)


@six.add_metaclass(abc.ABCMeta)
class FrameFactory(object):
    """ABC for creating :any:`nixnet.types.Frame` objects."""

    __slots__ = ()

    @_py2.abstractclassmethod
    def from_raw(cls, frame):  # NOQA: N805 can't detect abstractclassmethod
        # No type annotation because mypy doesn't understand
        # abstractclassmethod is the same as classmethod
        """Convert from RawFrame."""
        pass


@six.add_metaclass(abc.ABCMeta)
class Frame(FrameFactory):
    """ABC for frame objects."""

    __slots__ = ()

    @abc.abstractmethod
    def to_raw(self):
        # type: () -> RawFrame
        """Convert to RawFrame."""
        pass

    @abc.abstractproperty
    def type(self):
        # type: () -> constants.FrameType
        """:any:`nixnet._enums.FrameType`: Frame format."""
        pass

    @abc.abstractmethod
    def __eq__(self, other):
        pass

    def __ne__(self, other):
        result = self.__eq__(other)
        if result is NotImplemented:
            return result
        else:
            return not result

    @abc.abstractmethod
    def __repr__(self):
        pass


class RawFrame(Frame):
    """Raw Frame.

    Attributes:
        timestamp(int): Absolute time the XNET interface received the end-of-frame.
        identifier(int): Frame identifier.
        type(:any:`nixnet._enums.FrameType`): Frame type.
        flags(int): Flags that qualify the type.
        info(int): Info that qualify the type.
        payload(bytes): Payload.
    """

    __slots__ = [
        "timestamp",
        "identifier",
        "_type",
        "flags",
        "info",
        "payload"]

    def __init__(self, timestamp, identifier, type, flags=0, info=0, payload=b""):
        # type: (int, int, constants.FrameType, int, int, bytes) -> None
        self.timestamp = timestamp
        self.identifier = identifier
        self._type = type
        self.flags = flags
        self.info = info
        self.payload = payload

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame."""
        return frame

    def to_raw(self):
        """Convert to RawFrame."""
        return self

    @property
    def type(self):
        return self._type

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(RawFrame, other)
            return all((
                self.timestamp == other_frame.timestamp,
                self.identifier == other_frame.identifier,
                self.type == other_frame.type,
                self.flags == other_frame.flags,
                self.info == other_frame.info,
                self.payload == other_frame.payload))
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """RawFrame debug representation.

        >>> RawFrame(1, 2, constants.FrameType.CAN_DATA, 3, 4)
        RawFrame(timestamp=0x1, identifier=0x2, type=FrameType.CAN_DATA, flags=0x3, info=0x4)
        """
        optional = []
        if self.flags != 0:
            optional.append('flags=0x{:x}'.format(self.flags))
        if self.info != 0:
            optional.append('info=0x{:x}'.format(self.info))
        if self.payload:
            optional.append('len(payload)={}'.format(len(self.payload)))
        if optional:
            optional_params = ', {}'.format(", ".join(optional))
        else:
            optional_params = ''
        return "{}(timestamp=0x{:x}, identifier=0x{:x}, type={}{})".format(
            type(self).__name__,
            self.timestamp,
            self.identifier,
            self.type,
            optional_params)


class CanFrame(Frame):
    """CAN Frame.

    Attributes:
        identifier(:any:`nixnet.types.CanIdentifier`): CAN frame arbitration identifier.
        echo(bool): If the frame is an echo of a successful
            transmit rather than being received from the network.
        type(:any:`nixnet._enums.FrameType`): Frame type.
        timestamp(int): Absolute time the XNET interface received the end-of-frame.
        payload(bytes): Payload.
    """

    __slots__ = [
        "identifier",
        "echo",
        "_type",
        "timestamp",
        "payload"]

    def __init__(self, identifier, type=constants.FrameType.CAN_DATA, payload=b""):
        # type: (typing.Union[CanIdentifier, int], constants.FrameType, bytes) -> None
        if isinstance(identifier, int):
            self.identifier = CanIdentifier(identifier)
        else:
            self.identifier = identifier
        self.echo = False  # Used only for Read
        self._type = type
        self.timestamp = 0  # Used only for Read
        self.payload = payload

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame.

        >>> raw = RawFrame(5, 0x20000001, constants.FrameType.CAN_DATA, _cconsts.NX_FRAME_FLAGS_TRANSMIT_ECHO, 0, b'')
        >>> CanFrame.from_raw(raw)
        CanFrame(CanIdentifier(0x1, extended=True), echo=True, timestamp=0x5)
        """
        identifier = CanIdentifier.from_raw(frame.identifier)
        can_frame = CanFrame(identifier, constants.FrameType(frame.type), frame.payload)
        can_frame.timestamp = frame.timestamp
        can_frame.echo = bool(frame.flags & _cconsts.NX_FRAME_FLAGS_TRANSMIT_ECHO)
        return can_frame

    def to_raw(self):
        """Convert to RawFrame.

        >>> CanFrame(CanIdentifier(1, True), constants.FrameType.CAN_DATA).to_raw()
        RawFrame(timestamp=0x0, identifier=0x20000001, type=FrameType.CAN_DATA)
        >>> c = CanFrame(CanIdentifier(1, True), constants.FrameType.CAN_DATA)
        >>> c.echo = True
        >>> c.to_raw()
        RawFrame(timestamp=0x0, identifier=0x20000001, type=FrameType.CAN_DATA, flags=0x80)
        """
        identifier = int(self.identifier)
        flags = 0
        if self.echo:
            flags |= _cconsts.NX_FRAME_FLAGS_TRANSMIT_ECHO
        return RawFrame(self.timestamp, identifier, self.type, flags, 0, self.payload)

    @property
    def type(self):
        return self._type

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(CanFrame, other)
            return all((
                self.identifier == other_frame.identifier,
                self.echo == other_frame.echo,
                self.type == other_frame.type,
                self.timestamp == other_frame.timestamp,
                self.payload == other_frame.payload))
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """CanFrame debug representation.

        >>> CanFrame(1)
        CanFrame(CanIdentifier(0x1))
        >>> CanFrame(1, constants.FrameType.CANFD_DATA, b'\x01')
        CanFrame(CanIdentifier(0x1), type=FrameType.CANFD_DATA, len(payload)=1)
        """
        optional = []
        if self.echo:
            optional.append('echo={}'.format(self.echo))
        if self.type != constants.FrameType.CAN_DATA:
            optional.append('type={}'.format(self.type))
        if self.timestamp != 0:
            optional.append('timestamp=0x{:x}'.format(self.timestamp))
        if self.payload:
            optional.append('len(payload)={}'.format(len(self.payload)))
        if optional:
            optional_params = ', {}'.format(", ".join(optional))
        else:
            optional_params = ''
        return "{}({}{})".format(
            type(self).__name__,
            self.identifier,
            optional_params)


class CanBusErrorFrame(Frame):
    """Error detected on hardware bus of a :any:`nixnet.session.FrameInStreamSession`.

    .. note:: This requires enabling
       :any:`nixnet._session.intf.Interface.bus_err_to_in_strm`.

    See also :any:`nixnet.types.CanComm`.

    Attributes:
        timestamp(int): Absolute time when the bus error occurred.
        state (:any:`nixnet._enums.CanCommState`): Communication State
        tcvr_err (bool): Transceiver Error.
        bus_err (:any:`nixnet._enums.CanLastErr`): Last Error.
        tx_err_count (int): Transmit Error Counter.
        rx_err_count (int): Receive Error Counter.
    """

    __slots__ = [
        "timestamp",
        "state",
        "tcvr_err",
        "bus_err",
        "tx_err_count",
        "rx_err_count"]

    def __init__(self, timestamp, state, tcvr_err, bus_err, tx_err_count, rx_err_count):
        # type: (int, constants.CanCommState, bool, constants.CanLastErr, int, int) -> None
        self.timestamp = timestamp
        self.state = state
        self.tcvr_err = tcvr_err
        self.bus_err = bus_err
        self.tx_err_count = tx_err_count
        self.rx_err_count = rx_err_count

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame.

        >>> raw = RawFrame(0x64, 0x0, constants.FrameType.CAN_BUS_ERROR, 0, 0, b'\\x00\\x01\\x02\\x03\\x04')
        >>> CanBusErrorFrame.from_raw(raw)
        CanBusErrorFrame(0x64, CanCommState.ERROR_ACTIVE, True, CanLastErr.ACK, 1, 2)
        """
        timestamp = frame.timestamp
        state = constants.CanCommState(six.indexbytes(frame.payload, 0))
        tx_err_count = six.indexbytes(frame.payload, 1)
        rx_err_count = six.indexbytes(frame.payload, 2)
        bus_err = constants.CanLastErr(six.indexbytes(frame.payload, 3))
        tcvr_err = six.indexbytes(frame.payload, 4) != 0
        return CanBusErrorFrame(timestamp, state, tcvr_err, bus_err, tx_err_count, rx_err_count)

    def to_raw(self):
        """Convert to RawFrame.

        >>> CanBusErrorFrame(100, constants.CanCommState.BUS_OFF, True, constants.CanLastErr.STUFF, 1, 2).to_raw()
        RawFrame(timestamp=0x64, identifier=0x0, type=FrameType.CAN_BUS_ERROR, len(payload)=5)
        """
        identifier = 0
        flags = 0
        info = 0

        payload_data = [
            self.state.value,
            self.tx_err_count,
            self.rx_err_count,
            self.bus_err.value,
            1 if self.tcvr_err else 0,
        ]
        payload = bytes(bytearray(payload_data))
        return RawFrame(self.timestamp, identifier, self.type, flags, info, payload)

    @property
    def type(self):
        return constants.FrameType.CAN_BUS_ERROR

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(CanBusErrorFrame, other)
            return all((
                self.timestamp == other_frame.timestamp,
                self.state == other_frame.state,
                self.tcvr_err == other_frame.tcvr_err,
                self.bus_err == other_frame.bus_err,
                self.tx_err_count == other_frame.tx_err_count,
                self.rx_err_count == other_frame.rx_err_count))
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """CanBusErrorFrame debug representation.

        >>> CanBusErrorFrame(100, constants.CanCommState.BUS_OFF, True, constants.CanLastErr.STUFF, 1, 2)
        CanBusErrorFrame(0x64, CanCommState.BUS_OFF, True, CanLastErr.STUFF, 1, 2)
        """
        return "{}(0x{:x}, {}, {}, {}, {}, {})".format(
            type(self).__name__,
            self.timestamp,
            self.state,
            self.tcvr_err,
            self.bus_err,
            self.tx_err_count,
            self.rx_err_count)


class LinFrame(object):
    """LIN Frame.

    Attributes:
        identifier(int): LIN frame arbitration identifier.
        echo(bool): If the frame is an echo of a successful
            transmit rather than being received from the network.
        type(:any:`nixnet._enums.FrameType`): Frame type.
        timestamp(int): Absolute time the XNET interface received the end-of-frame.
        eventslot(bool): Whether the frame was received within an
            event-triggered slot or an unconditional or sporadic slot.
        eventid(int): Identifier for an event-triggered slot.
        payload(bytes): A byte string representing the payload.
    """

    __slots__ = [
        "identifier",
        "echo",
        "type",
        "timestamp",
        "eventslot",
        "eventid",
        "payload"]

    _FRAME_ID_MASK = 0x0000003F

    def __init__(self, identifier, type=constants.FrameType.LIN_DATA, payload=b""):
        # type: (int, constants.FrameType, bytes) -> None
        self.identifier = identifier
        self.echo = False  # Used only for Read
        self.type = type
        self.timestamp = 0  # Used only for Read
        self.eventslot = False  # Used only for Read
        self.eventid = 0  # Used only for Read
        self.payload = payload

    @classmethod
    def from_raw(cls, frame):
        # type: (RawFrame) -> LinFrame
        """Convert from RawFrame.

        >>> raw = RawFrame(5, 2, constants.FrameType.LIN_DATA, 0x81, 1, b'\x01')
        >>> LinFrame.from_raw(raw)
        LinFrame(identifier=0x2, echo=True, timestamp=0x5, eventslot=True, eventid=1, len(payload)=1)
        >>> raw = RawFrame(5, 2, constants.FrameType.LIN_DATA, _cconsts.NX_FRAME_FLAGS_TRANSMIT_ECHO, 0, b'\x01')
        >>> LinFrame.from_raw(raw)
        LinFrame(identifier=0x2, echo=True, timestamp=0x5, len(payload)=1)
        """
        identifier = frame.identifier & cls._FRAME_ID_MASK
        lin_frame = LinFrame(identifier, constants.FrameType(frame.type), frame.payload)
        lin_frame.timestamp = frame.timestamp
        lin_frame.echo = bool(frame.flags & _cconsts.NX_FRAME_FLAGS_TRANSMIT_ECHO)
        lin_frame.eventslot = bool(frame.flags & _cconsts.NX_FRAME_FLAGS_LIN_EVENT_SLOT)
        if lin_frame.eventslot:
            lin_frame.eventid = frame.info
        else:
            lin_frame.eventid = 0

        return lin_frame

    def to_raw(self):
        # type: () -> RawFrame
        """Convert to RawFrame.

        >>> LinFrame(2, constants.FrameType.LIN_DATA).to_raw()
        RawFrame(timestamp=0x0, identifier=0x2, type=FrameType.LIN_DATA)
        >>> l = LinFrame(2, constants.FrameType.LIN_DATA)
        >>> l.echo = True
        >>> l.eventslot = True
        >>> l.eventid = 1
        >>> l.to_raw()
        RawFrame(timestamp=0x0, identifier=0x2, type=FrameType.LIN_DATA, flags=0x81, info=0x1)
        """
        if self.identifier != (self.identifier & self._FRAME_ID_MASK):
            _errors.check_for_error(_cconsts.NX_ERR_UNDEFINED_FRAME_ID)
        flags = 0
        info = 0
        if self.echo:
            flags |= _cconsts.NX_FRAME_FLAGS_TRANSMIT_ECHO
        if self.eventslot:
            flags |= _cconsts.NX_FRAME_FLAGS_LIN_EVENT_SLOT
            info |= self.eventid
        return RawFrame(self.timestamp, self.identifier, self.type, flags, info, self.payload)

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(LinFrame, other)
            return all((
                self.identifier == other_frame.identifier,
                self.echo == other_frame.echo,
                self.type == other_frame.type,
                self.timestamp == other_frame.timestamp,
                self.eventslot == other_frame.eventslot,
                self.eventid == other_frame.eventid,
                self.payload == other_frame.payload))
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """LinFrame debug representation.

        >>> LinFrame(2)
        LinFrame(identifier=0x2)
        >>> LinFrame(2, constants.FrameType.LIN_NO_RESPONSE, b'\x01')
        LinFrame(identifier=0x2, type=FrameType.LIN_NO_RESPONSE, len(payload)=1)
        """
        optional = []
        if self.echo:
            optional.append('echo={}'.format(self.echo))
        if self.type != constants.FrameType.LIN_DATA:
            optional.append('type={}'.format(self.type))
        if self.timestamp != 0:
            optional.append('timestamp=0x{:x}'.format(self.timestamp))
        if self.eventslot:
            optional.append('eventslot={}'.format(self.eventslot))
        if self.eventid != 0:
            optional.append('eventid={}'.format(self.eventid))
        if self.payload:
            optional.append('len(payload)={}'.format(len(self.payload)))
        if optional:
            optional_params = ', {}'.format(", ".join(optional))
        else:
            optional_params = ''
        return "{}(identifier=0x{:x}{})".format(
            type(self).__name__,
            self.identifier,
            optional_params)


class LinBusErrorFrame(Frame):
    """Error detected on hardware bus of a :any:`nixnet.session.FrameInStreamSession`.

    .. note:: This requires enabling
       :any:`nixnet._session.intf.Interface.bus_err_to_in_strm`.

    See also :any:`nixnet.types.LinComm`.

    Attributes:
        timestamp(int): Absolute time when the bus error occurred.
        state (:any:`nixnet._enums.LinCommState`): Communication State.
        bus_err (:any:`nixnet._enums.LinLastErr`): Last Error.
        err_id (int): Identifier on bus.
        err_received (int): Received byte on bus
        err_expected (int): Expected byte on bus
    """

    __slots__ = [
        "timestamp",
        "state",
        "bus_err",
        "err_id",
        "err_received",
        "err_expected"]

    def __init__(self, timestamp, state, bus_err, err_id, err_received, err_expected):
        # type: (int, constants.LinCommState, constants.LinLastErr, int, int, int) -> None
        self.timestamp = timestamp
        self.state = state
        self.bus_err = bus_err
        self.err_id = err_id
        self.err_received = err_received
        self.err_expected = err_expected

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame.

        >>> raw = RawFrame(0x64, 0x0, constants.FrameType.LIN_BUS_ERROR, 0, 0, b'\\x00\\x01\\x02\\x03\\x04')
        >>> LinBusErrorFrame.from_raw(raw)
        LinBusErrorFrame(0x64, LinCommState.IDLE, LinLastErr.UNKNOWN_ID, 0x2, 3, 4)
        """
        timestamp = frame.timestamp
        state = constants.LinCommState(six.indexbytes(frame.payload, 0))
        bus_err = constants.LinLastErr(six.indexbytes(frame.payload, 1))
        err_id = six.indexbytes(frame.payload, 2)
        err_received = six.indexbytes(frame.payload, 3)
        err_expected = six.indexbytes(frame.payload, 4)
        return LinBusErrorFrame(timestamp, state, bus_err, err_id, err_received, err_expected)

    def to_raw(self):
        """Convert to RawFrame.

        >>> LinBusErrorFrame(100, constants.LinCommState.INACTIVE, constants.LinLastErr.UNKNOWN_ID, 2, 3, 4).to_raw()
        RawFrame(timestamp=0x64, identifier=0x0, type=FrameType.LIN_BUS_ERROR, len(payload)=5)
        """
        identifier = 0
        flags = 0
        info = 0

        payload_data = [
            self.state.value,
            self.bus_err.value,
            self.err_id,
            self.err_received,
            self.err_expected,
        ]
        payload = bytes(bytearray(payload_data))
        return RawFrame(self.timestamp, identifier, self.type, flags, info, payload)

    @property
    def type(self):
        return constants.FrameType.LIN_BUS_ERROR

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(LinBusErrorFrame, other)
            return all((
                self.timestamp == other_frame.timestamp,
                self.state == other_frame.state,
                self.bus_err == other_frame.bus_err,
                self.err_id == other_frame.err_id,
                self.err_received == other_frame.err_received,
                self.err_expected == other_frame.err_expected))
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """LinBusErrorFrame debug representation.

        >>> LinBusErrorFrame(100, constants.LinCommState.INACTIVE, constants.LinLastErr.CRC, 1, 2, 3)
        LinBusErrorFrame(0x64, LinCommState.INACTIVE, LinLastErr.CRC, 0x1, 2, 3)
        """
        return "{}(0x{:x}, {}, {}, 0x{:x}, {}, {})".format(
            type(self).__name__,
            self.timestamp,
            self.state,
            self.bus_err,
            self.err_id,
            self.err_received,
            self.err_expected)


class DelayFrame(Frame):
    """Delay hardware when DelayFrame is outputted.

    .. note:: This requires
       :any:`nixnet._session.intf.Interface.out_strm_timng` to be in replay mode.

    Attributes:
        offset(int): Time to delay in milliseconds.
    """

    __slots__ = [
        "offset"]

    def __init__(self, offset):
        # type: (int) -> None
        self.offset = offset

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame.

        >>> raw = RawFrame(5, 0, constants.FrameType.SPECIAL_DELAY, 0, 0, b'')
        >>> DelayFrame.from_raw(raw)
        DelayFrame(5)
        """
        return DelayFrame(frame.timestamp)

    def to_raw(self):
        """Convert to RawFrame.

        >>> DelayFrame(250).to_raw()
        RawFrame(timestamp=0xfa, identifier=0x0, type=FrameType.SPECIAL_DELAY)
        """
        identifier = 0
        flags = 0
        info = 0
        payload = b''
        return RawFrame(self.offset, identifier, self.type, flags, info, payload)

    @property
    def type(self):
        return constants.FrameType.SPECIAL_DELAY

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(DelayFrame, other)
            return self.offset == other_frame.offset
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """DelayFrame debug representation.

        >>> DelayFrame(250)
        DelayFrame(250)
        """
        return "{}({})".format(type(self).__name__, self.offset)


class LogTriggerFrame(Frame):
    """Timestamp of when a trigger occurred.

    This frame is generated on input sessions when a rising edge is detected on
    an external connection.

    .. note:: This requires using
       :any:`nixnet._session.base.SessionBase.connect_terminals` to connect an
       external connection to the internal ``LogTrigger`` terminal.

    Attributes:
        timestamp(int): Absolute time that the trigger occurred.
    """

    __slots__ = [
        "timestamp"]

    def __init__(self, timestamp):
        # type: (int) -> None
        self.timestamp = timestamp

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame.

        >>> raw = RawFrame(5, 0, constants.FrameType.SPECIAL_LOG_TRIGGER, 0, 0, b'')
        >>> LogTriggerFrame.from_raw(raw)
        LogTriggerFrame(0x5)
        """
        return LogTriggerFrame(frame.timestamp)

    def to_raw(self):
        """Convert to RawFrame.

        >>> LogTriggerFrame(250).to_raw()
        RawFrame(timestamp=0xfa, identifier=0x0, type=FrameType.SPECIAL_LOG_TRIGGER)
        """
        identifier = 0
        flags = 0
        info = 0
        payload = b''
        return RawFrame(self.timestamp, identifier, self.type, flags, info, payload)

    @property
    def type(self):
        return constants.FrameType.SPECIAL_LOG_TRIGGER

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(LogTriggerFrame, other)
            return self.timestamp == other_frame.timestamp
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """LogTriggerFrame debug representation.

        >>> LogTriggerFrame(250)
        LogTriggerFrame(0xfa)
        """
        return "{}(0x{:x})".format(type(self).__name__, self.timestamp)


class StartTriggerFrame(Frame):
    """Timestamp of :any:`nixnet.session.FrameInStreamSession` start.

    .. note:: This requires enabling
       :any:`nixnet._session.intf.Interface.start_trig_to_in_strm`.

    Attributes:
        timestamp(int): Absolute time that the trigger occurred.
    """

    __slots__ = [
        "timestamp"]

    def __init__(self, timestamp):
        # type: (int) -> None
        self.timestamp = timestamp

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame.

        >>> raw = RawFrame(5, 0, constants.FrameType.SPECIAL_START_TRIGGER, 0, 0, b'')
        >>> StartTriggerFrame.from_raw(raw)
        StartTriggerFrame(0x5)
        """
        return StartTriggerFrame(frame.timestamp)

    def to_raw(self):
        """Convert to RawFrame.

        >>> StartTriggerFrame(250).to_raw()
        RawFrame(timestamp=0xfa, identifier=0x0, type=FrameType.SPECIAL_START_TRIGGER)
        """
        identifier = 0
        flags = 0
        info = 0
        payload = b''
        return RawFrame(self.timestamp, identifier, self.type, flags, info, payload)

    @property
    def type(self):
        return constants.FrameType.SPECIAL_START_TRIGGER

    def __eq__(self, other):
        if isinstance(other, self.__class__):
            other_frame = typing.cast(StartTriggerFrame, other)
            return self.timestamp == other_frame.timestamp
        else:
            return NotImplemented

    def __repr__(self):
        # type: () -> typing.Text
        """StartTriggerFrame debug representation.

        >>> StartTriggerFrame(250)
        StartTriggerFrame(0xfa)
        """
        return "{}(0x{:x})".format(type(self).__name__, self.timestamp)


class XnetFrame(FrameFactory):
    """Create `Frame` based on `RawFrame` content."""

    __slots__ = ()

    @classmethod
    def from_raw(cls, frame):
        """Convert from RawFrame."""
        frame_type = {
            constants.FrameType.CAN_DATA: CanFrame,
            constants.FrameType.CAN20_DATA: CanFrame,
            constants.FrameType.CANFD_DATA: CanFrame,
            constants.FrameType.CANFDBRS_DATA: CanFrame,
            constants.FrameType.CAN_REMOTE: CanFrame,
            constants.FrameType.CAN_BUS_ERROR: CanBusErrorFrame,
            constants.FrameType.LIN_DATA: LinFrame,
            constants.FrameType.SPECIAL_DELAY: DelayFrame,
            constants.FrameType.SPECIAL_LOG_TRIGGER: LogTriggerFrame,
            constants.FrameType.SPECIAL_START_TRIGGER: StartTriggerFrame,
        }.get(frame.type)
        if frame_type is None:
            raise NotImplementedError("Unsupported frame type", frame.type)
        return frame_type.from_raw(frame)
