# Copyright 2021 - 2022, Martijn Braam and the OpenAtem contributors
# SPDX-License-Identifier: LGPL-3.0-only
import colorsys
import datetime
import struct


class Command:
    def get_command(self):
        pass

    def _make_command(self, name, data):
        header = struct.pack('>H 2x 4s', len(data) + 8, name.encode())
        return header + data


class CutCommand(Command):
    """
    Implementation of the `DCut` command.  This initiates a cut transition.
    It is equivalent to pressing the CUT button in the Transition Style
    area of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index):
        """
        :param index: 0-indexed M/E number for the transition
        """
        self.index = index

    def get_command(self):
        data = struct.pack('>B 3x', self.index)
        return self._make_command('DCut', data)


class AutoCommand(Command):
    """
    Implementation of the `DAut` command.  This initiates an Auto transition.
    It is equivalent to pressing the AUTO button in the Transition Style area
    of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index):
        """
        :param index: 0-indexed M/E number for the transition
        """
        self.index = index

    def get_command(self):
        data = struct.pack('>B 3x', self.index)
        return self._make_command('DAut', data)


class ProgramInputCommand(Command):
    """
    Implementation of the `CPgI` command.  This changes the Program source.
    It is equivalent to pressing one of the buttons in the Program arae of the
    UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    ?      unknown
    2      2    u16    Source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, source):
        """
        :param index: 0-indexed M/E number for the program bus
        :param source: Source index to send to the program bus
        """
        self.index = index
        self.source = source

    def get_command(self):
        data = struct.pack('>B x H', self.index, self.source)
        return self._make_command('CPgI', data)


class PreviewInputCommand(Command):
    """
    Implementation of the `CPvI` command.  This changes the Preview source.
    It is equivalent to pressing one of the buttons in the Preview area of
    the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    ?      unknown
    2      2    u16    Source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, source):
        """
        :param index: 0-indexed M/E number for the preview bus
        :param source: Source index to send to the preview bus
        """
        self.index = index
        self.source = source

    def get_command(self):
        data = struct.pack('>B x H', self.index, self.source)
        return self._make_command('CPvI', data)


class AuxSourceCommand(Command):
    """
    Implementation of the `CAuS` command.  This selects the source that will be
    sent to a specific AUX output.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, always 1
    1      1    u8     AUX index
    2      2    u16    Source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, source):
        """
        :param index: 0-indexed AUX output number
        :param source: Source index to send to the aux output
        """
        self.index = index
        self.source = source

    def get_command(self):
        data = struct.pack('>BBH', 1, self.index, self.source)
        return self._make_command('CAuS', data)


class TransitionPositionCommand(Command):
    """
    Implementation of the `CTPs` command.  This sets the position for the T-bar
    control of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    ?      unknown
    2      2    u16    Position [0-10000]
    ====== ==== ====== ===========

    """

    def __init__(self, index, position):
        """
        :param index: 0-indexed M/E number for the T-bar
        :param position: Position for the T-bar [0-10000]
        """
        self.index = index
        self.position = position

    def get_command(self):
        position = self.position
        data = struct.pack('>BxH', self.index, position)
        return self._make_command('CTPs', data)


class TransitionSettingsCommand(Command):
    """
    Implementation of the `CTTp` command.  This sets the transition style for
    a M/E unit to one of Mix, Dip, Wipe, String or DVE.  It is equivalent to
    pressing one of the top-row buttons in the Transition Style area of the
    UI Switcher panel.

    The style argument takes one of the values from the TransitionSettingsField
    constants:

    - TransitionSettingsField.STYLE_MXI
    - TransitionSettingsField.STYLE_DIP
    - TransitionSettingsField.STYLE_WIPE
    - TransitionSettingsField.STYLE_STING
    - TransitionSettingsField.STYLE_DVE

    The next_transition argument is a bit field that sets the state of the
    "Next Transition" buttons, the row with the BKGD button.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, bit0=set style, bit1=set next transition
    1      1    u8     M/E index
    2      1    u8     Style
    3      1    u8     Next transition
    ====== ==== ====== ===========

    ===== =============
    bit   Next transition button
    ===== =============
    0     BKGD
    1     Key 1
    2     Key 2
    3     Key 3
    4     Key 4
    ===== =============

    """

    def __init__(self, index, style=None, next_transition=None):
        """
        :param index: 0-indexed M/E number for the transition settings
        :param style: Transition style
        :param next_transition: Bit field for the next transition active layers
        """

        self.index = index
        self.style = style
        self.next_transition = next_transition

    def get_command(self):
        mask = 0
        if self.style is not None:
            mask |= 0x01
        if self.next_transition is not None:
            mask |= 0x02

        style = 0 if self.style is None else self.style
        next_transition = 0 if self.next_transition is None else self.next_transition
        data = struct.pack('>BBBB', mask, self.index, style, next_transition)
        return self._make_command('CTTp', data)


class TransitionPreviewCommand(Command):
    """
    Implementation of the `CTPr` command.  This sets the state of the
    Transition Preview function of the mixer.  It is equivalent to pressing
    the Prev Trans button in the Transition Style area of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    bool   Preview enabled
    2      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, enabled):
        """
        :param index: 0-indexed M/E number for the the preview bus
        :param enabled: True to enable; False to disable
        """
        self.index = index
        self.enabled = enabled

    def get_command(self):
        data = struct.pack('>B ? 2x', self.index, self.enabled)
        return self._make_command('CTPr', data)


class ColorGeneratorCommand(Command):
    """
    Implementation of the `CClV` command. This sets the color for a color
    generator.  It is equivalent to adjusting the Color Generators settings
    in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Set mask
    1      1    u8     Color generator index
    2      2    u16    Hue [0-3599]
    4      2    u16    Saturation [0-1000]
    6      2    u16    Luma [0-1000]
    ====== ==== ====== ===========

    """

    def __init__(self, index, hue=None, saturation=None, luma=None):
        """
        :param index: Color generator index
        :param hue: Hue for the generator
        :param saturation: Saturation for the generator
        :param luma: Luma for the generator
        """
        self.index = index
        self.hue = hue
        self.luma = luma
        self.saturation = saturation

    @classmethod
    def from_rgb(cls, index, red, green, blue):
        h, l, s = colorsys.rgb_to_hls(red, green, blue)
        return cls(index, hue=h * 359, saturation=s, luma=l)

    def get_command(self):
        mask = 0
        if self.hue is not None:
            mask |= 0x01
        if self.saturation is not None:
            mask |= 0x02
        if self.luma is not None:
            mask |= 0x04

        hue = 0 if self.hue is None else int(self.hue * 10)
        saturation = 0 if self.saturation is None else int(self.saturation * 1000)
        luma = 0 if self.luma is None else int(self.luma * 1000)
        data = struct.pack('>BB 3H', mask, self.index, hue, saturation, luma)
        return self._make_command('CClV', data)


class FadeToBlackCommand(Command):
    """
    Implementation of the `FtbA` command.  This triggers the fade-to-black
    transition.  It is equivalent to pressing FTB in the Fade to Black area
    of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    ?      unknown
    2      1    ?      unknown
    3      1    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index):
        """
        :param index: 0-indexed M/E number for the transition
        """
        self.index = index

    def get_command(self):
        data = struct.pack('>B 3x', self.index)
        return self._make_command('FtbA', data)


class FadeToBlackConfigCommand(Command):
    """
    Implementation of the `FtbC` command. This sets the duration for the
    fade-to-black transition.  It is equivalent to adjusting the Fade to Black
    settings in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, always 1
    1      1    u8     M/E index
    2      1    u8     frames
    3      1    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, frames):
        """
        :param index: 0-indexed M/E number for the transition
        :param frames: Transition duration in frames
        """
        self.index = index
        self.frames = frames

    def get_command(self):
        data = struct.pack('>BBBx', 1, self.index, self.frames)
        return self._make_command('FtbC', data)


class CaptureStillCommand(Command):
    """
    Implementation of the `Capt` command.  This saves the current frame of the
    program output into the media slots.  It is equivalent to pressing
    Capture Still in the Output menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
                       this command has no data
    ====== ==== ====== ===========

    """

    def get_command(self):
        return self._make_command('Capt', b'')


class MediaplayerSelectCommand(Command):
    """
    Implementation of the `MPSS` command. This sets the still or clip from the
    media pool to load into a mediaplayer.  It is equivalent to changing
    the mediaplayer clip in the UI Media panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     Mediaplayer index
    2      1    u8     Source type, 1=still, 2=clip
    3      1    u8     Still index
    4      1    u8     Clip index
    5      3    ?      padding
    ====== ==== ====== ===========

    """

    def __init__(self, index, still=None, clip=None):
        """
        :param index: Mediaplayer index
        :param still: Still index to load
        :param clip: Clip index to load
        """
        self.index = index
        self.still = still
        self.clip = clip
        if still is not None and clip is not None:
            raise ValueError("Can only set still= or clip=, not both")
        if still is None and clip is None:
            raise ValueError("Either still or clip is required")
        if clip is None:
            self.source_type = 1
        else:
            self.source_type = 2

    def get_command(self):
        mask = 1
        if self.still is not None:
            mask |= 1 << 1
        if self.clip is not None:
            mask |= 1 << 2

        still = self.still if self.still is not None else 0
        clip = self.clip if self.clip is not None else 0

        data = struct.pack('>BBBBBxxx', mask, self.index, self.source_type, still, clip)
        return self._make_command('MPSS', data)


class DkeyOnairCommand(Command):
    """
    Implementation of the `CDsL` command.  This sets the On Air state of the
    downstream keyer panel.  It is equivalent to pressing the On Air button
    in the DSK area of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Downstream keyer index
    1      1    ?      On air
    2      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, on_air):
        """
        :param index: 0-indexed DSK number to control
        :param on_air: True for on-air; False for off
        """
        self.index = index
        self.on_air = on_air

    def get_command(self):
        data = struct.pack('>B?xx', self.index, self.on_air)
        return self._make_command('CDsL', data)


class DkeyTieCommand(Command):
    """
    Implementation of the `CDsT` command.  This sets the "tie" state of the
    downstream keyer.  It is equivalent to pressing the Tie button in the DSK
    area of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Downstream keyer index
    1      1    ?      Tie
    2      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, tie):
        """
        :param index: 0-indexed DSK number to control
        :param tie: True for on; False for off
        """
        self.index = index
        self.tie = tie

    def get_command(self):
        data = struct.pack('>B?xx', self.index, self.tie)
        return self._make_command('CDsT', data)


class DkeyAutoCommand(Command):
    """
    Implementation of the `DDsA` command.  This triggers the auto transition of
    the downstream keyer.  It is equivalent to pressing the Auto button in the
    DSK area of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Downstream keyer index
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index):
        """
        :param index: 0-indexed DSK number to control
        """
        self.index = index

    def get_command(self):
        data = struct.pack('>Bxxx', self.index)
        return self._make_command('DDsA', data)


class DkeyRateCommand(Command):
    """
    Implementation of the `CDsR` command. This sets the transition duration
    for the downstream keyer.  It is equivalent to adjusting the DSK Rate of
    the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Downstream keyer index
    1      1    u8     Rate in frames
    2      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, rate):
        """
        :param index: 0-indexed DSK number to control
        :param rate: Transition duration in frames
        """
        self.index = index
        self.rate = rate

    def get_command(self):
        data = struct.pack('>BBxx', self.index, self.rate)
        return self._make_command('CDsR', data)


class DkeySetFillCommand(Command):
    """
    Implementation of the `CDsF` command.  This sets the fill source on a
    downstream keyer.  It is equivalent to adjusting the Fill Source of the
    Downstream Key settings in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Downstream keyer index
    1      1    ?      unknown
    2      2    u16    Fill source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, source):
        """
        :param index: 0-indexed DSK number to control
        :param source: The fill source index for the downstream keyer
        """
        self.index = index
        self.source = source

    def get_command(self):
        data = struct.pack('>BxH', self.index, self.source)
        return self._make_command('CDsF', data)


class DkeySetKeyCommand(Command):
    """
    Implementation of the `CDsC` command. This sets the key source on a
    downstream keyer.  It is equivalent to adjusting the Key Source of the
    Downstream Key settings in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Downstream keyer index
    1      1    ?      unknown
    2      2    u16    Key source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, source):
        """
        :param index: 0-indexed DSK number to control
        :param source: The key source index for downstream keyer
        """
        self.index = index
        self.source = source

    def get_command(self):
        data = struct.pack('>BxH', self.index, self.source)
        return self._make_command('CDsC', data)


class DkeyGainCommand(Command):
    """
    Implementation of the `CDsG` command.  This controls the gain, clip,
    premultiplied and invert settings for a downstream keyer.  It is equivalent
    to adjusting the Downstream Key settings in the Palettes menu of the UI
    Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     Downstream keyer index
    2      1    bool   Premultiplied
    3      1    ?      unknown
    4      2    u16    Clip
    6      2    u16    Gain
    8      1    bool   Invert
    9      3    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Pre-multiplied
    1   Clip
    2   Gain
    3   Invert key
    === ==========

    """

    def __init__(self, index, premultiplied=None, clip=None, gain=None, invert=None):
        """
        :param index: 0-indexed DSK number to control
        :param premultipled: Premultiplied state for the keyer
        :param clip: Vlip value for the keyer
        :param gain: Gain value for the keyer
        :param invert: Invert state for the keyer
        """
        self.index = index
        self.premultiplied = premultiplied
        self.clip = clip
        self.gain = gain
        self.invert = invert

    def get_command(self):
        mask = 0
        if self.premultiplied is not None:
            mask |= 0x01
        if self.clip is not None:
            mask |= 0x02
        if self.gain is not None:
            mask |= 0x04
        if self.invert is not None:
            mask |= 0x08

        premultiplied = False if self.premultiplied is None else self.premultiplied
        invert = False if self.invert is None else self.invert
        clip = 0 if self.clip is None else self.clip
        gain = 0 if self.gain is None else self.gain
        data = struct.pack('>BB ?x H H ?3x', mask, self.index, premultiplied, clip, gain, invert)
        return self._make_command('CDsG', data)


class DkeyMaskCommand(Command):
    """
    Implementation of the `CDsM` command.  This controls the mask values for a
    downstream keyer.  It is equivalent to adjusting Mask parameters in the
    Downstream Key settings of the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     Downstream keyer index
    2      1    bool   Mask enabled
    3      1    ?      unknown
    4      2    i16    Mask Top
    6      2    i16    Mask bottom
    8      2    i16    Mask left
    10     2    i16    Mask right
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Mask enabled
    1   Top
    2   Bottom
    3   Left
    4   Right
    === ==========

    """

    def __init__(self, index, enabled=None, top=None, bottom=None, left=None, right=None):
        """
        :param index: 0-indexed DSK number to control
        :param enabled: Enable or disable the mask
        :param top: Top offset for the mask
        :param bottom: Bottom offset for the mask
        :param left: Left offset for the mask
        :param right: Right offset for the mask
        """
        self.index = index
        self.enabled = enabled
        self.top = top
        self.bottom = bottom
        self.left = left
        self.right = right

    def get_command(self):
        mask = 0
        if self.enabled is not None:
            mask |= 0x01
        if self.top is not None:
            mask |= 0x02
        if self.bottom is not None:
            mask |= 0x04
        if self.left is not None:
            mask |= 0x08
        if self.right is not None:
            mask |= 0x10

        enabled = False if self.enabled is None else self.enabled
        top = 0 if self.top is None else self.top
        bottom = 0 if self.bottom is None else self.bottom
        left = 0 if self.left is None else self.left
        right = 0 if self.right is None else self.right
        data = struct.pack('>BB ?x 4h', mask, self.index, enabled, top, bottom, left, right)
        return self._make_command('CDsM', data)


class MixSettingsCommand(Command):
    """
    Implementation of the `CTMx` command.  This sets the parameters for mix
    transition.  It is equivalent to adjusting paramters on the Mix tab
    of the Transitions settings in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    u8     Rate in frames
    2      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, rate):
        """
        :param index: 0-indexed M/E number for the mix settings
        :param rate: Transition duration in frames
        """
        self.index = index
        self.rate = rate

    def get_command(self):
        data = struct.pack('>BBxx', self.index, self.rate)
        return self._make_command('CTMx', data)


class DipSettingsCommand(Command):
    """
    Implementation of the `CTDp` command.  This sets the parameters for
    dip transitions.  It is equivalent to adjusting parameters on the Dip tab
    of the Transitions settings in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, bit0=set rate, bit1=set source
    1      1    u8     M/E index
    2      1    u8     Rate in frames
    3      1    ?      unknown
    4      2    u16    Source index
    6      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, rate=None, source=None):
        """
        :param index: 0-indexed M/E number for the dip settings
        :param rate: Transition duration in frames
        :param source: The source for the dip transition
        """

        self.index = index
        self.rate = rate
        self.source = source

    def get_command(self):
        mask = 0
        if self.rate is not None:
            mask |= 0x01
        if self.source is not None:
            mask |= 0x02

        rate = 0 if self.rate is None else self.rate
        source = 0 if self.source is None else self.source
        data = struct.pack('>BBBx H 2x', mask, self.index, rate, source)
        return self._make_command('CTDp', data)


class WipeSettingsCommand(Command):
    """
    Implementation of the `CTWp` command.  This sets the parameters for
    wipe transitions.  It is equivalent to adjusting parameters on the Wipe tab
    of the Transitions settings in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Mask, see table below
    2      1    u8     M/E index
    3      1    u8     Rate in frames
    4      1    u8     Pattern style [0-17]
    5      1    ?      unknown
    6      2    u16    Border width [0-10000]
    8      2    u16    Border fill source
    10     2    u16    Symmetry [0-10000]
    12     2    u16    Softness [0-10000]
    14     2    16     Transition origin x [0-10000]
    16     2    16     Transition origin y [0-10000]
    18     1    bool   Reverse
    19     1    bool   Flip flop
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Rate
    1   Pattern
    2   Border width
    3   Border fill source
    4   Symmetry
    5   Softness
    6   Position x
    7   Position y
    8   Reverse
    9   Flip flop
    === ==========
    """

    def __init__(self, index, rate=None, pattern=None, width=None, source=None, symmetry=None, softness=None,
                 positionx=None, positiony=None, reverse=None, flipflop=None):
        """
        :param index: 0-indexed M/E number for the wipe settings
        :param rate: Transition duration in frames
        :param pattern: Transition pattern id
        :param width: Transition border width
        :param source: Transition border fill source index
        :param symmetry: Transition symmetry
        :param softness: Transition softness
        :param positionx: Transition origin x
        :param positiony: Transition origin y
        :param reverse: The reverse flag for the transition
        :param flipflop: The flipflop flag for the transition
        """

        self.index = index
        self.rate = rate
        self.pattern = pattern
        self.width = width
        self.source = source
        self.symmetry = symmetry
        self.softness = softness
        self.positionx = positionx
        self.positiony = positiony
        self.reverse = reverse
        self.flipflop = flipflop

    def get_command(self):
        mask = 0
        if self.rate is not None:
            mask |= 1 << 0
        if self.pattern is not None:
            mask |= 1 << 1
        if self.width is not None:
            mask |= 1 << 2
        if self.source is not None:
            mask |= 1 << 3
        if self.symmetry is not None:
            mask |= 1 << 4
        if self.softness is not None:
            mask |= 1 << 5
        if self.positionx is not None:
            mask |= 1 << 6
        if self.positiony is not None:
            mask |= 1 << 7
        if self.reverse is not None:
            mask |= 1 << 8
        if self.flipflop is not None:
            mask |= 1 << 9

        rate = 0 if self.rate is None else self.rate
        pattern = 0 if self.pattern is None else self.pattern
        width = 0 if self.width is None else self.width
        source = 0 if self.source is None else self.source
        symmetry = 0 if self.symmetry is None else self.symmetry
        softness = 0 if self.softness is None else self.softness
        x = 0 if self.positionx is None else self.positionx
        y = 0 if self.positiony is None else self.positiony
        reverse = False if self.reverse is None else self.reverse
        flipflop = False if self.flipflop is None else self.flipflop
        data = struct.pack('>HBBBx HHHHHH??', mask, self.index, rate, pattern, width, source, symmetry, softness, x, y,
                           reverse, flipflop)
        return self._make_command('CTWp', data)


class DveSettingsCommand(Command):
    """
    Implementation of the `CTDv` command.  This sets the parameters for
    DVE transitions.  It is equivalent to adjusting parameters of the DVE tab
    of the Transitions settings of the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Mask, see table below
    2      1    u8     M/E index
    3      1    u8     Rate in frames
    4      1    u8     Pattern style [0-17]
    5      1    ?      unknown
    6      2    u16    Fill source index
    8      2    u16    Key source index
    10     1    bool   Enable key
    11     1    bool   Key is premultiplied
    12     2    u16    Key clip [0-1000]
    14     2    u16    Key gain [0-1000]
    16     1    bool   Invert key
    17     1    bool   Reverse transition direction
    18     1    bool   Enable flip-flop
    19     1    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Rate
    1   ?
    2   Style
    3   Fill source
    4   Key source
    5   Enable key
    6   Key is premultiplied
    7   Key clip
    8   Key gain
    9   Invert key
    10  Reverse
    11  Flip-flop
    === ==========
    """

    def __init__(self, index, rate=None, style=None, fill_source=None, key_source=None, key_enable=None,
                 key_premultiplied=None, key_clip=None, key_gain=None, key_invert=None, reverse=None, flipflop=None):
        """
        :param index: 0-indexed M/E number for the DVE settings
        :param rate: Transition duration in frames
        :param style: Transition style
        :param fill_source: Fill source
        :param key_source: Key source
        :param key_enable: True enables the keyer; False disables
        :param key_premultiplied: Key is premultiplied alpha
        :param key_clip: Key clip
        :param key_gain: Key gain
        :param key_invert: Invert the key source
        :param reverse: Reverse flag for the transition
        :param flipflop: Flipflop flag for the transition
        """

        self.index = index
        self.rate = rate
        self.style = style
        self.fill_source = fill_source
        self.key_source = key_source
        self.key_enable = key_enable
        self.key_premultiplied = key_premultiplied
        self.key_clip = key_clip
        self.key_gain = key_gain
        self.key_invert = key_invert
        self.reverse = reverse
        self.flipflop = flipflop

    def get_command(self):
        mask = 0
        if self.rate is not None:
            mask |= 1 << 0
        if self.style is not None:
            mask |= 1 << 2
        if self.fill_source is not None:
            mask |= 1 << 3
        if self.key_source is not None:
            mask |= 1 << 4
        if self.key_enable is not None:
            mask |= 1 << 5
        if self.key_premultiplied is not None:
            mask |= 1 << 6
        if self.key_clip is not None:
            mask |= 1 << 7
        if self.key_gain is not None:
            mask |= 1 << 8
        if self.key_invert is not None:
            mask |= 1 << 9
        if self.reverse is not None:
            mask |= 1 << 10
        if self.flipflop is not None:
            mask |= 1 << 11

        rate = 0 if self.rate is None else self.rate
        style = 0 if self.style is None else self.style
        fill_source = 0 if self.fill_source is None else self.fill_source
        key_source = 0 if self.key_source is None else self.key_source
        key_enable = False if self.key_enable is None else self.key_enable
        key_premultiplied = False if self.key_premultiplied is None else self.key_premultiplied
        key_clip = 0 if self.key_clip is None else self.key_clip
        key_gain = 0 if self.key_gain is None else self.key_gain
        key_invert = False if self.key_invert is None else self.key_invert
        reverse = False if self.reverse is None else self.reverse
        flipflop = False if self.flipflop is None else self.flipflop
        data = struct.pack('>HBBx BHH ??HH? ?? x', mask, self.index, rate, style, fill_source, key_source, key_enable,
                           key_premultiplied, key_clip, key_gain, key_invert, reverse, flipflop)
        return self._make_command('CTDv', data)


class AudioMasterPropertiesCommand(Command):
    """
    Implementation of the `CAMM` command.  This sets the parameters of the
    master channel of legacy audio.  Audio properties are accessed via
    the UI Audio panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, see table below
    1      1    u8     unknown
    2      2    u16    Master volume [0 - 65381]
    4      2    ?      unknown
    6      1    bool   AFV
    7      1    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Volume
    1   ?
    2   AFV On/Off
    === ==========
    """

    def __init__(self, volume=None, afv=None):
        """
        :param volume: Volume of the master channel
        :param afv: Enable AFV for master, following the Fade-to-black
        """
        self.volume = volume
        self.afv = afv

    def get_command(self):
        mask = 0
        if self.volume is not None:
            mask |= 1 << 0
        if self.afv is not None:
            mask |= 1 << 2

        afv = False if self.afv is None else self.afv
        volume = 0 if self.volume is None else self.volume

        data = struct.pack('>B x H 2x ?x', mask, volume, afv)
        return self._make_command('CAMM', data)


class AudioMonitorPropertiesCommand(Command):
    """
    Implementation of the `CAMm` command.  This sets the parameters of the
    monitor bus of legacy audio.  Audio properties are accessed via
    the UI Audio panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, see table below
    1      1    bool   Enabled
    2      2    u16    Monitor volume [0 - 65381]
    4      1    bool   Mute
    5      1    bool   Solo
    6      2    u16    Solo source
    8      1    bool   Dim
    9      1    ?      unknown
    10     2    u16    Dim volume
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Enabled
    1   Gain
    2   Mute
    3   Solo
    4   Solo source
    5   Dim
    6   Dim volume
    === ==========
    """

    def __init__(self, enabled=None, volume=None, mute=None, solo=None, solo_source=None, dim=None, dim_volume=None):
        """
        :param enabled:
        :param volume: Volume of the master channel
        :param mute:
        :param solo:
        :param solo_source:
        :param dim:
        :param dim_volume:
        """
        self.enabled = enabled
        self.volume = volume
        self.mute = mute
        self.solo = solo
        self.solo_source = solo_source
        self.dim = dim
        self.dim_volume = dim_volume

    def get_command(self):
        mask = 0
        if self.enabled is not None:
            mask |= 1 << 0
        if self.volume is not None:
            mask |= 1 << 1
        if self.mute is not None:
            mask |= 1 << 2
        if self.solo is not None:
            mask |= 1 << 3
        if self.solo_source is not None:
            mask |= 1 << 4
        if self.dim is not None:
            mask |= 1 << 5
        if self.dim_volume is not None:
            mask |= 1 << 6

        enabled = False if self.enabled is None else self.enabled
        volume = 0 if self.volume is None else self.volume
        mute = False if self.mute is None else self.mute
        solo = False if self.solo is None else self.solo
        solo_source = 0 if self.solo_source is None else self.solo_source
        dim = False if self.dim is None else self.dim
        dim_volume = 0 if self.dim_volume is None else self.dim_volume

        data = struct.pack('>BB H ?? H ?x H', mask, enabled, volume, mute, solo, solo_source, dim, dim_volume)
        return self._make_command('CAMm', data)


class AudioInputCommand(Command):
    """
    Implementation of the `CAMI` command.  This sets the parameters of a
    channel strip in legacy audio.  Audio properties are accessed via the
    UI Audio panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, see table below
    2      2    u16    Source index
    4      1    u8     Mix Option [0: Off, 1: On, 2: AFV]
    5      1    u8     unknown
    6      2    u16    Volume [0 - 65381]
    8      2    i16    Balance [-10000 - 10000]
    10     2    u16    unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Mix Option
    1   Volume
    2   Balance
    3   ?
    4   ?
    5   ?
    6   ?
    7   ?
    8   ?
    === ==========


    """

    def __init__(self, source, balance=None, volume=None, on=None, afv=None):
        """
        :param source:
        :param balance:
        :param volume:
        :param on:
        :param afv:
        """
        self.source = source
        self.balance = balance
        self.volume = volume
        self.on = on
        self.afv = afv

    def get_command(self):
        mask = 0
        if self.on is not None or self.afv is not None:
            mask |= 1 << 0
        if self.volume is not None:
            mask |= 1 << 1
        if self.balance is not None:
            mask |= 1 << 2

        state = 0
        if self.on is not None:
            state = int(bool(self.on))
        elif self.afv is not None:
            state = int(bool(self.afv)) * 2

        balance = 0 if self.balance is None else self.balance
        volume = 0 if self.volume is None else self.volume

        data = struct.pack('>B x H B x H h x x', mask, self.source, state, volume, balance)
        return self._make_command('CAMI', data)


class FairlightMasterPropertiesCommand(Command):
    """
    Implementation of the `CFMP` command.  This sets the parameters of the
    master channel of fairlight audio.  Audio properties are accessed via
    the UI Audio panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, see table below
    6      2    i16    EQ gain [-2000 - 2000]
    10     2    u16    Dynamics make-up gain [0 - 2000]
    12     4    i32    Master volume [-10000 - 1000]
    16     1    bool   Audio follow video
    17     1    bool   Enable master EQ
    18     2    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   EQ Enable
    1   EQ Gain
    2   Dynamics gain
    3   Master volume
    4   AFV
    5   ?
    6   ?
    7   ?
    === ==========

    """

    def __init__(self, eq_gain=None, dynamics_gain=None, volume=None, afv=None, eq_enable=None):
        """
        :param eq_gain: Make-up gain for the master EQ dialog
        :param dynamics_gain: Make-up gain for the master Dynamics dialog
        :param volume: Master volume fader value
        :param afv: Make the master volume follow the fade-to-black function
        :param eq_enable: Enable the EQ on the master bus
        """

        self.eq_gain = eq_gain
        self.dynamics_gain = dynamics_gain
        self.volume = volume
        self.afv = afv
        self.eq_enable = eq_enable

    def get_command(self):
        mask = 0
        if self.eq_enable is not None:
            mask |= 1 << 0
        if self.eq_gain is not None:
            mask |= 1 << 1
        if self.dynamics_gain is not None:
            mask |= 1 << 2
        if self.volume is not None:
            mask |= 1 << 3
        if self.afv is not None:
            mask |= 1 << 4

        eq_enable = False if self.eq_enable is None else self.eq_enable
        eq_gain = 0 if self.eq_gain is None else self.eq_gain
        dynamics_gain = 0 if self.dynamics_gain is None else self.dynamics_gain
        volume = 0 if self.volume is None else self.volume
        afv = False if self.afv is None else self.afv

        data = struct.pack('>B 5x h 2x Hi?? 2x', mask, eq_gain, dynamics_gain, volume, afv, eq_enable)
        return self._make_command('CFMP', data)


class FairlightStripPropertiesCommand(Command):
    """
    Implementation of the `CFSP` command.  This sets the parameters of a channel
    strip in fairlight audio.  Audio properties are accessed via the UI Audio
    panel.

    The source index is the same as the video source index from the
    InputProperties fields.

    For normal channel strips the channel index should be -1.  If the channel
    is split (two mono channels instead of a stereo input) the channel numbers
    are 0 and 1.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Mask, see table below
    2      2    u16    Source index
    15     1    u8     Channel number
    16     1    u8     Delay in frames
    22     4    i32    Gain [-10000 - 600]
    30     2    i16    EQ Gain
    34     2    u16    Dynamics Gain
    36     2    i16    Pan [-10000 - 10000]
    40     4    i32    volume [-10000 - 1000]
    44     1    u8     State
    45     3    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Delay
    1   Gain
    2   ?
    3   EQ Enable
    4   EQ Gain
    5   Dynamics gain
    6   Balance
    7   Volume
    8   State
    === ==========


    """

    def __init__(self, source, channel, delay=None, gain=None, eq_gain=None, eq_enable=None, dynamics_gain=None,
                 balance=None, volume=None, state=None):
        """
        :param source: The source index
        :param channel: The channel index inside the source, -1 for the normal stereo channels and 0 and 1 for split
        :param delay: Delay in frames
        :param gain: Input gain
        :param eq_gain: Make-up gain from the EQ dialog
        :param eq_enable: Enable the EQ block
        :param dynamics_gain: Make-up gain from the dynamics dialog
        :param balance: Channel balance or pan
        :param volume: The volume fader level
        :param state: Bit field for the mute and AFV state. 1=off, 2=on, 4=afv
        """
        self.source = source
        self.channel = channel
        self.delay = delay
        self.gain = gain
        self.eq_gain = eq_gain
        self.eq_enable = eq_enable
        self.dynamics_gain = dynamics_gain
        self.balance = balance
        self.volume = volume
        self.state = state

    def get_command(self):
        mask = 0
        if self.delay is not None:
            mask |= 1 << 0
        if self.gain is not None:
            mask |= 1 << 1
        if self.eq_enable is not None:
            mask |= 1 << 3
        if self.eq_gain is not None:
            mask |= 1 << 4
        if self.dynamics_gain is not None:
            mask |= 1 << 5
        if self.balance is not None:
            mask |= 1 << 6
        if self.volume is not None:
            mask |= 1 << 7
        if self.state is not None:
            mask |= 1 << 8

        delay = 0 if self.delay is None else self.delay
        gain = 0 if self.gain is None else self.gain
        eq_enable = False if self.eq_enable is None else self.eq_enable
        eq_gain = 0 if self.eq_gain is None else self.eq_gain
        dynamics_gain = 0 if self.dynamics_gain is None else self.dynamics_gain
        balance = 0 if self.balance is None else self.balance
        volume = 0 if self.volume is None else self.volume
        state = 0 if self.state is None else self.state

        split = 0xff if self.channel > -1 else 0x01
        self.channel = 0x00 if self.channel == -1 else self.channel
        pad = b'\xff\xff\xff\xff\xff\xff\xff'
        data = struct.pack('>H H4x6sBb B 3x i xx? 1x h 2x H 2x h 2x iB 3x', mask, self.source, pad, split, self.channel,
                           delay,
                           gain, eq_enable, eq_gain,
                           dynamics_gain, balance, volume, state)
        return self._make_command('CFSP', data)


class FairlightEqBandPropertiesCommand(Command):
    """
    Implementation of the `CEBP` command.  This sets the parameters of a single
    EQ band in the fairlight audio mixer.  Audio properties are accessed via
    the UI Audio panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, see table below
    1      1    ?      padding
    2      2    u16    Source index
    15     1    u8     Channel number
    16     1    u8     EQ band index [0-5]
    17     1    bool   Band enabled
    18     1    u8     Band filter type
    19     1    u8     Band frequency range
    20     4    u32    Band frequency [30-21700]
    24     4    i32    Band gain [-2000-2000]
    28     2    i16    Band Q [30-1030]
    30     2    ?      padding
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Delay
    1   Gain
    2   ?
    3   EQ Enable
    4   EQ Gain
    5   Dynamics gain
    6   Balance
    7   Volume
    8   State
    === ==========

    """

    def __init__(self, source, channel, band, enabled=None, band_filter=None, band_range=None, frequency=None,
                 gain=None, q=None):
        """
        :param source: The source index
        :param channel: The channel index inside the source, -1 for the normal stereo channels and 0 and 1 for split
        :param band:
        :param enabled:
        :param band_filter:
        :param band_range:
        :param frequency:
        :param gain:
        :param q:
        """
        self.source = source
        self.channel = channel
        self.band = band
        self.enabled = enabled
        self.filter = band_filter
        self.range = band_range
        self.frequency = frequency
        self.gain = gain
        self.q = q

    def get_command(self):
        mask = 0
        if self.enabled is not None:
            mask |= 1 << 0
        if self.filter is not None:
            mask |= 1 << 1
        if self.range is not None:
            mask |= 1 << 2
        if self.frequency is not None:
            mask |= 1 << 3
        if self.gain is not None:
            mask |= 1 << 4
        if self.q is not None:
            mask |= 1 << 5

        enabled = 0 if self.enabled is None else self.enabled
        filter = 0 if self.filter is None else self.filter
        range = 0 if self.range is None else self.range
        frequency = 0 if self.frequency is None else self.frequency
        gain = 0 if self.gain is None else self.gain
        q = 0 if self.q is None else self.q

        split = 0xff if self.channel > -1 else 0x01
        self.channel = 0x00 if self.channel == -1 else self.channel
        pad = b'\xff\xff\xff\xff\xff\xff\xff'
        data = struct.pack('>Bx H4x6sBb B?BB I i h2x', mask, self.source, pad, split,
                           self.channel, self.band,
                           enabled, filter, range, frequency, gain, q)
        return self._make_command('CEBP', data)


class KeyOnAirCommand(Command):
    """
    Implementation of the `CKOn` command.  This enables an upstream keyer using
    a cut transition.  It is equivalent to pressing the On Air button in the
    Next Transition area of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    u8     Keyer index
    2      1    bool   Enabled
    4      1    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, keyer, enabled):
        """
        :param index: 0-indexed M/E number for the upstream keyer
        :param keyer: 0-indexed keyer number
        :param enabled: True set the keyer on-air; False for off
        """
        self.index = index
        self.keyer = keyer
        self.enabled = enabled

    def get_command(self):
        data = struct.pack('>BB?x', self.index, self.keyer, self.enabled)
        return self._make_command('CKOn', data)


class KeyFillCommand(Command):
    """
    Implementation of the `CKeF` command.  This sets the fill source for the
    upstream keyer.  It is equivalent to adjusting a Fill Source on a
    selected tab in the Upstream Key settings of the Palettes menu of the
    UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    u8     Keyer index
    2      2    u16    Source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, keyer, source):
        """
        :param index: 0-indexed M/E number for the upstream keyer
        :param keyer: 0-indexed keyer number
        :param source: Source index for the fill
        """
        self.index = index
        self.keyer = keyer
        self.source = source

    def get_command(self):
        data = struct.pack('>BBH', self.index, self.keyer, self.source)
        return self._make_command('CKeF', data)


class KeyCutCommand(Command):
    """
    Implementation of the `CKeC` command.  This sets the key source for an
    upstream keyer.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    u8     Keyer index
    2      2    u16    Source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, keyer, source):
        """
        :param index: 0-indexed M/E number for the upstream keyer
        :param keyer: 0-indexed keyer number
        :param source: Source index for the keyer fill
        """
        self.index = index
        self.keyer = keyer
        self.source = source

    def get_command(self):
        data = struct.pack('>BBH', self.index, self.keyer, self.source)
        return self._make_command('CKeC', data)


class KeyTypeCommand(Command):
    """
    Implementation of the `CKTp` command.  This sets the type of an upstream
    keyer.  It is equivalent to selecting an Upstream Key tab in the Palettes
    menu of the UI Switcher panel.

    The type argument takes one of the values from the KeyTypeCommand constants:

    - KeyTypeCommand.LUMA
    - KeyTypeCommand.CHROMA
    - KeyTypeCommand.PATTERN
    - KeyTypeCommand.DVE

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     M/E index
    2      1    u8     Keyer index
    3      1    u8     Keyer type
    4      1    bool   Fly enabled
    5      3    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Key type
    1   Fly enabled
    === ==========

    """

    LUMA = 0
    CHROMA = 1
    PATTERN = 2
    DVE = 3

    def __init__(self, index, keyer, type=None, fly_enabled=None):
        """
        :param index: 0-indexed M/E number for the upstream keyer
        :param keyer: 0-indexed keyer number
        :param type: The key type
        :param fly-enabled: True enables the flying key; False disables
        """
        self.index = index
        self.keyer = keyer
        self.type = type
        self.fly_enabled = fly_enabled

    def get_command(self):
        mask = 0
        if self.type is not None:
            mask |= 1 << 0
        if self.fly_enabled is not None:
            mask |= 1 << 1

        key_type = 0 if self.type is None else self.type
        fly_enabled = 0 if self.fly_enabled is None else self.fly_enabled

        data = struct.pack('>BBB B? 3x', mask, self.index, self.keyer, key_type, fly_enabled)
        return self._make_command('CKTp', data)


class KeyPropertiesDveCommand(Command):
    """
    Implementation of the `CKDV` command.  This sets the parameters of the DVE
    effect in an upstream keyer.  It is equivalent to adjusting parameters in
    the Upstream Key settings DVE tab of the Palettes menu of the UI Switcher
    panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u32    Mask
    4      1    u8     M/E index
    5      1    u8     Keyer index
    6      2    ?      unknown
    8      4    i32    Size x [0 - 9900]
    12     4    i32    Size y [0 - 9900]
    16     4    i32    Position x [-16000 - 16000]
    20     4    i32    Position y [-9000 - 9000]
    24     4    i32    Rotation
    28     1    bool   Border enabled
    29     1    bool   Shadow enabled
    30     1    u8     Border bevel mode
    31     1    ?      unknown
    32     2    u16    Border outer width [0 - 1600]
    34     2    u16    Border inner width [0 - 1600]
    36     1    u8     Border outer softness [0 - 100]
    37     1    u8     Border inner softness [0 - 100]
    38     1    u8     Border bevel softness [0 - 100]
    39     1    u8     Border bevel position [0 - 100]
    40     1    u8     Border opacity [0-100]
    41     1    ?      unknown
    42     2    u16    Border color hue [0 - 3599]
    44     2    u16    Border color saturation [0 - 1000]
    46     2    u16    Border color luma [0 - 1000]
    48     2    u16    Shadow light source angle [0 - 3590]
    50     1    u8     Shadow light source altitude [10-100]
    51     1    bool   Mask enabled
    52     2    i16    Mask top [-9000 - 9000]
    54     2    i16    Mask bottom [-9000 - 9000]
    56     2    i16    Mask left [-16000 - 16000]
    58     2    i16    Mask right [-16000 - 16000]
    60     1    u8     Rate
    61     3    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Size x
    1   Size y
    2   Position x
    3   Position y
    4   Rotation
    5   Border enabled
    6   Shadow enabled
    7   Border bevel mode
    8   Outer width
    9   Inner width
    10  Outer softness
    11  Inner softness
    12  Bevel softness
    13  Bevel position
    14  Border opacity
    15  Border hue
    16  Border saturation
    17  Border luma
    18  Shadow angle
    19  Shadow altitude
    20  Mask enabled
    21  Mask top
    22  Mask bottom
    23  Mask left
    24  Mask right
    25  Rate
    === ==========

    """

    def __init__(self, index, keyer, size_x=None, size_y=None, pos_x=None, pos_y=None, rotation=None,
                 border_enabled=None, shadow_enabled=None, border_bevel_enabled=None, outer_width=None,
                 inner_width=None, outer_softness=None, inner_softness=None, bevel_softness=None, bevel_position=None,
                 border_opacity=None, border_hue=None, border_saturation=None, border_luma=None, angle=None,
                 altitude=None, mask_enabled=None, mask_top=None, mask_bottom=None, mask_left=None, mask_right=None,
                 rate=None):
        """
        :param index: 0-indexed M/E number of the upstream keyer
        :param keyer: 0-indexed keyer number
        :param size_x: The relative size in the x-direction; 1000 is full
        :param size_y: The relative size in the y-direction; 1000 is full
        :param pos_x: The position in the x-direction; 0 is centre
        :param size_y: The position in the y-direction; 0 is centre
        :param rotation:
        :param border_enabled:
        :param shadow_enabled:
        :param border_bevel_enabled:
        :param outer_width:
        :param inner_width:
        :param outer_softness:
        :param inner_softness:
        :param bevel_softness:
        :param bevel_position:
        :param order_opacity:
        :param border_hue:
        :param border_saturation:
        :param border_luma:
        :param angle:
        :param altitude:
        :param mask_enabled:
        :param mask_top:
        :param mask_bottom:
        :param mask_left:
        :param mask_right:
        :param rate: Effect duration in frames
        """
        self.index = index
        self.keyer = keyer
        self.size_x = size_x
        self.size_y = size_y
        self.pos_x = pos_x
        self.pos_y = pos_y
        self.rotation = rotation
        self.border_enabled = border_enabled
        self.shadow_enabled = shadow_enabled
        self.border_bevel_enabled = border_bevel_enabled
        self.outer_width = outer_width
        self.inner_width = inner_width
        self.outer_softness = outer_softness
        self.inner_softness = inner_softness
        self.bevel_softness = bevel_softness
        self.bevel_position = bevel_position
        self.border_opacity = border_opacity
        self.border_hue = border_hue
        self.border_saturation = border_saturation
        self.border_luma = border_luma
        self.angle = angle
        self.altitude = altitude
        self.mask_enabled = mask_enabled
        self.mask_top = mask_top
        self.mask_bottom = mask_bottom
        self.mask_left = mask_left
        self.mask_right = mask_right
        self.rate = rate

    def set_border_color_rgb(self, red, green, blue):
        h, l, s = colorsys.rgb_to_hls(red, green, blue)
        self.border_hue = int(h * 3590)
        self.border_saturation = int(s * 1000)
        self.border_luma = int(l * 1000)

    def get_command(self):
        mask = 0
        if self.size_x is not None:
            mask |= 1 << 0
        if self.size_y is not None:
            mask |= 1 << 1
        if self.pos_x is not None:
            mask |= 1 << 2
        if self.pos_y is not None:
            mask |= 1 << 3
        if self.rotation is not None:
            mask |= 1 << 4
        if self.border_enabled is not None:
            mask |= 1 << 5
        if self.shadow_enabled is not None:
            mask |= 1 << 6
        if self.border_bevel_enabled is not None:
            mask |= 1 << 7
        if self.outer_width is not None:
            mask |= 1 << 8
        if self.inner_width is not None:
            mask |= 1 << 9
        if self.outer_softness is not None:
            mask |= 1 << 10
        if self.inner_softness is not None:
            mask |= 1 << 11
        if self.bevel_softness is not None:
            mask |= 1 << 12
        if self.bevel_position is not None:
            mask |= 1 << 13
        if self.border_opacity is not None:
            mask |= 1 << 14
        if self.border_hue is not None:
            mask |= 1 << 15
        if self.border_saturation is not None:
            mask |= 1 << 16
        if self.border_luma is not None:
            mask |= 1 << 17
        if self.angle is not None:
            mask |= 1 << 18
        if self.altitude is not None:
            mask |= 1 << 19
        if self.mask_enabled is not None:
            mask |= 1 << 20
        if self.mask_top is not None:
            mask |= 1 << 21
        if self.mask_bottom is not None:
            mask |= 1 << 22
        if self.mask_left is not None:
            mask |= 1 << 23
        if self.mask_right is not None:
            mask |= 1 << 24
        if self.rate is not None:
            mask |= 1 << 25

        index = 0 if self.index is None else self.index
        keyer = 0 if self.keyer is None else self.keyer
        size_x = 0 if self.size_x is None else self.size_x
        size_y = 0 if self.size_y is None else self.size_y
        pos_x = 0 if self.pos_x is None else self.pos_x
        pos_y = 0 if self.pos_y is None else self.pos_y
        rotation = 0 if self.rotation is None else self.rotation
        border_enabled = 0 if self.border_enabled is None else self.border_enabled
        shadow_enabled = 0 if self.shadow_enabled is None else self.shadow_enabled
        border_bevel_enabled = 0 if self.border_bevel_enabled is None else self.border_bevel_enabled
        outer_width = 0 if self.outer_width is None else self.outer_width
        inner_width = 0 if self.inner_width is None else self.inner_width
        outer_softness = 0 if self.outer_softness is None else self.outer_softness
        inner_softness = 0 if self.inner_softness is None else self.inner_softness
        bevel_softness = 0 if self.bevel_softness is None else self.bevel_softness
        bevel_position = 0 if self.bevel_position is None else self.bevel_position
        border_opacity = 0 if self.border_opacity is None else self.border_opacity
        border_hue = 0 if self.border_hue is None else self.border_hue
        border_saturation = 0 if self.border_saturation is None else self.border_saturation
        border_luma = 0 if self.border_luma is None else self.border_luma
        angle = 0 if self.angle is None else self.angle
        altitude = 0 if self.altitude is None else self.altitude
        mask_enabled = 0 if self.mask_enabled is None else self.mask_enabled
        mask_top = 0 if self.mask_top is None else self.mask_top
        mask_bottom = 0 if self.mask_bottom is None else self.mask_bottom
        mask_left = 0 if self.mask_left is None else self.mask_left
        mask_right = 0 if self.mask_right is None else self.mask_right
        rate = 0 if self.rate is None else self.rate

        data = struct.pack('>I BBxx 5i ??Bx HH5Bx 4HB?hhhhBxxx', mask, index,
                           keyer,

                           size_x,
                           size_y,
                           pos_x,
                           pos_y,
                           rotation,

                           border_enabled,
                           shadow_enabled,
                           border_bevel_enabled,

                           outer_width,
                           inner_width,
                           outer_softness,
                           inner_softness,
                           bevel_softness,
                           bevel_position,
                           border_opacity,

                           border_hue,
                           border_saturation,
                           border_luma,
                           angle,

                           altitude,
                           mask_enabled,
                           mask_top,
                           mask_bottom,
                           mask_left,
                           mask_right,
                           rate)
        return self._make_command('CKDV', data)


class KeyPropertiesAdvancedChromaColorpickerCommand(Command):
    """
    Implementation of the `CACC` command.  This sets the state of the
    colorpicker for the upstream advanced chroma keyer.  It is equivalent to
    adjusting color picker parameters in the Upstream Key settings Chroma tab
    of the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     M/E index
    2      1    u8     Keyer index
    3      1    bool   Enable cursor
    4      1    bool   Enable preview
    5      1    ?      padding
    6      2    i16    Cursor X [-16000 - 16000]
    8      2    i16    Cursor Y [-9000 - 9000]
    10     2    u16    Cursor size [620 - 9925]
    12     2    u16    Color Y
    14     2    i16    Color Cb
    16     2    i16    Color Cr
    18     2    ?      padding
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Enable cursor
    1   Enable preview
    2   Cursor X
    3   Cursor Y
    4   Cursor size
    5   Color Y
    6   Color Cb
    7   color Cr
    === ==========

    """

    def __init__(self, index, keyer, cursor=None, preview=None, x=None, y=None, size=None, Y=None, Cb=None, Cr=None):
        """
        :param index: 0-indexed M/E number of the upstream keyer
        :param keyer: 0-indexed keyer number
        :param cursor:
        :param preview:
        :param x:
        :param y:
        :param size:
        :param Y:
        :param Cb:
        :param Cr:
        """
        self.index = index
        self.keyer = keyer
        self.cursor = cursor
        self.preview = preview
        self.x = x
        self.y = y
        self.size = size
        self.Y = Y
        self.Cb = Cb
        self.Cr = Cr

    def get_command(self):
        mask = 0
        if self.cursor is not None:
            mask |= 1 << 0
        if self.preview is not None:
            mask |= 1 << 1
        if self.x is not None:
            mask |= 1 << 2
        if self.y is not None:
            mask |= 1 << 3
        if self.size is not None:
            mask |= 1 << 4
        if self.Y is not None:
            mask |= 1 << 5
        if self.Cb is not None:
            mask |= 1 << 6
        if self.Cr is not None:
            mask |= 1 << 7

        index = 0 if self.index is None else self.index
        keyer = 0 if self.keyer is None else self.keyer
        cursor = False if self.cursor is None else self.cursor
        preview = False if self.preview is None else self.preview
        x = 0 if self.x is None else self.x
        y = 0 if self.y is None else self.y
        size = 0 if self.size is None else self.size
        Y = 0 if self.Y is None else self.Y
        Cb = 0 if self.Cb is None else self.Cb
        Cr = 0 if self.Cr is None else self.Cr

        data = struct.pack('>BBB? ?x hhH Hhh xx', mask, index, keyer,
                           cursor, preview, x, y, size, Y, Cb, Cr)

        return self._make_command('CACC', data)


class KeyPropertiesAdvancedChromaCommand(Command):
    """
    Implementation of the `CACK` command.  This sets the state for the
    upstream advanced chroma keyer.  It is equivalent to adjusting parameters
    in the Upstream Key settings Chroma tab of the Palettes menu of the UI
    Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Mask
    2      1    u8     M/E index
    3      1    u8     Keyer index
    4      2    u16    Foreground [0 - 1000]
    6      2    u16    Background [0 - 1000]
    8      2    u16    Key edge [0 - 1000]
    10     2    u16    Spill suppress [0 - 1000]
    12     2    u16    Flare suppress [0 - 1000]
    14     2    i16    Brightness [-1000 - 1000]
    16     2    i16    Contrast [-1000 - 1000]
    18     2    u16    Saturation [0 - 2000]
    20     2    i16    Red [-1000 - 1000]
    22     2    i16    Green [-1000 - 1000]
    24     2    i16    Blue [-1000 - 1000]
    26     2    ?      padding
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Foreground
    1   Background
    2   Key edge
    3   Spill suppress
    4   Flare suppress
    5   Brightness
    6   Contrast
    7   Saturation
    8   Red
    9   Green
    10  Blue
    === ==========

    """

    def __init__(self, index, keyer, foreground=None, background=None, key_edge=None, spill=None, flare=None,
                 brightness=None, contrast=None, saturation=None, red=None, green=None, blue=None):
        """
        :param index: 0-indexed M/E number of the upstream keyer
        :param keyer: 0-indexed keyer number
        :param foreground:
        :param background:
        :param key_edge:
        :param spill:
        :param flare:
        :param brightne:
        :param contrast:
        :param saturation:
        :param red:
        :param green:
        :param blue:
        """
        self.index = index
        self.keyer = keyer
        self.foreground = foreground
        self.background = background
        self.key_edge = key_edge
        self.spill = spill
        self.flare = flare
        self.brightness = brightness
        self.contrast = contrast
        self.saturation = saturation
        self.red = red
        self.green = green
        self.blue = blue

    def get_command(self):
        mask = 0
        if self.foreground is not None:
            mask |= 1 << 0
        if self.background is not None:
            mask |= 1 << 1
        if self.key_edge is not None:
            mask |= 1 << 2
        if self.spill is not None:
            mask |= 1 << 3
        if self.flare is not None:
            mask |= 1 << 4
        if self.brightness is not None:
            mask |= 1 << 5
        if self.contrast is not None:
            mask |= 1 << 6
        if self.saturation is not None:
            mask |= 1 << 7
        if self.red is not None:
            mask |= 1 << 8
        if self.green is not None:
            mask |= 1 << 9
        if self.blue is not None:
            mask |= 1 << 10

        foreground = 0 if self.foreground is None else self.foreground
        background = 0 if self.background is None else self.background
        key_edge = 0 if self.key_edge is None else self.key_edge
        spill = 0 if self.spill is None else self.spill
        flare = 0 if self.flare is None else self.flare
        brightness = 0 if self.brightness is None else self.brightness
        contrast = 0 if self.contrast is None else self.contrast
        saturation = 0 if self.saturation is None else self.saturation
        red = 0 if self.red is None else self.red
        green = 0 if self.green is None else self.green
        blue = 0 if self.blue is None else self.blue

        data = struct.pack('>HBB HHH HH hhHhhh xx', mask, self.index, self.keyer, foreground, background, key_edge,
                           spill,
                           flare, brightness, contrast, saturation, red, green, blue)

        return self._make_command('CACK', data)


class KeyPropertiesLumaCommand(Command):
    """
    Implementation of the `CKLm` command. This sets the key source for an
    upstream luma keyer.  It is equivalent to adjusting parameters in the
    Upstream Key settings Luma tab of the Palettes menu of the UI Switcher
    panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     M/E index
    2      1    u8     Keyer index
    3      1    bool   Pre-multiplied
    4      2    u16    Clip
    6      2    u16    Gain
    8      1    bool   Invert key
    9      3    ?      unknown
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Pre-multiplied
    1   Clip
    2   Gain
    3   Invert key
    === ==========

    """

    def __init__(self, index, keyer, premultiplied=None, clip=None, gain=None, invert_key=None):
        """
        :param index: 0-indexed M/E number of the upstream keyer
        :param keyer: 0-indexed keyer number
        :param premultiplied:
        :param clip:
        :param gain:
        :param invert_key:
        """
        self.index = index
        self.keyer = keyer
        self.premultiplied = premultiplied
        self.clip = clip
        self.gain = gain
        self.invert_key = invert_key

    def get_command(self):
        mask = 0
        if self.premultiplied is not None:
            mask |= 1 << 0
        if self.clip is not None:
            mask |= 1 << 1
        if self.gain is not None:
            mask |= 1 << 2
        if self.invert_key is not None:
            mask |= 1 << 3

        premultiplied = 0 if self.premultiplied is None else self.premultiplied
        clip = 0 if self.clip is None else self.clip
        gain = 0 if self.gain is None else self.gain
        invert_key = 0 if self.invert_key is None else self.invert_key

        data = struct.pack('>BBB?HH?3x', mask, self.index, self.keyer, premultiplied, clip, gain, invert_key)
        return self._make_command('CKLm', data)


class KeyerKeyframeSetCommand(Command):
    """
    Implementation of the `SFKF` command.  This sets the A or B keyframe
    for the upstream flying key to the current DVE values (pos_x, pos_y,
    size_x, size_y).  It is equivalent to adjusting Keyframe parameters in the
    Upstream Key settings DVE tab in the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    u8     Keyer index
    2      1    u8     keyframe, A = 1, B = 2
    3      1    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, keyer, keyframe):
        """
        :param index: 0-indexed M/E number of the upstream keyer
        :param keyer: 0-indexed keyer number
        :param keyframe: The keyframe to set, either 'A' or 'B'
        """
        self.index = index
        self.keyer = keyer
        self.keyframe = keyframe

    def get_command(self):
        keyframe = 1 if self.keyframe == 'A' else 2
        data = struct.pack('>BBBx', self.index, self.keyer, keyframe)
        return self._make_command('SFKF', data)


class KeyerKeyframeRunCommand(Command):
    """
    Implementation of the `RFlK` command.  This makes the upstream DVE keyer
    flying key run to A, B, Full or Infinite.  It is equivalent to pressing
    A, B or fill in the Keyframe area of the Upstream Key settings DVE tab in
    the Palettes menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     M/E index
    2      1    u8     Keyer index
    3      1    ?      unknown
    4      1    u8     Keyframe, 1=A, 2=B, 3=Full, 4=Infinite
    5      1    u8     Ininite run index
    6      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, index, keyer, run_to=None, set_infinite=None):
        """
        :param index: 0-indexed M/E number of the upstream keyer
        :param keyer: 0-indexed keyer number
        :param run_to: The keyframe: 'A', 'B', 'Full' or 'Infinite'.
        :param set_infinite: Infinite run index
        """
        self.index = index
        self.keyer = keyer
        self.run_to = run_to
        self.set_infinite = set_infinite

    def get_command(self):
        run_to_lut = {
            'A': 1,
            'B': 2,
            'Full': 3,
            'Infinite': 4,
        }
        run_to = run_to_lut[self.run_to]
        set_infinite = self.set_infinite or 0

        mask = 0
        if self.set_infinite is not None:
            mask |= 1 << 1

        data = struct.pack('>BBBxBB2x', mask, self.index, self.keyer, run_to, set_infinite)
        return self._make_command('RFlK', data)


class RecorderStatusCommand(Command):
    """
    Implementation of the `RcTM` command.  This starts and stops the stream
    recorder.  It is equivalent to pressing REC in the Record area of the
    Output menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    bool   Recording state
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, recording):
        """
        :param recording: Whether the hardware should be recording
        """
        self.recording = recording

    def get_command(self):
        data = struct.pack('>?3x', self.recording)
        return self._make_command('RcTM', data)


class RecordingSettingsSetCommand(Command):
    """
    Implementation of the `CRMS` command.  This sets the parameters for the
    stream recorder.  It is equivalent to adjusting parameters in the Record
    settings of the Output menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      128  str    Filename
    132    4    u32    Disk 1 id
    136    4    i32    Disk 2 id
    140    1    bool   Record in all cameras
    ====== ==== ====== ===========

    """

    def __init__(self, filename=None, disk1=None, disk2=None, record_in_camera=None):
        """
        :param filename: Filename for recording
        :param disk1: ID for disk 1
        :param disk2: ID for disk 2
        :param record_in_camera: Trigger recording on attached cameras
        """
        self.filename = filename
        self.disk1 = disk1
        self.disk2 = disk2
        self.record_in_camera = record_in_camera

    def get_command(self):
        mask = 0
        if self.filename is not None:
            mask |= 1 << 0
        if self.disk1 is not None:
            mask |= 1 << 1
        if self.disk2 is not None:
            mask |= 1 << 2
        if self.record_in_camera is not None:
            mask |= 1 << 3

        filename = self.filename.encode() if self.filename is not None else b''
        ric = self.record_in_camera if self.record_in_camera is not None else False

        data = struct.pack('>B 128s xxx II ?xxx', mask, filename, self.disk1 or 0, self.disk2 or 0, ric)
        return self._make_command('CRMS', data)


class StreamingServiceSetCommand(Command):
    """
    Implementation of the `CRSS` command.  This sets the parameters for the live
    stream output.  It is equivalent to adjusting parameters in the Live Stream
    settings of the Output menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      64   str    Service name
    65     512  str    Url
    577    512  str    Streaming key
    1089   3    ?      padding bytes
    1092   4    u32    Minimum bitrate (Bps)
    1096   4    u32    Maximum bitrate (Bps)
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Service name
    1   Url
    2   Key
    3   Min/Max bitrate
    === ==========

    """

    def __init__(self, name=None, url=None, key=None, bitrate_min=None, bitrate_max=None):
        """
        :param name: New streaming service name
        :param url: RTMP url
        :param key: Stream key
        :param bitrate_min: Minimum bitrate
        :param bitrate_max: Maximum bitrate
        """
        self.name = name
        self.url = url
        self.key = key
        self.bitrate_min = bitrate_min
        self.bitrate_max = bitrate_max

    def get_command(self):
        if self.bitrate_min is None and self.bitrate_max is not None:
            return ValueError("Both min and max bitrate required")
        if self.bitrate_max is None and self.bitrate_min is not None:
            return ValueError("Both min and max bitrate required")
        mask = 0
        if self.name is not None:
            mask |= 1 << 0
        if self.url is not None:
            mask |= 1 << 1
        if self.key is not None:
            mask |= 1 << 2
        if self.bitrate_min is not None:
            mask |= 1 << 3

        name = self.name.encode() if self.name is not None else b''
        url = self.url.encode() if self.url is not None else b''
        key = self.key.encode() if self.key is not None else b''

        data = struct.pack('>B 64s 512s 512s 3x II', mask, name, url, key,
                           self.bitrate_min or 0, self.bitrate_max or 0)
        return self._make_command('CRSS', data)


class StreamingAudioBitrateCommand(Command):
    """
    Implementation of the `STAB` command.  This sets the audio bitrate
    parameters for the stream and recording.  In practice this is always set
    to 128k min/max bitrate.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      4    u32    Minimum bitrate (Bps)
    4      4    u32    Maximum bitrate (Bps)
    ====== ==== ====== ===========
    """

    def __init__(self, bitrate_min, bitrate_max):
        """
        :param bitrate_min: Minimum bitrate
        :param bitrate_max: Maximum bitrate
        """
        self.bitrate_min = bitrate_min
        self.bitrate_max = bitrate_max

    def get_command(self):
        data = struct.pack('>II', self.bitrate_min, self.bitrate_max)
        return self._make_command('STAB', data)


class StreamingStatusSetCommand(Command):
    """
    Implementation of the `StrR` command. This starts and stops the live
    stream.  It is equivalent to pressing ON AIR in the Live Stream settings
    of the Output menu of the UI Switcher panel.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    bool   Stream enable
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, streaming):
        """
        :param streaming: True starts streaming; False stops
        """
        self.streaming = streaming

    def get_command(self):
        data = struct.pack('>?xxx', self.streaming)
        return self._make_command('StrR', data)


class MultiviewPropertiesCommand(Command):
    """
    Implementation of the `CMvP` command. This sets the layout of a multiview
    output and can set a flag for swapping the program and preview window.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask, see table below
    1      1    u8     Multiviewer index
    2      1    u8     Layout
    3      1    bool   Swap program/preview
    ====== ==== ====== ===========

    See the MultiviewPropertiesField for the description of the layout value.

    """

    def __init__(self, index, layout=None, swap=None):
        """
        :param index: 0-indexed multiview output number
        :param layout: The layout number to use
        :param swap: Make the program/preview swapped or not
        """
        self.index = index
        self.layout = layout
        self.swap = swap

    def get_command(self):
        mask = 0
        if self.layout is not None:
            mask |= 1 << 0
        if self.swap is not None:
            mask |= 1 << 1

        layout = 0 if self.layout is None else self.layout
        swap = False if self.swap is None else self.swap
        data = struct.pack('>BBB?', mask, self.index, layout, swap)
        return self._make_command('CMvP', data)


class MultiviewInputCommand(Command):
    """
    Implementation of the `CMvI` command.  This routes a source to one of the windows in a multiview output.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Multiviewer index
    1      1    u8     Window index
    2      2    u16    Source index
    ====== ==== ====== ===========

    """

    def __init__(self, index, window, source):
        """
        :param index: 0-indexed multiview output number
        :param window: The window number on the multiview
        :param source: The source index index to route to the window
        """
        self.index = index
        self.window = window
        self.source = source

    def get_command(self):
        data = struct.pack('>BBH', self.index, self.window, self.source)
        return self._make_command('CMvI', data)


class LockCommand(Command):
    """
    Implementation of the `LOCK` command.  This requests a new lock, used for
    data transfers.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u16    Store index
    2      1    bool   Lock state
    3      1    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, store, state):
        """
        :param store: Store number to get the lock for
        :param state: True requests the lock; False releases it
        """
        self.store = store
        self.state = state

    def get_command(self):
        data = struct.pack('>H?x', self.store, self.state)
        return self._make_command('LOCK', data)


class PartialLockCommand(Command):
    """
    Implementation of the `PLCK` command.  This requests a new lock, used for
    data transfers.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u16    Store index
    2      2    u16    Slot
    4      4    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, store, slot):
        """
        :param store: Store number to request a lock for
        :param slot: Slot number inside the store to get the partial lock for
        """
        self.store = store
        self.slot = slot

    def get_command(self):
        data = struct.pack('>HH BBxx', self.store, self.slot, 0xff, 0x01)
        return self._make_command('PLCK', data)


class TransferDownloadRequestCommand(Command):
    """
    Implementation of the `FTSU` command. This requests download from the
    switcher to the client.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Tranfer id
    2      2    u16    Store
    4      2    u16    Slot
    8      4    u32    unknown
    ====== ==== ====== ===========

    """

    def __init__(self, transfer, store, slot):
        """
        :param transfer: Unique transfer number
        :param store: Store index
        :param slot: Slot index
        """
        self.transfer = transfer
        self.store = store
        self.slot = slot

    def get_command(self):
        # Special case for macro downloads, not sure why
        if self.store == 0xffff:
            u1 = 0x03
        else:
            u1 = 0x00
        u2 = 0xd0
        u3 = 0x9b
        u4 = 0x8c
        data = struct.pack('>HHI 4B', self.transfer, self.store, self.slot, u1, u2, u3, u4)
        return self._make_command('FTSU', data)


class TransferUploadRequestCommand(Command):
    """
    Implementation of the `FTSD` command. This requests an upload from the
    client to the switcher.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Tranfer id
    2      2    u16    Store
    4      2    ?      unknown
    6      2    u16    Slot
    8      2    u32    Data length
    12     2    u16    Mode bit field
    14     2    ?      unknown
    ====== ==== ====== ===========

    === ==========
    bit Mode
    === ==========
    0   Unknown
    1   Write RLE
    2   Clear
    256 Write uncompressed
    512 Pre-erase
    === ==========

    """

    MODE_WRITE_RLE = 1
    MODE_WRITE = 256
    MODE_ERASE = 512

    def __init__(self, transfer, store, slot, length, mode):
        """
        :param transfer: Unique transfer number
        :param store: Store index
        :param slot: Slot index
        :param length:
        :param mode:
        """
        self.transfer = transfer
        self.store = store
        self.slot = slot
        self.length = length
        self.mode = mode

    def get_command(self):
        data = struct.pack('>HHxxHIHxx', self.transfer, self.store, self.slot, self.length, self.mode)
        return self._make_command('FTSD', data)


class TransferDataCommand(Command):
    """
    Implementation of the `FTDa` command.  This is a chunk of data being sent
    from the client to the hardware.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Tranfer id
    2      2    u16    Size
    4      ?    u8[]   Chunk of data
    ====== ==== ====== ===========

    """

    def __init__(self, transfer, data):
        """
        :param transfer: Unique transfer number
        :param data: Chunk of data to send
        """
        self.transfer = transfer
        self.data = data

    def get_command(self):
        data = struct.pack('>HH', self.transfer, len(self.data))
        return self._make_command('FTDa', data + self.data)


class TransferFileDataCommand(Command):
    """
    Implementation of the `FTFD` command.  This is the file metadata for the
    uploaded file and should be sent to the hardware after all the FTDa
    commands have been sent. It contains the name and MD5 hash of the uploaded
    data.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Tranfer id
    2      64   str    Name
    66     128  str    Description
    194    16   u8[]   MD5 hash
    210    2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, transfer, hash, name=None, description=None):
        """
        :param transfer: Unique transfer number
        :param hash: Data MD5 hash
        :param name: Filename
        :param description: File description
        """
        self.transfer = transfer
        self.name = name
        self.description = description
        self.hash = hash

    def get_command(self):
        name = self.name.encode() if self.name is not None else b''
        description = self.description.encode() if self.description is not None else b''

        data = struct.pack('>H 64s 128s 16s 2x', self.transfer, name, description, self.hash)
        return self._make_command('FTFD', data)


class TransferAckCommand(Command):
    """
    Implementation of the `FTUA` command.  This is an acknowledgement for FTDa
    packets.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Tranfer id
    2      2    u16    Slot
    ====== ==== ====== ===========

    """

    def __init__(self, transfer, slot):
        """
        :param transfer: Unique transfer number
        :param slot: Slot index
        """
        self.transfer = transfer
        self.slot = slot

    def get_command(self):
        data = struct.pack('>HH', self.transfer, self.slot)
        return self._make_command('FTUA', data)


class SendAudioLevelsCommand(Command):
    """
    Implementation of the `SALN` command.  This is an acknowledgement for FTDa
    packets.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    bool   Enable sending levels
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, enable):
        """
        :param enable:
        """
        self.enable = enable

    def get_command(self):
        data = struct.pack('>? 3x', self.enable)
        return self._make_command('SALN', data)


class SendFairlightLevelsCommand(Command):
    """
    Implementation of the `SFLN` command.  This enables or disables receiving
    the 10Hz audio level update packets for the Audio page.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    bool   Enable sending levels
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, enable):
        """
        :param enable: Enable or disable receiving audio level data
        """
        self.enable = enable

    def get_command(self):
        data = struct.pack('>? 3x', self.enable)
        return self._make_command('SFLN', data)


class CameraControlCommand(Command):
    """
    Implementation of the `CCmd` command.  This sends a camera control command
    to an attached blackmagic design camera.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    int8   Destination
    1      1    int8   Category
    2      1    int8   Parameter
    3      1    int8   Relative 0=off 1=on
    4      1    int8   Datatype
    5      4    ?      unknown
    8      1    int8   Number of elements
    9      6    ?      unknown
    ...    ...  ...    data depending on command
    ====== ==== ====== ===========
    """

    def __init__(self, destination, category, parameter, relative=False, datatype=None, data=None):
        """
        :param destination: Camera index, or 255 for broadcast
        :param category: Command category
        :param parameter: Command parameter
        :param relative: Relative adjustment enable
        :param datatype: Data type index
        :param data: Data elements for command
        """
        self.destination = destination
        self.category = category
        self.parameter = parameter
        self.relative = relative
        self.datatype = datatype if datatype is not None else 0
        self.data = data

    def get_command(self):
        count = 0
        if self.data is not None:
            count = len(self.data)
        data = struct.pack('>5B', self.destination, self.category, self.parameter, self.relative, self.datatype)
        elements = [0] * 11
        countoffset = {
            0: 2,
            1: 2,
            2: 2,
            3: 4,
            4: 2,
            5: 2,
            128: 4
        }
        elements[countoffset[self.datatype]] = count
        data += struct.pack('>11B', *elements)
        if self.data is not None:
            fmt = '>{}'.format(count)
            if self.datatype == 5:
                fmt = '>{}s'.format(len(self.data[0]))
            fmtmap = {
                0: '?',
                1: 'b',
                2: 'h',
                3: 'i',
                4: 'q',
                5: '',
                128: 'h'
            }
            fmt += fmtmap[self.datatype]
            if self.datatype == 128:
                for i in range(0, len(self.data)):
                    self.data[i] = int(self.data[i] * (2 ** 11))
            elif self.datatype == 5:
                for i in range(0, len(self.data)):
                    self.data[i] = self.data[i].encode()
            packed_data = struct.pack(fmt, *self.data)
            packed_data += b'\0' * (8 - len(packed_data))
            data += packed_data
        return self._make_command('CCmd', data)


class VideoModeCommand(Command):
    """
    Implementation of the `CVdM` command.  This sets the main video mode for
    the hardware.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     M/E index
    1      1    bool   Preview enabled
    2      2    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, mode):
        """
        :param mode: Video mode ID
        """
        self.mode = mode

    def get_command(self):
        data = struct.pack('>B3x', self.mode)
        return self._make_command('CVdM', data)


class AutoInputVideoModeCommand(Command):
    """
    Implementation of the `AiVM` command.  This enables or disables automatic
    video mode detection based on the video signal.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    bool   Enable
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, enable):
        """
        :param enable: Set auto mode enabled or disabled
        """
        self.enable = enable

    def get_command(self):
        data = struct.pack('>?3x', self.enable)
        return self._make_command('AiVM', data)


class InputPropertiesCommand(Command):
    """
    Implementation of the `CInL` command.  This sets labels and routing for
    video inputs.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    ?      unknown
    2      2    u16    Source index
    4      20   str[]  Label
    24     4    str[]  Short label
    28     2    u16    Port type
    ====== ==== ====== ===========

    """

    def __init__(self, source_index, label=None, short_label=None, port_type=None):
        """
        :param source_index: Input index to change the properties for
        :param label: Long display label for the input
        :param short_label: Short display label for the button of the input, 4 letter code
        :param port_type: Port type enum
        """
        self.source_index = source_index
        self.label = label
        self.short_label = short_label
        self.port_type = port_type

    def get_command(self):
        mask = 0
        if self.label is not None:
            mask |= 1 << 0
        if self.short_label is not None:
            mask |= 1 << 1
        if self.port_type is not None:
            mask |= 1 << 2

        label = self.label.encode() if self.label is not None else b''
        short = self.short_label.encode() if self.short_label is not None else b''
        port_type = self.port_type if self.port_type is not None else 0

        data = struct.pack('>Bx H 20s 4s Hxx', mask, self.source_index, label, short, port_type)
        return self._make_command('CInL', data)


class TimeRequestCommand(Command):
    """
    Request the system time of the hardware, this is also used as a NOP command.
    This command has no arguments.
    """

    def get_command(self):
        return self._make_command('TiRq', b'')


class SetTimeOfDayCommand(Command):
    """
    Set the time of day on the hardware.  Used my ATEMSC at startup to set the
    ATEM to the system clock of the computer.
    """

    def __init__(self, time):
        if not isinstance(time, datetime.datetime):
            raise ValueError()
        self.time = time

    def get_command(self):
        timestamp = self.time.timestamp()
        data = struct.pack('>I4x', timestamp)
        return self._make_command('SToD', data)


class TransferCompleteCommand(Command):
    """
    Implementation of the `*XFC` command.  This command is part of the
    OpenSwitcher TCP protocol and not part of the actual ATEM protocol.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Store
    2      2    u16    Slot
    4      1    bool   Is upload
    5      3    x      padding
    ====== ==== ====== ===========

    """

    def __init__(self, store, slot, upload):
        """
        :param store: Transfer store index
        :param slot: Transfer slot index
        :param upload: True if an upload was completed; False if a download
                       was completed
        """
        self.store = store
        self.slot = slot
        self.upload = upload

    def get_command(self):
        data = struct.pack('>HH ?xxx', self.store, self.slot, self.upload)
        return self._make_command('*XFC', data)


class SupersourcePropertiesCommand(Command):
    """
    Implementation of the `CSSc` command.  This sets global options for a SuperSource element like the Art sources.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    u8     Mask
    1      1    u8     SuperSource index
    2      2    u16    Artwork fill source
    4      2    u16    Artwork key source
    6      1    u8     Artwork layer [0=background, 1=foreground]
    7      1    bool   Premultiplied
    8      2    u16    Keyer clip [0-1000]
    10     2    u16    Keyer gain [0-1000]
    12     1    bool   Keyer invert
    13     3    ?      padding
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Fill source
    1   Key source
    2   Artwork layer
    3   Premultiplied
    4   Clip
    5   Gain
    6   Invert key
    === ==========

    """

    def __init__(self, index, fill_source=None, key_source=None, layer=None, premultiplied=None, clip=None, gain=None,
                 invert=None):
        """
        :param index: 0-indexed SuperSource number
        :param fill_source: Artwork fill source
        :param key_source: Artwork key source
        :param layer: Whether the artwork is on the foreground or background
        :param premultiplied: Use premultiplied alpha in the keyer
        :param clip: Clipping value for the luma keyer
        :param gain: Gain value for the luma keyer
        :param invert: Invert the keyer result
        """
        self.index = index
        self.fill = fill_source
        self.key = key_source
        self.layer = layer
        self.premultiplied = premultiplied
        self.clip = clip
        self.gain = gain
        self.invert = invert

    def get_command(self):
        mask = 0
        if self.fill is not None:
            mask |= 1 << 0
        if self.key is not None:
            mask |= 1 << 1
        if self.layer is not None:
            mask |= 1 << 2
        if self.premultiplied is not None:
            mask |= 1 << 3
        if self.clip is not None:
            mask |= 1 << 4
        if self.gain is not None:
            mask |= 1 << 5
        if self.invert is not None:
            mask |= 1 << 6

        fill = self.fill or 0
        key = self.key or 0
        layer = self.layer or 0
        premultiplied = self.premultiplied or False
        clip = self.clip or 0
        gain = self.gain or 0
        invert = self.invert or False

        data = struct.pack('>BBH HB? HH ?xxx', mask, self.index, fill, key, layer, premultiplied, clip, gain, invert)

        return self._make_command('CSSc', data)


class SupersourceBoxPropertiesCommand(Command):
    """
    Implementation of the `CSBP` command.  This sets the properties on a single box in the SuperSource element.
    Every SuperSource will have four of these boxes to control.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Mask
    2      1    u8     SuperSource index
    3      1    u8     Box index
    4      1    bool   Enabled
    5      1    ?      padding
    6      2    u16    Source index
    8      2    i16    Position X [-4800 - 4800]
    10     2    i16    Position Y [-3400 - 3400]
    12     2    u16    Size [70 - 1000]
    14     1    bool   Crop enable
    15     1    ?      padding
    16     2    u16    Crop top [0 - 18000]
    18     2    u16    Crop bottom [0 - 18000]
    20     2    u16    Crop left [0 - 32000]
    22     2    u16    Crop right [0 - 32000]
    ====== ==== ====== ===========

    === ==========
    Bit Mask value
    === ==========
    0   Enabled
    1   Source
    2   X
    3   Y
    4   Size
    5   Mask enable
    6   Mask top
    7   Mask bottom
    8   Mask left
    9   Mask right
    === ==========

    """

    def __init__(self, index, box, enabled=None, source=None, x=None, y=None, size=None, masked=None, top=None,
                 bottom=None,
                 left=None, right=None):
        """
        :param index: 0-indexed M/E number to control the preview bus of
        :param box:
        :param enabled:
        :param source:
        :param x:
        :param y:
        :param size:
        :param masked:
        :param top:
        :param bottom:
        :param left:
        :param right:
        """
        self.index = index
        self.box = box
        self.enabled = enabled
        self.source = source
        self.x = x
        self.y = y
        self.size = size
        self.masked = masked
        self.top = top
        self.bottom = bottom
        self.left = left
        self.right = right

    def get_command(self):
        mask = 0
        if self.enabled is not None:
            mask |= 1 << 0
        if self.source is not None:
            mask |= 1 << 1
        if self.x is not None:
            mask |= 1 << 2
        if self.y is not None:
            mask |= 1 << 3
        if self.size is not None:
            mask |= 1 << 4
        if self.masked is not None:
            mask |= 1 << 5
        if self.top is not None:
            mask |= 1 << 6
        if self.bottom is not None:
            mask |= 1 << 7
        if self.left is not None:
            mask |= 1 << 8
        if self.right is not None:
            mask |= 1 << 9

        enabled = False if self.enabled is None else self.enabled
        source = 0 if self.source is None else self.source
        x = 0 if self.x is None else self.x
        y = 0 if self.y is None else self.y
        size = 0 if self.size is None else self.size
        masked = False if self.masked is None else self.masked
        top = 0 if self.top is None else self.top
        bottom = 0 if self.bottom is None else self.bottom
        left = 0 if self.left is None else self.left
        right = 0 if self.right is None else self.right

        data = struct.pack('>HBB ?xH hh H?x HHHH', mask, self.index, self.box, enabled, source, x, y, size, masked, top,
                           bottom, left, right)

        return self._make_command('CSBP', data)


class MacroRecordCommand(Command):
    """
    Implementation of the `MSRc` command.  This triggers recording of a macro.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Macro index
    2      2    u16    Name length
    4      2    u16    Description length
    6      ?    char[] Name
    ?      ?    char[] Description
    ====== ==== ====== ===========

    """

    def __init__(self, index, name, description):
        """
        :param index:
        :param name:
        :param description:
        """
        self.index = index
        self.name = name
        self.description = description

    def get_command(self):
        data = struct.pack('>HHH', self.index, len(self.name), len(self.description))
        data += self.name.encode('ascii')
        data += self.description.encode('ascii')

        alignment = len(data) % 4
        if alignment != 0:
            padding = 4 - alignment
            data += b'\0' * padding
        return self._make_command('MSRc', data)


class MacroActionCommand(Command):
    """
    Implementation of the `MAct` command.  This performs special actions while
    recording a macro.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Macro index
    2      1    u8     Action
    3      1    ?      padding
    ====== ==== ====== ===========

    """
    ACTION_RUN = 0
    ACTION_STOP = 1
    ACTION_STOP_RECORD = 2
    ACTION_INSERT_USER_WAIT = 3
    ACTION_CONTINUE = 4
    ACTION_DELETE = 5

    def __init__(self, action, index=None):
        """
        :param action:
        :param index:
        """
        self.index = index if index is not None else 0xFFFF
        self.action = action

    def get_command(self):
        data = struct.pack('>HBx', self.index, self.action)
        return self._make_command('MAct', data)


class SaveStartupStateCommand(Command):
    """
    Implementation of the `SRsv` command.  This is equivalent to pressing the "Save startup state" button
    in the official application.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      4    ?      padding?
    ====== ==== ====== ===========

    """

    def __init__(self):
        pass

    def get_command(self):
        data = struct.pack('>xxxx')
        return self._make_command('SRsv', data)


class ClearStartupStateCommand(Command):
    """
    Implementation of the `SRcl` command.  This is equivalent to pressing the "Clear startup state" button
    in the official application.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      4    ?      padding?
    ====== ==== ====== ===========

    """

    def __init__(self):
        pass

    def get_command(self):
        data = struct.pack('>xxxx')
        return self._make_command('SRcl', data)
