
from enum import IntEnum, IntFlag
from functools import total_ordering
from typing import TYPE_CHECKING, Any, Iterable, NewType, Optional, Union

from ._ffi import alsa, ffi
from .address import Address, AddressType
from .exceptions import StateError
from .util import _ensure_4bit, _ensure_7bit

if TYPE_CHECKING:
    from .port import Port
    from .queue import Queue


@total_ordering
class RealTime:
    """Event time in seconds and nanoseconds.

    Used for precise timing of events.

    :param seconds: time in seconds
    :param nanoseconds: nanoseconds

    :ivar seconds: number of seconds
    :ivar nanoseconds: number of nanoseconds
    """
    __slots__ = ('seconds', 'nanoseconds')

    seconds: int
    nanoseconds: int

    def __init__(self, seconds: Union[float, int, str, 'RealTime'], nanoseconds: int = 0):
        if isinstance(seconds, RealTime):
            self.seconds = seconds.seconds
            self.nanoseconds = seconds.nanoseconds + nanoseconds
        else:
            if isinstance(seconds, str):
                if "." in seconds:
                    seconds = float(seconds)
                else:
                    seconds = int(seconds)
            if isinstance(seconds, float):
                self.seconds = int(seconds)
                self.nanoseconds = int(nanoseconds + 1000000000 * (seconds - self.seconds))
            else:
                self.seconds = seconds
                self.nanoseconds = nanoseconds
        if self.nanoseconds >= 1000000000:
            self.seconds += self.nanoseconds // 1000000000
            self.nanoseconds = self.nanoseconds % 1000000000
        if self.seconds < 0 or self.nanoseconds < 0:
            raise ValueError("Negative RealTime is not allowed")

    def __repr__(self):
        return f"RealTime(seconds={self.seconds}, nanoseconds={self.nanoseconds})"

    def __str__(self):
        return f"{self.seconds}.{self.nanoseconds:09d}"

    def __int__(self):
        return self.seconds

    def __float__(self):
        return float(self.seconds) + self.nanoseconds / 1000000000

    def __eq__(self, other):
        if not isinstance(other, RealTime):
            other = RealTime(other)
        return (self.seconds, self.nanoseconds) == (other.seconds, other.nanoseconds)

    def __lt__(self, other):
        if not isinstance(other, RealTime):
            other = RealTime(other)
        return (self.seconds, self.nanoseconds) < (other.seconds, other.nanoseconds)


class EventType(IntEnum):
    """Event type values."""
    SYSTEM = alsa.SND_SEQ_EVENT_SYSTEM
    RESULT = alsa.SND_SEQ_EVENT_RESULT
    NOTE = alsa.SND_SEQ_EVENT_NOTE
    NOTEON = alsa.SND_SEQ_EVENT_NOTEON
    NOTEOFF = alsa.SND_SEQ_EVENT_NOTEOFF
    KEYPRESS = alsa.SND_SEQ_EVENT_KEYPRESS
    CONTROLLER = alsa.SND_SEQ_EVENT_CONTROLLER
    PGMCHANGE = alsa.SND_SEQ_EVENT_PGMCHANGE
    CHANPRESS = alsa.SND_SEQ_EVENT_CHANPRESS
    PITCHBEND = alsa.SND_SEQ_EVENT_PITCHBEND
    CONTROL14 = alsa.SND_SEQ_EVENT_CONTROL14
    NONREGPARAM = alsa.SND_SEQ_EVENT_NONREGPARAM
    REGPARAM = alsa.SND_SEQ_EVENT_REGPARAM
    SONGPOS = alsa.SND_SEQ_EVENT_SONGPOS
    SONGSEL = alsa.SND_SEQ_EVENT_SONGSEL
    QFRAME = alsa.SND_SEQ_EVENT_QFRAME
    TIMESIGN = alsa.SND_SEQ_EVENT_TIMESIGN
    KEYSIGN = alsa.SND_SEQ_EVENT_KEYSIGN
    START = alsa.SND_SEQ_EVENT_START
    CONTINUE = alsa.SND_SEQ_EVENT_CONTINUE
    STOP = alsa.SND_SEQ_EVENT_STOP
    SETPOS_TICK = alsa.SND_SEQ_EVENT_SETPOS_TICK
    SETPOS_TIME = alsa.SND_SEQ_EVENT_SETPOS_TIME
    TEMPO = alsa.SND_SEQ_EVENT_TEMPO
    CLOCK = alsa.SND_SEQ_EVENT_CLOCK
    TICK = alsa.SND_SEQ_EVENT_TICK
    QUEUE_SKEW = alsa.SND_SEQ_EVENT_QUEUE_SKEW
    SYNC_POS = alsa.SND_SEQ_EVENT_SYNC_POS
    TUNE_REQUEST = alsa.SND_SEQ_EVENT_TUNE_REQUEST
    RESET = alsa.SND_SEQ_EVENT_RESET
    SENSING = alsa.SND_SEQ_EVENT_SENSING
    ECHO = alsa.SND_SEQ_EVENT_ECHO
    OSS = alsa.SND_SEQ_EVENT_OSS
    CLIENT_START = alsa.SND_SEQ_EVENT_CLIENT_START
    CLIENT_EXIT = alsa.SND_SEQ_EVENT_CLIENT_EXIT
    CLIENT_CHANGE = alsa.SND_SEQ_EVENT_CLIENT_CHANGE
    PORT_START = alsa.SND_SEQ_EVENT_PORT_START
    PORT_EXIT = alsa.SND_SEQ_EVENT_PORT_EXIT
    PORT_CHANGE = alsa.SND_SEQ_EVENT_PORT_CHANGE
    PORT_SUBSCRIBED = alsa.SND_SEQ_EVENT_PORT_SUBSCRIBED
    PORT_UNSUBSCRIBED = alsa.SND_SEQ_EVENT_PORT_UNSUBSCRIBED
    USR0 = alsa.SND_SEQ_EVENT_USR0
    USR1 = alsa.SND_SEQ_EVENT_USR1
    USR2 = alsa.SND_SEQ_EVENT_USR2
    USR3 = alsa.SND_SEQ_EVENT_USR3
    USR4 = alsa.SND_SEQ_EVENT_USR4
    USR5 = alsa.SND_SEQ_EVENT_USR5
    USR6 = alsa.SND_SEQ_EVENT_USR6
    USR7 = alsa.SND_SEQ_EVENT_USR7
    USR8 = alsa.SND_SEQ_EVENT_USR8
    USR9 = alsa.SND_SEQ_EVENT_USR9
    SYSEX = alsa.SND_SEQ_EVENT_SYSEX
    BOUNCE = alsa.SND_SEQ_EVENT_BOUNCE
    USR_VAR0 = alsa.SND_SEQ_EVENT_USR_VAR0
    USR_VAR1 = alsa.SND_SEQ_EVENT_USR_VAR1
    USR_VAR2 = alsa.SND_SEQ_EVENT_USR_VAR2
    USR_VAR3 = alsa.SND_SEQ_EVENT_USR_VAR3
    USR_VAR4 = alsa.SND_SEQ_EVENT_USR_VAR4
    NONE = alsa.SND_SEQ_EVENT_NONE

    @classmethod
    def _missing_(cls, value):
        obj = int.__new__(cls, value)
        obj._value_ = value
        obj._name_ = f"UNKNOWN_{value}"
        return obj


MIDI_BYTES_EVENTS = [
    EventType.NOTEOFF,
    EventType.NOTEON,
    EventType.KEYPRESS,
    EventType.CONTROLLER,
    EventType.PGMCHANGE,
    EventType.CHANPRESS,
    EventType.PITCHBEND,
    EventType.SYSEX,
    EventType.QFRAME,
    EventType.SONGPOS,
    EventType.SONGSEL,
    EventType.TUNE_REQUEST,
    EventType.CLOCK,
    EventType.START,
    EventType.CONTINUE,
    EventType.STOP,
    EventType.SENSING,
    EventType.RESET,
    EventType.CONTROL14,
    EventType.NONREGPARAM,
    EventType.REGPARAM,
    ]


class EventFlags(IntFlag):
    """Event flags."""
    TIME_STAMP_TICK = alsa.SND_SEQ_TIME_STAMP_TICK
    TIME_STAMP_REAL = alsa.SND_SEQ_TIME_STAMP_REAL
    TIME_STAMP_MASK = alsa.SND_SEQ_TIME_STAMP_MASK

    TIME_MODE_ABS = alsa.SND_SEQ_TIME_MODE_ABS
    TIME_MODE_REL = alsa.SND_SEQ_TIME_MODE_REL
    TIME_MODE_MASK = alsa.SND_SEQ_TIME_MODE_MASK

    EVENT_LENGTH_FIXED = alsa.SND_SEQ_EVENT_LENGTH_FIXED
    EVENT_LENGTH_VARIABLE = alsa.SND_SEQ_EVENT_LENGTH_VARIABLE
    EVENT_LENGTH_VARUSR = alsa.SND_SEQ_EVENT_LENGTH_VARUSR
    EVENT_LENGTH_MASK = alsa.SND_SEQ_EVENT_LENGTH_MASK

    PRIORITY_NORMAL = alsa.SND_SEQ_PRIORITY_NORMAL
    PRIORITY_HIGH = alsa.SND_SEQ_PRIORITY_HIGH
    PRIORITY_MASK = alsa.SND_SEQ_PRIORITY_MASK


_snd_seq_event_t = NewType("_snd_seq_event_t", Any)


class Event:
    """Base class for ALSA sequencer events.

    Normally not instantiated directly – specialized subclasses will be used.

    Most attributes are optional – when not set some defaults will be used
    when passing this to ALSA. :attr:`time` and :attr:`tick` cannot be ever be
    both set.

    Contains python representation of ALSA :alsa:`snd_seq_event_t`.

    :param event: event type
    :param flags: event flags
    :param tag: event tag
    :param queue_id: queue id for the event. `None` for direct dispatch.
    :param time: event time in seconds and nanoseconds
    :param tick: event time in MIDI ticks
    :param source: event source address
    :param dest: event destination address
    :param relative: When true then :attr:`tick` or :attr:`tick` are relative
    :param raw_data: Unparsed raw data part of the ALSA event. Note: this is not MIDI data.

    :ivar event: event type
    :ivar flags: event flags
    :ivar tag: event tag
    :ivar queue_id: queue id for the event. `None` for direct dispatch.
    :ivar time: event time in seconds and nanoseconds
    :ivar tick: event time in MIDI ticks
    :ivar source: event source address
    :ivar dest: event destination address
    :ivar relative: When true then :attr:`tick` or :attr:`tick` are relative
    :ivar raw_data: Unparsed raw data part of the ALSA event. Note: this is not MIDI data.
    """
    _specialized = {}
    type = None

    flags: Optional[EventFlags]
    tag: int
    queue_id: Optional[int]
    time: Optional[RealTime]
    tick: Optional[int]
    source: Optional[Address]
    dest: Optional[Address]
    relative: Optional[bool]
    raw_data: Optional[bytes]

    def __init__(self,
                 type: Optional[EventType] = None,
                 *,
                 flags: Optional[Union[EventFlags, int]] = 0,
                 tag: int = 0,
                 queue_id: Optional[int] = None,
                 time: Optional[Union[RealTime, float, int]] = None,
                 tick: Optional[int] = None,
                 source: Optional[AddressType] = None,
                 dest: Optional[AddressType] = None,
                 relative: Optional[bool] = None,
                 raw_data: bytes = None,
                 ):

        if type is None and self.__class__.type is not None:
            self.type = self.__class__.type
        else:
            self.type = type
        if flags is not None:
            self.flags = EventFlags(flags)
        else:
            flags = None
        self.tag = tag

        self.queue_id = queue_id

        if time is not None and tick is not None:
            raise ValueError("Either 'time' or 'tick' may be set, not both")

        if time is not None:
            self.time = RealTime(time)
        else:
            self.time = None
        self.tick = tick

        self.relative = relative

        if source is not None:
            self.source = Address(*source)
        else:
            self.source = None
        if dest is not None:
            self.dest = Address(*dest)
        else:
            self.dest = None

        self.raw_data = raw_data

    def __repr__(self):
        if self.type is None:
            type_s = " unknown"
        elif self.type == self.__class__.type:
            type_s = ""
        else:
            type_s = " " + self.type.name
        return f"<{self.__class__.__name__}{type_s}>"

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        flags = a_event.flags
        if (flags & EventFlags.TIME_STAMP_MASK) == EventFlags.TIME_STAMP_REAL:
            ev_time = RealTime(a_event.time.time.tv_sec, a_event.time.time.tv_nsec)
        else:
            ev_time = None
        if (flags & EventFlags.TIME_STAMP_MASK) == EventFlags.TIME_STAMP_TICK:
            ev_tick = a_event.time.tick
        else:
            ev_tick = None
        relative = (flags & EventFlags.TIME_MODE_MASK) == EventFlags.TIME_MODE_REL
        raw_data = bytes(ffi.buffer(ffi.addressof(a_event.data)))
        if cls.type is None:
            kwargs["type"] = EventType(a_event.type)
        return cls(flags=flags,
                   tag=a_event.tag,
                   queue_id=a_event.queue,
                   time=ev_time,
                   tick=ev_tick,
                   relative=relative,
                   source=Address(a_event.source.client, a_event.source.port),
                   dest=Address(a_event.dest.client, a_event.dest.port),
                   raw_data=raw_data,
                   **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, *,
                 queue: Union['Queue', int] = None,
                 port: Union['Port', int] = None,
                 dest: AddressType = None
                 ) -> _snd_seq_event_t:
        if self.type is not None:
            a_event.type = int(self.type)
        if self.flags is not None:
            flags = self.flags
        else:
            flags = 0
        a_event.tag = self.tag
        if queue is not None:
            if isinstance(queue, int):
                a_event.queue = queue
            else:
                a_event.queue = queue.queue_id
        elif self.queue_id is not None:
            a_event.queue = self.queue_id
        else:
            a_event.queue = alsa.SND_SEQ_QUEUE_DIRECT
        assert self.time is None or self.tick is None
        if self.time is not None:
            a_event.time.time.tv_sec = self.time.seconds
            a_event.time.time.tv_nsec = self.time.nanoseconds
            flags = flags & ~EventFlags.TIME_STAMP_MASK | EventFlags.TIME_STAMP_REAL
        if self.tick is not None:
            a_event.time.tick = self.tick
            flags = flags & ~EventFlags.TIME_STAMP_MASK | EventFlags.TIME_STAMP_TICK
        if self.relative is not None:
            rel = EventFlags.TIME_MODE_REL if self.relative else EventFlags.TIME_MODE_ABS
            flags = flags & ~EventFlags.TIME_MODE_MASK | rel
        if port is not None:
            if isinstance(port, int):
                a_event.source.port = port
            else:
                a_event.source.port = port.port_id
        elif self.source is not None:
            a_event.source.client = self.source.client_id
            a_event.source.port = self.source.port_id
        if dest is not None:
            client_id, port_id = Address(dest)
            a_event.dest.client = client_id
            a_event.dest.port = port_id
        elif self.dest is not None:
            a_event.dest.client = self.dest.client_id
            a_event.dest.port = self.dest.port_id
        else:
            a_event.dest.client = alsa.SND_SEQ_ADDRESS_SUBSCRIBERS

        if self.raw_data is not None:
            # set raw data
            # may be overwritten by structured data in a specialized class
            buf = ffi.buffer(ffi.addressof(a_event.data))
            buf[:min(len(self.raw_data), 12)] = self.raw_data[:12]

        a_event.flags = int(flags)

        return a_event

    def length(self):
        """Calculates the (encoded) byte-stream size of the event.

        Wraps :alsa:`snd_seq_event_length`."""
        alsa_event: _snd_seq_event_t = ffi.new("snd_seq_event_t *")
        self._to_alsa(alsa_event)
        return alsa.snd_seq_event_length(alsa_event)


class MidiBytesEvent(Event):
    """Pseudo ALSA event to hold MIDI messages as byte sequences.

    Used to interface code that operates on MIDI bytes.

    :param midi_bytes: the MIDI message
    :param kwargs: :class:`Event` constructor parameters

    :ivar midi_bytes: the MIDI message
    """
    midi_bytes: bytes

    def __init__(self,
                 midi_bytes: Union[bytes, Iterable[int]],
                 **kwargs):
        super().__init__(**kwargs)
        self.midi_bytes = bytes(midi_bytes)

    def __repr__(self):
        length = self.midi_bytes
        if len(length) < 32:
            hex_bytes = " ".join(f"{b:02X}" for b in self.midi_bytes)
        else:
            hex_bytes = " ".join(f"{b:02X}" for b in self.midi_bytes[:2])
            hex_bytes += " .. <{length - 4} more> .. "
            hex_bytes = " ".join(f"{b:02X}" for b in self.midi_bytes[-2:])
        return (f"<{self.__class__.__name__} {hex_bytes}>")


def _specialized_event_class(event_type):
    def decorator(cls):
        cls.type = event_type
        Event._specialized[event_type.value] = cls
        if "\n" not in cls.__doc__:
            base_doc = cls.__base__.__doc__
            cls.__doc__ += "\n" + base_doc.split("\n", 1)[1]
        return cls
    return decorator


class ResultEventBase(Event):
    """Base class for :class:`SystemEvent` and :class:`ResultEvent`.

    :param event: event
    :param result: result

    :ivar event: event
    :ivar result: result
    """
    event: int
    result: int

    def __init__(self,
                 event: int,
                 result: int,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.event = event
        self.result = result

    def __repr__(self):
        return (f"<{self.__class__.__name__} event={self.event} result={self.result}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["event"] = a_event.data.result.event
        kwargs["result"] = a_event.data.result.result
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.result.event = self.event
        a_event.data.result.result = self.result
        return a_event


class NoteEventBase(Event):
    """Base class for :class:`NoteEvent`, :class:`NoteOnEvent` and :class:`NoteOffEvent`.

    :param note: MIDI note number
    :param channel: MIDI channel
    :param velocity: note velocity

    :ivar note: MIDI note number
    :ivar channel: MIDI channel
    :ivar velocity: note velocity
    """
    note: int
    channel: int
    velocity: int

    def __init__(self,
                 note: int,
                 channel: int = 0,
                 velocity: int = 127,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.channel = _ensure_4bit(channel)
        self.note = _ensure_7bit(note)
        self.velocity = _ensure_7bit(velocity)

    def __repr__(self):
        return (f"<{self.__class__.__name__} channel={self.channel} note={self.note}"
                f" velocity={self.velocity}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["note"] = a_event.data.note.note
        kwargs["channel"] = a_event.data.note.channel
        kwargs["velocity"] = a_event.data.note.velocity
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.note.note = self.note
        a_event.data.note.channel = self.channel
        a_event.data.note.velocity = self.velocity
        return a_event


class ControlChangeEventBase(Event):
    """Base class for various control/parameter change events.

    :param channel: MIDI channel
    :param param: parameter number
    :param value: new value

    :ivar channel: MIDI channel
    :ivar param: parameter number
    :ivar value: new value
    """
    channel: int
    param: int
    value: int

    def __init__(self,
                 channel: int,
                 param: int,
                 value: int,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.channel = channel
        self.param = param
        self.value = value

    def __repr__(self):
        return (f"<{self.__class__.__name__} channel={self.channel}"
                f" param={self.param} value={self.value}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["channel"] = a_event.data.control.channel
        kwargs["param"] = a_event.data.control.param
        kwargs["value"] = a_event.data.control.value
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.control.channel = self.channel
        a_event.data.control.param = self.param
        a_event.data.control.value = self.value
        return a_event


class ParamChangeEventBase(Event):
    """Base class for various parameter change events.

    :param channel: MIDI channel
    :param value: new value

    :ivar channel: MIDI channel
    :ivar value: new value
    """

    channel: int
    value: int

    def __init__(self,
                 channel: int,
                 value: int,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.channel = channel
        self.value = value

    def __repr__(self):
        return (f"<{self.__class__.__name__} channel={self.channel} value={self.value}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["channel"] = a_event.data.control.channel
        kwargs["value"] = a_event.data.control.value
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.control.channel = self.channel
        a_event.data.control.value = self.value
        return a_event


class QueueControlEventBase(Event):
    """Base class for MIDI clock events and queue control events.

    :param control_queue: affected queue

    :ivar control_queue: affected queue id
    """
    control_queue: Optional[int]

    def __init__(self,
                 control_queue: Optional[Union[int, 'Queue']] = None,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        if isinstance(control_queue, int) or control_queue is None:
            self.control_queue = control_queue
        elif control_queue is not None:
            self.control_queue = control_queue.queue_id
        else:
            raise StateError("Queue already closed")

    def __repr__(self):
        if self.control_queue is not None:
            return (f"<{self.__class__.__name__} queue={self.control_queue}>")
        else:
            return (f"<{self.__class__.__name__}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["control_queue"] = a_event.data.queue.queue
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        queue_id = self.control_queue
        if queue_id is not None:
            a_event.data.queue.queue = queue_id
        return a_event


class AddressEventBase(Event):
    """Base class for events containing single sequencer address.

    :param addr: sequencer address

    :ivar addr: sequencer address
    """
    addr: Address

    def __init__(self,
                 addr: AddressType,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.addr = Address(addr)

    def __repr__(self):
        return (f"<{self.__class__.__name__} {self.addr}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["addr"] = Address(a_event.data.addr.client, a_event.data.addr.port)
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.addr.client = self.addr.client_id
        a_event.data.addr.port = self.addr.port_id
        return a_event


class ConnectEventBase(Event):
    """Base class for sequencer connect and disconnect events.

    :param sender: sender address
    :param dest: destination address

    :ivar sender: sender address
    :ivar dest: destination address
    """

    connect_sender: Address
    connect_dest: Address

    def __init__(self,
                 connect_sender: AddressType,
                 connect_dest: AddressType,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.connect_sender = Address(connect_sender)
        self.connect_dest = Address(connect_dest)

    def __repr__(self):
        return (f"<{self.__class__.__name__} from {self.connect_sender} to {self.connect_dest}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["connect_sender"] = Address(a_event.data.connect.sender.client,
                                           a_event.data.connect.sender.port)
        kwargs["connect_dest"] = Address(a_event.data.connect.dest.client,
                                         a_event.data.connect.dest.port)
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.connect.sender.client = self.connect_sender.client_id
        a_event.data.connect.sender.port = self.connect_sender.port_id
        a_event.data.connect.dest.client = self.connect_dest.client_id
        a_event.data.connect.dest.port = self.connect_dest.port_id
        return a_event


class ExternalDataEventBase(Event):
    """Base class for events containing external data.

    :param data: the data

    :ivar data: the data
    """

    data: bytes

    def __init__(self,
                 data: bytes,
                 **kwargs):
        assert self.type is not None
        super().__init__(self.type, **kwargs)
        self.data = bytes(data)

    def __repr__(self):
        if len(self.data) < 32:
            return (f"<{self.__class__.__name__} data={self.data!r}>")
        else:
            return (f"<{self.__class__.__name__} data=<{len(self.data)} bytes>>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        data = bytes(ffi.buffer(a_event.data.ext.ptr, a_event.data.ext.len))
        kwargs["data"] = data
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.flags |= EventFlags.EVENT_LENGTH_VARIABLE
        a_event.data.ext.len = len(self.data)
        a_event.data.ext.ptr = ffi.from_buffer(self.data)
        return a_event


@_specialized_event_class(EventType.SYSTEM)
class SystemEvent(ResultEventBase):
    """System status event."""
    pass


@_specialized_event_class(EventType.RESULT)
class ResultEvent(ResultEventBase):
    """Returned result status event."""
    pass


@_specialized_event_class(EventType.NOTE)
class NoteEvent(NoteEventBase):
    """Note event – a Note On followed by a Note Off.

    :param note: MIDI note number
    :param channel: MIDI channel
    :param velocity: note velocity
    :param duration: note duration
    :param off_velocity: Note Off velocity

    :ivar note: MIDI note number
    :ivar channel: MIDI channel
    :ivar velocity: note velocity
    :ivar duration: note duration
    :ivar off_velocity: Note Off velocity
    """

    duration: int
    off_velocity: int

    def __init__(self,
                 note: int,
                 channel: int = 0,
                 velocity: int = 127,
                 duration: int = 0,
                 off_velocity: int = 0,
                 **kwargs):
        super().__init__(note, channel, velocity, **kwargs)
        self.off_velocity = _ensure_7bit(off_velocity)
        self.duration = duration

    def __repr__(self):
        return (f"<{self.__class__.__name__} channel={self.channel} note={self.note}"
                f" velocity={self.velocity} duration={self.duration}"
                f" off_velocity={self.off_velocity}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["off_velocity"] = a_event.data.note.off_velocity
        kwargs["duration"] = a_event.data.note.duration
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.note.off_velocity = self.off_velocity
        a_event.data.note.duration = self.duration
        return a_event


@_specialized_event_class(EventType.NOTEON)
class NoteOnEvent(NoteEventBase):
    """Note On event."""


@_specialized_event_class(EventType.NOTEOFF)
class NoteOffEvent(NoteEventBase):
    """Note Off event."""


@_specialized_event_class(EventType.KEYPRESS)
class KeyPressureEvent(NoteEventBase):
    """Key pressure changed (aftertouch) event."""


@_specialized_event_class(EventType.CONTROLLER)
class ControlChangeEvent(ControlChangeEventBase):
    """Control Change event."""


@_specialized_event_class(EventType.PGMCHANGE)
class ProgramChangeEvent(ParamChangeEventBase):
    """Program Change event."""


@_specialized_event_class(EventType.CHANPRESS)
class ChannelPressureEvent(ParamChangeEventBase):
    """Channel Pressure event."""


@_specialized_event_class(EventType.PITCHBEND)
class PitchBendEvent(ParamChangeEventBase):
    """Pitch Bend event."""


@_specialized_event_class(EventType.CONTROL14)
class Control14BitChangeEvent(ControlChangeEventBase):
    """14-bit Control Change event."""


@_specialized_event_class(EventType.NONREGPARAM)
class NonRegisteredParameterChangeEvent(ControlChangeEventBase):
    """Non Registered Parameter Change event."""


@_specialized_event_class(EventType.REGPARAM)
class RegisteredParameterChangeEvent(ControlChangeEventBase):
    """Registered Parameter Change event."""


@_specialized_event_class(EventType.SONGPOS)
class SongPositionPointerEvent(ParamChangeEventBase):
    """Song Position Pointer event."""


@_specialized_event_class(EventType.SONGSEL)
class SongSelectEvent(ParamChangeEventBase):
    """Song Select event."""


# how is it encoded into snd_seq_ev_ctrl_t?
@_specialized_event_class(EventType.TIMESIGN)
class TimeSignatureEvent(ParamChangeEventBase):
    """Time Signature event."""


# how is it encoded into snd_seq_ev_ctrl_t?
@_specialized_event_class(EventType.KEYSIGN)
class KeySignatureEvent(ParamChangeEventBase):
    """Key Signature event."""


@_specialized_event_class(EventType.START)
class StartEvent(QueueControlEventBase):
    """Start event."""


@_specialized_event_class(EventType.CONTINUE)
class ContinueEvent(QueueControlEventBase):
    """Continue event."""


@_specialized_event_class(EventType.STOP)
class StopEvent(QueueControlEventBase):
    """Stop event."""


@_specialized_event_class(EventType.SETPOS_TICK)
class SetQueuePositionTickEvent(QueueControlEventBase):
    """Set Queue Position Tick event.

    :param control_queue: affected queue
    :param position: new position

    :ivar control_queue: affected queue id
    :ivar position: new position
    """

    position: int

    def __init__(self,
                 position: int,
                 *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.position = position

    def __repr__(self):
        if self.control_queue is not None:
            return (f"<{self.__class__.__name__} queue={self.control_queue}"
                    f" position={self.position}>")
        else:
            return (f"<{self.__class__.__name__} position={self.position}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["position"] = a_event.data.queue.param.time.tick
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.queue.param.time.tick = self.position
        return a_event


@_specialized_event_class(EventType.SETPOS_TIME)
class SetQueuePositionTimeEvent(QueueControlEventBase):
    """Set Queue Position Time event.

    :param control_queue: affected queue
    :param position: new position

    :ivar control_queue: affected queue id
    :ivar position: new position
    """

    position: RealTime

    def __init__(self,
                 position: Union[RealTime, int, float],
                 *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.position = RealTime(position)

    def __repr__(self):
        if self.control_queue is not None:
            return (f"<{self.__class__.__name__} queue={self.control_queue}"
                    f" position={self.position}>")
        else:
            return (f"<{self.__class__.__name__} position={self.position}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["position"] = RealTime(a_event.data.queue.param.time.time.tv_sec,
                                      a_event.data.queue.param.time.time.tv_nsec)
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.queue.param.time.time.tv_sec = self.position.seconds
        a_event.data.queue.param.time.time.tv_nsec = self.position.nanoseconds
        return a_event


@_specialized_event_class(EventType.TEMPO)
class SetQueueTempoEvent(QueueControlEventBase):
    """Set Queue Position Time event.

    :param control_queue: affected queue
    :param midi_tempo: MIDI tempo (microseconds per quarter note)
    :param bpm: beats per minute as an alternative to `midi_tempo`

    :ivar control_queue: affected queue id
    :ivar midi_tempo: MIDI tempo (microseconds per quarter note)
    """

    def __init__(self,
                 midi_tempo: int = None,
                 *,
                 bpm: float = None,
                 **kwargs):
        if midi_tempo is not None and bpm is not None:
            raise ValueError("Either tempo or must be provided, not both.")
        if midi_tempo is not None:
            self.midi_tempo = midi_tempo
        elif bpm is not None:
            self.midi_tempo = round(60000000.0 / bpm)
        else:
            raise ValueError("Either tempo or must be provided.")
        super().__init__(**kwargs)

    @property
    def bpm(self):
        """Approximate beats per minute value for the selected tempo."""
        return 60000000.0 / self.midi_tempo

    def __repr__(self):
        tempo = self.midi_tempo
        bpm = self.bpm
        if self.control_queue is not None:
            return (f"<{self.__class__.__name__} queue={self.control_queue}"
                    f" tempo={tempo} ({bpm:0.1f} bpm)>")
        else:
            return (f"<{self.__class__.__name__} tempo={tempo} ({bpm} bpm)>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["midi_tempo"] = a_event.data.queue.param.value
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.queue.param.value = self.midi_tempo
        return a_event


@_specialized_event_class(EventType.CLOCK)
class ClockEvent(QueueControlEventBase):
    """MIDI Clock event."""
    pass


@_specialized_event_class(EventType.TICK)
class TickEvent(QueueControlEventBase):
    """MIDI Tick event."""
    pass


@_specialized_event_class(EventType.QUEUE_SKEW)
class QueueSkewEvent(QueueControlEventBase):
    """Queue Skew event.

    :param control_queue: affected queue
    :param value: skew value
    :param base: skew base

    :ivar control_queue: affected queue id
    :ivar value: skew value
    :ivar base: skew base
    """

    value: int
    base: int

    def __init__(self,
                 value: int,
                 base: int,
                 *args, **kwargs):
        self.value = value
        self.base = base
        super().__init__(*args, **kwargs)

    def __repr__(self):
        if self.control_queue is not None:
            return (f"<{self.__class__.__name__} queue={self.control_queue}"
                    f" value={self.value} base={self.base}>")
        else:
            return (f"<{self.__class__.__name__} value={self.value} base={self.base}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["value"] = a_event.data.queue.param.skew.value
        kwargs["base"] = a_event.data.queue.param.skew.base
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.queue.param.skew.value = self.value
        a_event.data.queue.param.skew.base = self.base
        return a_event


@_specialized_event_class(EventType.SYNC_POS)
class SyncPositionChangedEvent(QueueControlEventBase):
    """Sync Position Changed event.

    :param control_queue: affected queue
    :param position: new position

    :ivar control_queue: affected queue id
    :ivar position: new position
    """

    position: int

    def __init__(self,
                 position: int,
                 *args, **kwargs):
        self.position = position
        super().__init__(*args, **kwargs)

    def __repr__(self):
        if self.control_queue is not None:
            return (f"<{self.__class__.__name__} queue={self.control_queue}"
                    f" position={self.position}>")
        else:
            return (f"<{self.__class__.__name__} position={self.position}>")

    @classmethod
    def _from_alsa(cls, a_event: _snd_seq_event_t, **kwargs):
        kwargs["position"] = a_event.data.queue.param.position
        return super()._from_alsa(a_event, **kwargs)

    def _to_alsa(self, a_event: _snd_seq_event_t, **kwargs):
        super()._to_alsa(a_event, **kwargs)
        a_event.data.queue.param.position = self.position
        return a_event


@_specialized_event_class(EventType.TUNE_REQUEST)
class TuneRequestEvent(Event):
    """Tune request event."""
    pass


@_specialized_event_class(EventType.RESET)
class ResetEvent(Event):
    """Reset event."""
    pass


@_specialized_event_class(EventType.SENSING)
class ActiveSensingEvent(Event):
    """Active Sensing event."""
    pass


@_specialized_event_class(EventType.ECHO)
class EchoEvent(Event):
    """Echo event."""
    def __repr__(self):
        return (f"<{self.__class__.__name__} data={self.raw_data!r}>")


@_specialized_event_class(EventType.OSS)
class OSSEvent(Event):
    """OSS emulation event."""
    def __repr__(self):
        return (f"<{self.__class__.__name__} data={self.raw_data!r}>")


@_specialized_event_class(EventType.CLIENT_START)
class ClientStartEvent(AddressEventBase):
    """Client start event."""


@_specialized_event_class(EventType.CLIENT_EXIT)
class ClientExitEvent(AddressEventBase):
    """Client exit event."""


@_specialized_event_class(EventType.CLIENT_CHANGE)
class ClientChangeEvent(AddressEventBase):
    """Client change event."""


@_specialized_event_class(EventType.PORT_START)
class PortStartEvent(AddressEventBase):
    """Port start event."""


@_specialized_event_class(EventType.PORT_EXIT)
class PortExitEvent(AddressEventBase):
    """Port exit event."""


@_specialized_event_class(EventType.PORT_CHANGE)
class PortChangeEvent(AddressEventBase):
    """Port change event."""


@_specialized_event_class(EventType.PORT_SUBSCRIBED)
class PortSubscribedEvent(ConnectEventBase):
    """Port subscribed event."""


@_specialized_event_class(EventType.PORT_UNSUBSCRIBED)
class PortUnsubscribedEvent(ConnectEventBase):
    """Port unsubscribed event."""


@_specialized_event_class(EventType.SYSEX)
class SysExEvent(ExternalDataEventBase):
    """System Exclusive message event."""


@_specialized_event_class(EventType.BOUNCE)
class BounceEvent(ExternalDataEventBase):
    """Error event."""


@_specialized_event_class(EventType.USR_VAR0)
class UserVar0Event(ExternalDataEventBase):
    """USR_VAR0 event."""


@_specialized_event_class(EventType.USR_VAR1)
class UserVar1Event(ExternalDataEventBase):
    """USR_VAR1 event."""


@_specialized_event_class(EventType.USR_VAR2)
class UserVar2Event(ExternalDataEventBase):
    """USR_VAR2 event."""


@_specialized_event_class(EventType.USR_VAR3)
class UserVar3Event(ExternalDataEventBase):
    """USR_VAR3 event."""
    pass


@_specialized_event_class(EventType.USR_VAR4)
class UserVar4Event(ExternalDataEventBase):
    """USR_VAR4 event."""
    pass


__all__ = [
        "RealTime",
        "EventType", "EventFlags", "Event", "MidiBytesEvent",
        "NoteEventBase",
        "MIDI_BYTES_EVENTS",

        "SystemEvent", "ResultEvent", "NoteEvent", "NoteOnEvent", "NoteOffEvent",
        "KeyPressureEvent", "ControlChangeEvent", "ProgramChangeEvent", "ChannelPressureEvent",
        "PitchBendEvent", "Control14BitChangeEvent", "NonRegisteredParameterChangeEvent",
        "RegisteredParameterChangeEvent", "SongPositionPointerEvent", "SongSelectEvent",
        "TimeSignatureEvent", "KeySignatureEvent", "StartEvent", "ContinueEvent", "StopEvent",
        "SetQueuePositionTickEvent", "SetQueuePositionTimeEvent", "SetQueueTempoEvent",
        "ClockEvent", "TickEvent", "QueueSkewEvent", "SyncPositionChangedEvent",
        "TuneRequestEvent", "ResetEvent", "ActiveSensingEvent", "EchoEvent", "OSSEvent",
        "ClientStartEvent", "ClientExitEvent", "ClientChangeEvent", "PortStartEvent",
        "PortExitEvent", "PortChangeEvent", "PortSubscribedEvent", "PortUnsubscribedEvent",
        "SysExEvent", "BounceEvent", "UserVar0Event", "UserVar1Event", "UserVar2Event",
        "UserVar3Event", "UserVar4Event",
        ]
