# -*- coding: UTF-8 -*-

#  Copyright (C) 2019 Parrot Drones SAS
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions
#  are met:
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in
#    the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the Parrot Company nor the names
#    of its contributors may be used to endorse or promote products
#    derived from this software without specific prior written
#    permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
#  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
#  PARROT COMPANY BE LIABLE FOR ANY DIRECT, INDIRECT,
#  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
#  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
#  OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
#  AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
#  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
#  SUCH DAMAGE.


from __future__ import unicode_literals
from __future__ import print_function
from __future__ import absolute_import
from future.builtins import str, bytes
from future.builtins import int
try:
    # Python 2
    from __builtin__ import str as builtin_str
except ImportError:
    # Python 3
    from builtins import str as builtin_str

import arsdkparser
import ctypes
try:
    # Python 2
    import textwrap3 as textwrap
except ImportError:
    # Python 3
    import textwrap

from aenum import OrderedEnum
from collections import OrderedDict
from itertools import starmap
import logging
import olympe_deps as od
import re
from six import with_metaclass

from olympe.arsdkng.enums import ArsdkEnums, ArsdkEnum, list_flags, ArsdkBitfield
from olympe.arsdkng.expectations import ArsdkEventExpectation
from olympe.arsdkng.expectations import ArsdkCommandExpectation
from olympe.arsdkng.expectations import ArsdkWhenAnyExpectation
from olympe.arsdkng.expectations import ArsdkWhenAllExpectations
from olympe.arsdkng.expectations import ArsdkCheckStateExpectation
from olympe.arsdkng.expectations import ArsdkCheckWaitStateExpectation
from olympe.arsdkng.expectations import ExpectPolicy
from olympe.arsdkng.events import ArsdkMessageEvent

from olympe._private import string_from_arsdkxml, DEFAULT_FLOAT_TOL


ARSDK_CLS_DEFAULT_ID = 0

#########################################################################
#                        MAPPING SDK COMMANDS to OLYMPE COMMANDS        #
#########################################################################

ARSDK_OLYMPE_CMD_MAP = {
    "Ardrone3.Animations.Flip": "animation_flip",
    "Ardrone3.Antiflickering.ElectricFrequency": "set_antiflickering_frequency",
    "Ardrone3.Antiflickering.SetMode": "set_antiflickering_mode",
    "Ardrone3.Camera.OrientationV2": "set_camera_orientation_v2",
    "Ardrone3.Camera.Velocity": "set_camera_velocity",
    "Ardrone3.GPSSettings.HomeType": "set_home_type_location",
    "Ardrone3.GPSSettings.ReturnHomeDelay": "set_return_home_delay",
    "Ardrone3.GPSSettings.ReturnHomeMinAltitude": "set_return_home_altitude",
    "Ardrone3.GPSSettings.SendControllerGPS": "set_controller_gps_location",
    "Ardrone3.MediaRecord.PictureV2": "take_picture_v2",

    # "Ardrone3.MediaRecord.VideoV2": ["stop_video_v2", "start_video_v2"],
    # "Ardrone3.MediaStreaming.VideoEnable": ["disable_video_streaming", "enable_video_streaming"],

    "Ardrone3.MediaStreaming.VideoStreamMode": "set_stream_mode",
    "Ardrone3.Network.WifiAuthChannel": "get_wifi_auth_channels",
    "Ardrone3.Network.WifiScan": "wifi_scan",
    # FIXME: set_wifi_security is mapped to two different commands
    "Ardrone3.NetworkSettings.WifiSecurity": "set_wifi_security",
    "Ardrone3.NetworkSettings.WifiSelection": "set_wifi_settings",
    "Ardrone3.PictureSettings.AutoWhiteBalanceSelection": "set_picture_white_balance",
    "Ardrone3.PictureSettings.ExpositionSelection": "set_picture_exposition",
    "Ardrone3.PictureSettings.PictureFormatSelection": "set_picture_format",
    "Ardrone3.PictureSettings.SaturationSelection": "set_picture_saturation",

    # "Ardrone3.PictureSettings.TimelapseSelection":
    #   ["disable_picture_timelapse","enable_picture_timelapse"],
    # "Ardrone3.PictureSettings.VideoAutorecordSelection":
    #  ["disable_autorecord_video","enable_autorecord_video"],

    "Ardrone3.PictureSettings.VideoFramerate": "set_video_framerate",
    "Ardrone3.PictureSettings.VideoRecordingMode": "set_video_recording_mode",
    "Ardrone3.PictureSettings.VideoResolutions": "set_resolutions_mode",
    "Ardrone3.PictureSettings.VideoStabilizationMode": "set_video_stabilization_mode",

    "Ardrone3.Piloting.CancelMoveTo": "piloting_cancel_move_to",
    "Ardrone3.Piloting.Circle": "piloting_circle",
    "Ardrone3.Piloting.Emergency": "emergency",
    "Ardrone3.Piloting.FlatTrim": "flat_trim",
    "Ardrone3.Piloting.Landing": "landing",
    "Ardrone3.Piloting.MoveBy": "piloting_move_by",
    "Ardrone3.Piloting.MoveTo": "piloting_move_to",

    # "Ardrone3.Piloting.NavigateHome": ["stop_piloting_return_home", "start_piloting_return_home"],

    "Ardrone3.Piloting.StartPilotedPOI": "start_piloted_poi",
    "Ardrone3.Piloting.StopPilotedPOI": "stop_piloted_poi",
    "Ardrone3.Piloting.TakeOff": "take_off",
    "Ardrone3.Piloting.UserTakeOff": "set_user_take_off_state",

    # "Ardrone3.PilotingSettings.BankedTurn": ["disable_banked_turn", "enable_banked_turn" ],

    "Ardrone3.PilotingSettings.CirclingAltitude": "set_circling_altitude",
    "Ardrone3.PilotingSettings.CirclingDirection": "set_default_circling_direction",
    "Ardrone3.PilotingSettings.MaxAltitude": "set_max_altitude",
    "Ardrone3.PilotingSettings.MaxDistance": "set_max_distance",
    "Ardrone3.PilotingSettings.MaxTilt": "set_max_tilt",
    "Ardrone3.PilotingSettings.MinAltitude": "set_min_altitude",
    "Ardrone3.PilotingSettings.NoFlyOverMaxDistance": "set_no_fly_over_max_distance",
    "Ardrone3.PilotingSettings.PitchMode": "set_pitch_mode",
    "Ardrone3.PilotingSettings.SetAutonomousFlightMaxHorizontalAcceleration":
        "set_flightplan_max_horizontal_acceleration",

    "Ardrone3.PilotingSettings.SetAutonomousFlightMaxHorizontalSpeed": "",
        # ["set_max_horizontal_speed","set_flightplan_max_horizontal_speed"],

    "Ardrone3.PilotingSettings.SetAutonomousFlightMaxRotationSpeed":
        "set_flightplan_max_rotation_speed",
    "Ardrone3.PilotingSettings.SetAutonomousFlightMaxVerticalAcceleration":
        "set_flightplan_max_vertical_acceleration",
    "Ardrone3.PilotingSettings.SetAutonomousFlightMaxVerticalSpeed":
        "set_flightplan_max_vertical_speed",

    "Ardrone3.SpeedSettings.HullProtection": "set_hull_protection",
    "Ardrone3.SpeedSettings.MaxPitchRollRotationSpeed": "set_max_pitch_roll_rot_speed",
    "Ardrone3.SpeedSettings.MaxRotationSpeed": "set_max_rotation_speed",
    "Ardrone3.SpeedSettings.MaxVerticalSpeed": "set_max_vertical_speed",
    "Animation.Cancel": "animation_cancel",
    "Animation.Start_candle": "animation_start_candle",
    "Animation.Start_dolly_slide": "animation_start_dolly_slide",
    "Animation.Start_dronie": "animation_start_dronie",
    "Animation.Start_flip": "animation_start_flip",
    "Animation.Start_horizontal_panorama": "animation_start_horizontal_panorama",
    "Animation.Start_horizontal_reveal": "animation_start_horizontal_reveal",
    "Animation.Start_parabola": "animation_start_parabola",
    "Animation.Start_spiral": "animation_start_spiral",
    "Animation.Start_vertical_reveal": "animation_start_vertical_reveal",

    "Camera.Unlock_exposure": "",

    "Common.Accessory.Config": "set_accessory_config",
    "Common.Animations.StartAnimation": "start_animation",
    "Common.Animations.StopAllAnimations": "stop_all_animations",
    "Common.Animations.StopAnimation": "stop_animation",

    # "Common.Calibration.MagnetoCalibration":
    #   ["aborted_calibration","start_calibration"],
    # "Common.Calibration.PitotCalibration":
    #   ["aborted_calibration_pitot", "start_calibration_pitot"],

    "Common.Common.AllStates": "get_all_states",

    "Common.Common.Reboot": "reboot",
    "Common.Controller.IsPiloting": "change_hud_state",


    "Common.FlightPlanSettings.ReturnHomeOnDisconnect": "set_rth_during_flightplan",

    "Common.Mavlink.Pause": "mavlink_pause",
    "Common.Mavlink.Start": "mavlink_start",
    "Common.Mavlink.Stop": "mavlink_stop",
    "Common.Settings.AllSettings": "get_all_settings",
    "Common.Settings.AutoCountry": "set_network_auto_country",
    "Common.Settings.Country": "set_network_country_code",
    "Common.Settings.ProductName": "set_product_name",
    "Common.Settings.Reset": "reset_all_settings",
    "Common.WifiSettings.OutdoorSetting": "set_wifi_settings_outdoor",

    "Follow_me.Configure_geographic": "follow_me_configure_geographic_run",
    "Follow_me.Configure_relative": "follow_me_configure_relative_run",
    "Follow_me.Start": "follow_me_start",
    "Follow_me.Stop": "follow_me_stop",
    "Follow_me.Target_framing_position": "follow_me_target_framing_position",
    "Follow_me.Target_image_detection": "follow_me_target_image_detection",

    "Skyctrl.AccessPointSettings.AccessPointSSID": "controller_set_wifi_ap_ssid",

    "Skyctrl.AccessPointSettings.WifiSelection": "controller_set_wifi_ap_settings",
    "Skyctrl.AxisFilters.DefaultAxisFilters": "controller_set_default_axis_filters",
    "Skyctrl.AxisFilters.GetCurrentAxisFilters": "controller_current_axis_filters",
    "Skyctrl.AxisFilters.SetAxisFilter": "controller_set_axis_filter",
    "Skyctrl.AxisMappings.DefaultAxisMapping": "controller_set_default_axis_mapping",
    "Skyctrl.AxisMappings.GetAvailableAxisMappings": "controller_available_axis_mappings",
    "Skyctrl.AxisMappings.GetCurrentAxisMappings": "controller_current_axis_mappings",
    "Skyctrl.AxisMappings.SetAxisMapping": "controller_set_axis_mapping",

    "Skyctrl.ButtonMappings.DefaultButtonMapping": "controller_set_default_button_mapping",
    "Skyctrl.ButtonMappings.GetAvailableButtonMappings": "controller_available_button_mappings",
    "Skyctrl.ButtonMappings.GetCurrentButtonMappings": "controller_current_button_mappings",
    "Skyctrl.ButtonMappings.SetButtonMapping": "controller_set_button_mapping",

    "Skyctrl.CoPiloting.SetPilotingSource": "set_piloting_source_controller",

    "Skyctrl.Common.AllStates": "get_controller_states",

    "Thermal_cam.Activate": "activate_camera_thermal",
    "Thermal_cam.Deactivate": "deactivate_camera_thermal",
    "Thermal_cam.Set_sensitivity": "set_camera_thermal_sensitivity",

    # FIXME: set_wifi_security is mapped to two different commands
    "Wifi.Set_security": "set_wifi_security",
    "Wifi.Update_authorized_channels": "",  # "get_wifi_auth_channels",
}

DEFAULT_TIMEOUT = 10
TIMEOUT_BY_COMMAND = {
    "Animation.Cancel": 5,
    "Animation.Start_candle": 5,
    "Animation.Start_dolly_slide": 5,
    "Animation.Start_dronie": 5,
    "Animation.Start_flip": 5,
    "Animation.Start_horizontal_panorama": 5,
    "Animation.Start_horizontal_reveal": 5,
    "Animation.Start_parabola": 5,
    "Animation.Start_spiral": 5,
    "Animation.Start_vertical_reveal": 5,
    "Ardrone3.Animations.Flip": 5,
    "Ardrone3.Antiflickering.ElectricFrequency": 5,
    "Ardrone3.Antiflickering.SetMode": 5,
    "Ardrone3.Camera.OrientationV2": 20,
    "Ardrone3.GPSSettings.HomeType": 20,
    "Ardrone3.GPSSettings.ReturnHomeDelay": 20,
    "Ardrone3.GPSSettings.ReturnHomeMinAltitude": 20,
    "Ardrone3.MediaRecord.PictureV2": 20,
    "Ardrone3.MediaRecord.VideoV2": 15,
    "Ardrone3.MediaStreaming.VideoEnable": 3,
    "Ardrone3.PictureSettings.ExpositionSelection": 20,
    "Ardrone3.PictureSettings.PictureFormatSelection": 20,
    "Ardrone3.Piloting.CancelMoveTo": 5,
    "Ardrone3.Piloting.Emergency": 10,
    "Ardrone3.Piloting.FlatTrim": 5,
    "Ardrone3.Piloting.MoveBy": 20,
    "Ardrone3.Piloting.MoveTo": 20,
    "Ardrone3.Piloting.NavigateHome": 7,
    "Ardrone3.Piloting.StartPilotedPOI": 5,
    "Ardrone3.Piloting.StopPilotedPOI": 5,
    "Ardrone3.PilotingSettings.CirclingAltitude": 3,
    "Ardrone3.PilotingSettings.CirclingDirection": 3,
    "Ardrone3.PilotingSettings.MaxAltitude": 20,
    "Ardrone3.PilotingSettings.MinAltitude": 20,
    "Ardrone3.PilotingSettings.PitchMode": 3,
    "Common.Calibration.MagnetoCalibration": 3,
    "Common.Calibration.PitotCalibration": 3,
    "Common.FlightPlanSettings.ReturnHomeOnDisconnect": 20,
    "Common.Mavlink.Pause": 20,
    "Common.Mavlink.Start": 20,
    "Common.Mavlink.Stop": 20,
    "Generic.Default": 5,
    "Generic.SetDroneSettings": 5,
    "Thermal_cam.Activate": 5,
    "Thermal_cam.Deactivate": 5,
    "Thermal_cam.Set_sensitivity": 5,
}

FLOAT_TOLERANCE_BY_FEATURE = {
    "gimbal": (1e-1, 1e-1)  # yaw/pitch/roll angles in degrees
}


class ArsdkMessageMeta(type):

    _base = None

    def __new__(mcls, *args, **kwds):
        """
        ArsdkMessage constructor
        @type obj: arsdkparser.ArMsg
        @param name_path: the full xml path of the message as a list of names
            ["<feature_name>", "<class_name>" or None, "message_name"]
        @param id_path: the full xml path of the message as a list ids
            [<feature_id>, <class_id> or None, message_id]
        @type enums: olympe.arsdkng.ArsdkEnums
        """
        if mcls._base is None:
            cls = type.__new__(mcls, *args, **kwds)
            mcls._base = cls
            return cls

        obj, name_path, id_path, enums = args

        fullPath = tuple(filter(None, name_path))
        fullName = '.'.join(fullPath)

        cls = type.__new__(mcls, builtin_str(fullName), (mcls._base,), {})

        cls.fullName = fullName
        cls.prefix = fullPath[:-1]
        cls.FULL_NAME = '_'.join(fullPath).upper()
        cls.Full_Name = '_'.join((name[0].upper() + name[1:] for name in fullPath))
        cls.FullName = '.'.join((name[0].upper() + name[1:] for name in fullPath))

        cls.obj = obj
        cls.name_path = name_path
        cls.id_path = id_path

        cls.args_pos = OrderedDict()
        cls.args_enum = OrderedDict()
        cls.args_bitfield = OrderedDict()

        cls.callback_type = ArsdkMessageCallbackType.from_arsdk(cls.obj.listType)
        cls.message_type = ArsdkMessageType.from_arsdk(type(cls.obj))
        cls.buffer_type = ArsdkMessageBufferType.from_arsdk(cls.obj.bufferType)

        cls.loglevel = logging.INFO
        if (cls.message_type is ArsdkMessageType.EVT and
           cls.buffer_type is not ArsdkMessageBufferType.ACK):
            # Avoid being flooded by spontaneous event messages sent by the drone
            cls.loglevel = logging.DEBUG
        elif cls.fullName in \
            ("ardrone3.PilotingState.AltitudeChanged",
             "ardrone3.PilotingState.AltitudeAboveGroundChanged",
             "ardrone3.PilotingState.AttitudeChanged",
             "ardrone3.PilotingState.GpsLocationChanged",
             "ardrone3.PilotingState.PositionChanged",
             "ardrone3.PilotingState.SpeedChanged",
             "skyctrl.SkyControllerState.AttitudeChanged",
             "mapper.button_mapping_item",
             "mapper.axis_mapping_item",
             "mapper.expo_map_item",
             "mapper.inverted_map_item",):
            cls.loglevel = logging.DEBUG

        cls.feature_name = name_path[0]
        cls.FeatureName = cls.feature_name[0].upper() + cls.feature_name[1:]
        cls.class_name = name_path[1]
        if (cls.class_name is not None and
           ("state" in cls.class_name.lower() or "event" in cls.class_name.lower())):
            cls.message_type = ArsdkMessageType.EVT
        cls.name = name_path[2]

        cls.feature_id = id_path[0]
        cls.class_id = id_path[1] or ARSDK_CLS_DEFAULT_ID
        cls.msg_id = id_path[2]

        cls.id = cls.feature_id << 24 | cls.class_id << 16 | cls.msg_id
        cls.id_name = '-'.join(map(str, filter(lambda x: x is not None, cls.id_path)))

        # build a list of olympe command name aliases
        cls.aliases = []
        if cls.FullName in ARSDK_OLYMPE_CMD_MAP.keys():
            mapped_value = ARSDK_OLYMPE_CMD_MAP[cls.FullName]
            if isinstance(mapped_value, str) and mapped_value is not "":
                cls.aliases = [cls.Full_Name, mapped_value]
            else:
                cls.aliases = [cls.Full_Name]

        elif not cls.FullName[0].isdigit():
            cls.aliases = [cls.Full_Name]

        # process arguments information
        for i, arg in enumerate(cls.obj.args):
            cls.args_pos[arg.name] = i
            if isinstance(arg.argType, arsdkparser.ArEnum):
                enum_name = arg.argType.name
                if cls.class_name is not None:
                    prefix = cls.class_name + "_"
                    if arg.argType.name.startswith(prefix):
                        enum_name = arg.argType.name[len(prefix):]
                cls.args_enum[arg.name] = enums[cls.FeatureName][enum_name]
            elif isinstance(arg.argType, arsdkparser.ArBitfield):
                cls.args_bitfield[arg.name] = \
                    enums[cls.FeatureName][arg.argType.enum.name]._bitfield_type_

        cls.args_name = [arg.name for arg in cls.obj.args]

        cls.key_name = None
        if cls.obj.listType == arsdkparser.ArCmdListType.MAP:
            cls.key_name = cls.obj.mapKey and cls.obj.mapKey.name or cls.obj.args[0].name
        elif "cam_id" in cls.args_name:
            # FIXME: workaround missing MAP_ITEMS in camera.xml
            cls.callback_type = ArsdkMessageCallbackType.MAP
            cls.key_name = "cam_id"
        elif ("list_flags" in cls.args_bitfield and
              cls.args_bitfield["list_flags"] == list_flags._bitfield_type_):
            cls.callback_type = ArsdkMessageCallbackType.LIST

        if cls.obj.args:
            cls.arsdk_type_args, cls.arsdk_value_attr, cls.encode_ctypes_args = map(list, zip(*(
                cls._ar_argtype_encode_type(ar_arg.argType)
                for ar_arg in cls.obj.args
            )))
        else:
            cls.arsdk_type_args, cls.arsdk_value_attr, cls.encode_ctypes_args = [], [], []

        cls.args_type = OrderedDict()
        for argname, ar_arg in zip(cls.args_name, cls.obj.args):
            cls.args_type[argname] = cls._ar_argtype_to_python(argname, ar_arg.argType)

        cls.timeout = TIMEOUT_BY_COMMAND.get(cls.FullName, DEFAULT_TIMEOUT)

        cls.float_tol = FLOAT_TOLERANCE_BY_FEATURE.get(cls.feature_name, DEFAULT_FLOAT_TOL)

        cls.send = None

        cls._expectation = None

        # Get information on callback ctypes arguments
        cls.arsdk_desc = od.arsdk_cmd_find_desc(od.struct_arsdk_cmd.bind({
            "prj_id": cls.feature_id,
            "cls_id": cls.class_id,
            "cmd_id": cls.msg_id,
        }))

        cls.decode_ctypes_args = []

        decode_ctypes_args_map = {
            od.ARSDK_ARG_TYPE_I8: ctypes.c_int8,
            od.ARSDK_ARG_TYPE_U8: ctypes.c_uint8,
            od.ARSDK_ARG_TYPE_I16: ctypes.c_int16,
            od.ARSDK_ARG_TYPE_U16: ctypes.c_uint16,
            od.ARSDK_ARG_TYPE_I32: ctypes.c_int32,
            od.ARSDK_ARG_TYPE_U32: ctypes.c_uint32,
            od.ARSDK_ARG_TYPE_I64: ctypes.c_int64,
            od.ARSDK_ARG_TYPE_U64: ctypes.c_uint64,
            od.ARSDK_ARG_TYPE_FLOAT: ctypes.c_float,
            od.ARSDK_ARG_TYPE_DOUBLE: ctypes.c_double,
            od.ARSDK_ARG_TYPE_STRING: ctypes.c_char_p,
            od.ARSDK_ARG_TYPE_ENUM: ctypes.c_int,
        }

        for i in range(cls.arsdk_desc.contents.arg_desc_count):
            arg_type = cls.arsdk_desc.contents.arg_desc_table[i].type
            cls.decode_ctypes_args.append(decode_ctypes_args_map[arg_type])

        # Fixup missing list_flags arguments for LIST_ITEM/MAP_ITEM messages
        if ("list_flags" not in cls.args_name) and (
            cls.message_type is ArsdkMessageType.EVT and
            cls.callback_type in (ArsdkMessageCallbackType.LIST, ArsdkMessageCallbackType.MAP)
        ):
            cls.args_pos["list_flags"] = len(cls.args_pos)
            cls.args_name.append("list_flags")
            cls.args_bitfield["list_flags"] = list_flags._bitfield_type_
            cls.args_type[argname] = int
            cls.decode_ctypes_args.append(ctypes.c_uint8)
            cls.encode_ctypes_args.append(ctypes.c_uint8)

        if cls.message_type is ArsdkMessageType.CMD:
            cls.args_default = ArsdkMessages._default_arguments.get(cls.FullName, OrderedDict())
        else:
            cls.args_default = OrderedDict(zip(cls.args_name, [None] * len(cls.args_name)))
        cls.args_default_str = ", ".join((
            "{}={}".format(argname, cls.args_default[argname])
            if argname in cls.args_default else argname
            for argname in cls.args_name + ['**kwds']
        ))

        # docstring
        cls.doc_todos = u""
        cls.docstring = cls._py_ar_cmd_docstring()
        cls.__doc__ = cls.docstring + "\n"

        cls.__call__ = cls._create_call()
        return cls

    def _py_ar_cmd_docstring(cls):
        """
        Returns a python docstring from an ArCmd object
        """
        docstring = u"\n\n".join(
            [cls.FullName] +
            [cls._py_ar_comment_docstring(
                cls.obj.doc,
                cls._py_ar_args_docstring(cls.obj.args),
                cls.obj.isDeprecated)]
        )
        return docstring

    def _py_ar_arg_directive(cls, directive, argname, doc):
        directive = u":{} {}: ".format(directive, argname)
        doc = u"{}{}".format(directive, doc)
        doc = textwrap.fill(
            doc,
            subsequent_indent=(' ' * len(directive)),
            break_long_words=False
        )
        return doc

    def _py_ar_args_docstring(cls, ar_args):
        if cls.message_type == ArsdkMessageType.CMD:
            extra_params_docstring = (
                "\n\n" +
                ":param _timeout: command message timeout (defaults to {})\n".format(cls.timeout) +
                ":type _timeout: int\n" +
                ":param _no_expect: if True for,do not expect the usual command expectation " +
                "(defaults to False)\n" +
                ":type _no_expect: bool\n"
            )
        else:
            extra_params_docstring = (
                "\n\n" +
                ":param _policy: specify how to check the expectation. Possible values are " +
                "'check', 'wait' and 'check_wait' (defaults to 'check_wait')\n" +
                ":type _policy: `olympe.arsdkng.expectations.ExpectPolicy`\n"
            )
        extra_params_docstring += (
            ":param _float_tol: specify the float comparison tolerance, a 2-tuple containing a " +
            "relative tolerance float value and an absolute tolerate float value " +
            "(default to {}). ".format(cls.float_tol) + "See python 3 stdlib `math.isclose` " +
            "documentation for more information\n" +
            ":type _float_tol: `tuple`\n"
        )
        return "\n".join((cls._py_ar_arg_docstring(arg) for arg in ar_args)) + extra_params_docstring

    def _py_ar_arg_docstring(cls, ar_arg):
        """
        Returns a python docstring from an ArArg object
        """
        if isinstance(ar_arg.argType, (int,)):
            type_ = cls._py_ar_arg_directive(
                "type", ar_arg.name, arsdkparser.ArArgType.TO_STRING[ar_arg.argType])
        elif isinstance(ar_arg.argType, (arsdkparser.ArBitfield,)):
            enum = ":py:class:`olympe.enums.{}.{}`".format(
                ".".join(cls.prefix), cls.args_bitfield[ar_arg.name]._enum_type_.__name__)
            doc = "BitfieldOf({}, {})".format(
                enum,
                arsdkparser.ArArgType.TO_STRING[ar_arg.argType.btfType],
            )
            type_ = cls._py_ar_arg_directive("type", ar_arg.name, doc)
        elif isinstance(ar_arg.argType, (arsdkparser.ArEnum,)):
            doc = ":py:class:`olympe.enums.{}.{}`".format(
                ".".join(cls.prefix), cls.args_enum[ar_arg.name].__name__)
            type_ = cls._py_ar_arg_directive("type", ar_arg.name, doc)
        else:
            raise RuntimeError("Unknown argument type {}".format(
                type(ar_arg.argType)))

        param = cls._py_ar_arg_directive(
            "param", ar_arg.name, cls._py_ar_comment_docstring(ar_arg.doc))
        return u"\n\n{}\n\n{}".format(type_, param)

    def _py_ar_supported(cls, supported_devices, deprecated):
        unsupported_notice = "**Unsupported message**"
        if not cls.feature_name == "debug":
            unsupported_notice += (
                "\n\n.. todo::\n    "
                "Remove unsupported message {}\n".format(cls.fullName)
            )
        deprecation_notice = (
            "**Deprecated message**\n\n.. warning::\n    "
            "This message is deprecated and should no longer be used"
        )
        if deprecated:
            unsupported_notice += "\n\n" + deprecation_notice
        if not supported_devices:
            return unsupported_notice
        supported_devices = string_from_arsdkxml(supported_devices)
        if supported_devices == "drones":
            return "**Supported by every drone product**"
        elif supported_devices == "none":
            return unsupported_notice
        supported_devices = supported_devices.split(';')
        supported_devices = list(
            map(lambda s: s.split(':', maxsplit=2), supported_devices))
        try:
            supported_devices = list(
                map(lambda s: (int(s[0], base=16), *s[1:]), supported_devices))
        except ValueError:
            return unsupported_notice
        ret = []
        for device in supported_devices:
            device_str, *versions = device
            versions = iter(versions)
            device_str = od.string_cast(od.arsdk_device_type_str(device_str))
            since = next(versions, None)
            until = next(versions, None)
            mapping = {
                "ANAFI4K": "Anafi/AnafiFPV",
                "ANAFI_THERMAL": "AnafiThermal",
                "SKYCTRL_3": "SkyController3",
            }
            device_str = mapping.get(device_str, device_str)
            if "anafi" in device_str.lower() or "skycontroller" in device_str.lower():
                if until:
                    ret.append("    :{}: since {} and until {} firmware release".format(
                        device_str,
                        since,
                        until
                    ))
                else:
                    ret.append("    :{}: with an up to date firmware".format(device_str))
        if not ret:
            return unsupported_notice

        ret = "\n".join(ret)
        ret = "\n\n" + ret + "\n\n"
        ret = "**Supported by**: " + ret
        if deprecated:
            ret += "\n\n" + deprecation_notice
        return ret

    def _py_ar_triggered(cls, triggered):
        ret = string_from_arsdkxml(triggered)
        if not ret.startswith("Triggered "):
            ret = "Triggered " + ret
        return textwrap.fill(ret, break_long_words=False)

    def _py_ar_comment_docstring(cls, ar_comment, ar_args_doc=None, ar_is_deprecated=False):
        """
        Returns a python docstring from an ArComment object
        """
        if isinstance(ar_comment, (str, bytes)):
            return string_from_arsdkxml(str(ar_comment))
        ret = u""
        if ar_comment.title and not ar_comment.desc:
            ret += u"\n\n{}".format(
                textwrap.fill(string_from_arsdkxml(ar_comment.title), break_long_words=False),
            )
        elif ar_comment.desc:
            ret += u"\n\n{}".format(
                textwrap.fill(string_from_arsdkxml(ar_comment.desc), break_long_words=False),
            )
        if ar_args_doc is not None:
            ret += ar_args_doc
        # FIXME: arsdk-xml "support" attribute is currently unreliable
        ret += "\n\n{}".format(
            cls._py_ar_supported(ar_comment.support, ar_is_deprecated),
        )
        if ar_comment.triggered:
            ret += "\n\n{}".format(
                cls._py_ar_triggered(ar_comment.triggered),
            )
        if ar_comment.result:
            ret += "\n\n**Result**: {}".format(
                textwrap.fill(string_from_arsdkxml(ar_comment.result), break_long_words=False),
            )
        return ret

    def _py_ar_cmd_expectation_docstring(cls):
        ret = u""
        if cls.message_type == ArsdkMessageType.CMD:
            for i, expectation in enumerate(cls._expectation):
                if isinstance(expectation, ArsdkWhenAnyExpectation):
                    ret += cls._py_ar_cmd_any_expectation_docstring(expectation)
                else:
                    ret += (
                        "#" + expectation.expected_message.id_name +
                        cls._py_ar_cmd_expectation_args_docstring(
                            expectation.expected_args)
                    )
                if i < len(cls._expectation) - 1:
                    ret += " & "
        if ret:
            ret = "**Expectations**: {}".format(ret)
        return ret

    def _py_ar_cmd_any_expectation_docstring(cls, any_expectations):
        ret = u"("
        for i, expectation in enumerate(any_expectations):
            ret += (
                "#" + expectation.expected_message.id_name +
                cls._py_ar_cmd_expectation_args_docstring(
                    expectation.expected_args)
            )
            if i < len(any_expectations) - 1:
                ret += " | "
        ret += u")"
        return ret

    def _py_ar_cmd_expectation_args_docstring(cls, args):
        args = args.copy()
        args.update(_policy="'wait'")
        ret = u"("
        ret += u", ".join((argname + "=" + cls._py_ar_cmd_expectation_argval_docstring(
            argname, argval) for argname, argval in args.items()))
        ret += u")"
        ret = ret.replace('this.', 'self.')
        return ret

    def _py_ar_cmd_expectation_argval_docstring(cls, argname, argval):
        if isinstance(argval, ArsdkEnum):
            return "'" + argval._name_ + "'"
        elif isinstance(argval, ArsdkBitfield):
            return argval.pretty()
        elif callable(argval):
            command_args = OrderedDict(((arg, "this.{}".format(arg)) for arg in cls.args_name))
            try:
                return argval(cls, command_args)
            except KeyError:
                cls.doc_todos += u"\n\n.. todo::\n    {}".format(
                    "Fix wrong expectation definition for {}:\n    {}".format(
                        cls.fullName,
                        "Invalid parameter value for the '{}' expectation parameter\n".format(
                            argname)
                    )
                )
                return "InternalError"
        else:
            return str(argval)

    def get_source(cls):
        args = ", ".join(cls.args_name + ["**kwds"])
        docstring = cls.docstring
        # The docstring needs to be correctly indented in order to be
        # interpreted just below
        docstring = u"\n" + u"\n".join(
            [u" " * 16 + doc.strip() for doc in docstring.splitlines()])
        return textwrap.dedent(
            u"""
            def {name}(self, {defaulted_args}):
                u\"""{docstring}
                \"""
                try:
                    return self._expect({args})
                except Exception as e:
                    import logging
                    logging.exception("")
                    raise
            """.format(
                name=cls.name,
                defaulted_args=cls.args_default_str,
                args=args, docstring=docstring))

    def _create_call(cls):
        """
        Returns a python function that sends a specific ARSDK command

        The name of the returned python function is cls.name
        The parameters of the returned function repsect the naming of arsdk-xml.
        The docstring of the returned function is also extracted from the XMLs.

        @param send_command: ArCmd object provided by the arsdk-xml parser

        """
        exec(cls.get_source(), locals())
        return locals()[cls.name]

    def _ar_arsdk_encode_type_info(cls, ar_argtype):
        arsdk_encode_type_info_map = {
            arsdkparser.ArArgType.I8: (od.ARSDK_ARG_TYPE_I8, "i8", ctypes.c_int8),
            arsdkparser.ArArgType.U8: (od.ARSDK_ARG_TYPE_U8, "u8", ctypes.c_uint8),
            arsdkparser.ArArgType.I16: (od.ARSDK_ARG_TYPE_I16, "i16", ctypes.c_int16),
            arsdkparser.ArArgType.U16: (od.ARSDK_ARG_TYPE_U16, "u16", ctypes.c_uint16),
            arsdkparser.ArArgType.I32: (od.ARSDK_ARG_TYPE_I32, "i32", ctypes.c_int32),
            arsdkparser.ArArgType.U32: (od.ARSDK_ARG_TYPE_U32, "u32", ctypes.c_uint32),
            arsdkparser.ArArgType.I64: (od.ARSDK_ARG_TYPE_I64, "i64", ctypes.c_int64),
            arsdkparser.ArArgType.U64: (od.ARSDK_ARG_TYPE_U64, "u64", ctypes.c_uint64),
            arsdkparser.ArArgType.FLOAT: (od.ARSDK_ARG_TYPE_FLOAT, "f32", ctypes.c_float),
            arsdkparser.ArArgType.DOUBLE: (od.ARSDK_ARG_TYPE_DOUBLE, "f64", ctypes.c_double),
            arsdkparser.ArArgType.STRING: (od.ARSDK_ARG_TYPE_STRING, "cstr", od.char_pointer_cast),
            arsdkparser.ArArgType.ENUM: (od.ARSDK_ARG_TYPE_ENUM, "i32", ctypes.c_int32),
        }
        return arsdk_encode_type_info_map[ar_argtype]

    def _ar_argtype_encode_type(cls, ar_argtype):
        """
        Returns the ctypes type associated to the ar_argtype from arsdkparser
        """
        arbitfield = isinstance(ar_argtype, arsdkparser.ArBitfield)
        ar_bitfield_value = ar_argtype == arsdkparser.ArArgType.BITFIELD

        if isinstance(ar_argtype, arsdkparser.ArEnum):
            return od.ARSDK_ARG_TYPE_ENUM, "i32", ctypes.c_int32
        elif (arbitfield or ar_bitfield_value):
            return cls._ar_arsdk_encode_type_info(ar_argtype.btfType)
        else:
            return cls._ar_arsdk_encode_type_info(ar_argtype)

    def _ar_argtype_to_python(cls, argname, ar_argtype):
        """
        Returns the python type associated to the ar_argtype from arsdkparser
        """

        if argname in cls.args_enum:
            return cls.args_enum[argname]
        elif argname in cls.args_bitfield:
            return cls.args_bitfield[argname]
        elif ar_argtype in (arsdkparser.ArArgType.I8,
                            arsdkparser.ArArgType.U8,
                            arsdkparser.ArArgType.I16,
                            arsdkparser.ArArgType.U16,
                            arsdkparser.ArArgType.I32,
                            arsdkparser.ArArgType.U32,
                            arsdkparser.ArArgType.I64,
                            arsdkparser.ArArgType.U64):
            return int
        elif ar_argtype in (arsdkparser.ArArgType.DOUBLE, arsdkparser.ArArgType.FLOAT):
            return float
        elif ar_argtype == arsdkparser.ArArgType.STRING:
            return str
        else:
            return None

    def _is_list_item(self):
        return self.callback_type is ArsdkMessageCallbackType.LIST

    def _is_map_item(self):
        return self.callback_type is ArsdkMessageCallbackType.MAP

    def _resolve_expectations(cls, messages):
        if cls.message_type == ArsdkMessageType.CMD:
            if cls.obj.expect is not None:
                expectation_objs = cls.obj.expect.immediate + cls.obj.expect.delayed
            else:
                expectation_objs = []
            cls._expectation = ArsdkCommandExpectation(cls)
            for expectation_obj in expectation_objs:
                if not isinstance(expectation_obj, list):
                    cls._expectation.append(
                        ArsdkEventExpectation.from_arsdk(messages, expectation_obj))
                else:
                    cls._expectation.append(
                        ArsdkWhenAnyExpectation.from_arsdk(messages, expectation_obj))
        else:
            cls._expectation = ArsdkEventExpectation(
                cls, OrderedDict((zip(cls.args_name, [None] * len(cls.args_name)))))

    def _resolve_doc(cls, messages):
        cls.docstring += "\n\n" + cls._py_ar_cmd_expectation_docstring()

        def message_ref(m):
            try:
                message = messages.by_id_name[m.group(1)]
            except KeyError:
                return m.group(0)
            if m.lastindex > 1:
                args = "(" + m.group(2) + ")"
            else:
                args = "()"
            return ":py:func:`{}{}<olympe.messages.{}>`".format(
                message.name,
                args,
                message.fullName)
        cls.docstring = re.sub(r"\[[\w_\\]+\]\(#([\d-]+)\)", message_ref, cls.docstring)
        cls.docstring = re.sub(r"#([\d-]+)\(([^)]*)\)", message_ref, cls.docstring)
        cls.docstring = re.sub(r"#([\d-]+)", message_ref, cls.docstring)
        if cls.doc_todos:
            cls.docstring += cls.doc_todos
        cls.__doc__ = cls.docstring


class ArsdkMessage(with_metaclass(ArsdkMessageMeta)):

    def __init__(self):
        self.send = None
        self._reset_state()

    @classmethod
    def new(cls):
        self = cls.__new__(cls, cls.__name__, ArsdkMessage, {})
        self.__init__()
        return self

    def copy(self):
        other = self.new()
        other.send = self.send
        other._last_event = self._last_event
        other._state = self._state.copy()
        return other

    def _bind_send_command(self, send_command):
        """
        Returns a python function that sends a specific ARSDK command

        The name of the returned python function is self.name
        The parameters of the returned function repsect the naming of arsdk-xml.
        The docstring of the returned function is also extracted from the XMLs.

        @param send_command: ArCmd object provided by the arsdk-xml parser

        """
        args = ", ".join(self.args_name + ["**kwds"])
        docstring = self.docstring
        # The docstring needs to be correctly indented in order to be
        # interpreted just below
        docstring = u"\n" + u"\n".join(
            [u" " * 16 + doc.strip() for doc in docstring.splitlines()])
        # TODO: remove backward compatibility for the 'timeout' message parameter
        # this parameter has been replaced by '_timeout'
        exec(textwrap.dedent(
            u"""
            def {name}_SEND({defaulted_args}):
                u\"""{docstring}
                \"""
                try:
                    kwds['_timeout'] = kwds.pop('timeout')
                except KeyError:
                    pass
                kwds['_deprecated_statedict'] = True
                return send_command(self, {args})
            """.format(
                name=self.name,
                defaulted_args=self.args_default_str,
                args=args, docstring=docstring)), locals())
        self.send = locals()[self.name + "_SEND"]

    @classmethod
    def _argsmap_from_args(cls, *args, **kwds):
        args = OrderedDict((zip(map(lambda a: a, cls.args_name), args)))
        args_set = set(args.keys())
        kwds_set = set(kwds.keys())
        if not args_set.isdisjoint(kwds_set):
            raise RuntimeError(
                "Message `{}` got multiple values for argument(s) {}".format(
                    cls.fullName, list(args_set & kwds_set)))
        args.update(kwds)

        # filter out None value
        args = OrderedDict([(k, v) for k, v in args.items() if v is not None])

        # enum conversion
        args = OrderedDict(starmap(
            lambda name, value: (name, cls.args_enum[name][value])
            if (name in cls.args_enum and isinstance(value, (bytes, str))) else (name, value),
            args.items()
        ))

        # bitfield conversion
        args = OrderedDict(starmap(
            lambda name, value: (name, cls.args_bitfield[name](value))
            if name in cls.args_bitfield else (name, value),
            args.items()
        ))

        args = OrderedDict(starmap(lambda k, v: (k, v), args.items()))
        return args

    @classmethod
    def _expectation_from_args(cls, *args, **kwds):
        expected_args = cls._argsmap_from_args(*args, **kwds)
        return ArsdkEventExpectation(cls, expected_args)

    @classmethod
    def _event_from_args(cls, *args, **kwds):
        args = cls._argsmap_from_args(*args, **kwds)
        return ArsdkMessageEvent(cls, args)

    def last_event(self, key=None):
        if key is None:
            return self._last_event
        else:
            return self._last_event[key]

    def _set_last_event(self, event):
        if event.message.id != self.id:
            raise RuntimeError("Cannot set message {} last event to {}".format(
                self.fullName, event.message.fullName))

        if self.callback_type == ArsdkMessageCallbackType.STANDARD:
            self._last_event = event
            self._state = event.args
        elif self.callback_type == ArsdkMessageCallbackType.MAP:
            if self._last_event is None:
                self._last_event = OrderedDict()
            key = event.args[self.key_name]
            if (not event.args["list_flags"] or
                    event.args["list_flags"] == [list_flags.Last]):
                self._state[key] = event.args
            if list_flags.First in event.args["list_flags"]:
                self._state = OrderedDict()
                self._state[key] = event.args
            if list_flags.Empty in event.args["list_flags"]:
                self._state = OrderedDict()
            if list_flags.Remove in event.args["list_flags"]:
                # remove the received element from the current map
                if key in self._state:
                    self._state.pop(key)
            else:
                self._last_event[key] = event
        elif self.callback_type == ArsdkMessageCallbackType.LIST:
            if (not event.args["list_flags"] or
                    event.args["list_flags"] == [list_flags.Last]):
                # append to the current list
                insert_pos = next(reversed(self._state), -1) + 1
                self._state[insert_pos] = event.args
            if list_flags.First in event.args["list_flags"]:
                self._state = OrderedDict()
                self._state[0] = event.args
            if list_flags.Empty in event.args["list_flags"]:
                self._state = OrderedDict()
            if list_flags.Remove in event.args["list_flags"]:
                # remove the received element from the current list
                for k, v in self._state:
                    for argname, argval in v.items():
                        if argname == "list_flags":
                            continue
                        if argval != event.args[argname]:
                            break
                    else:
                        # if all arguments have matched except "list_flags"
                        self._state.pop(k, None)
            else:
                self._last_event = event

    def state(self):
        if self._last_event is None:
            raise RuntimeError("{} state is uninitialized".format(self.fullName))
        return self._state

    def _reset_state(self):
        self._last_event = None
        self._state = OrderedDict()

    def _expect(cls, *args, **kwds):
        """
        For a command message, returns the list of expectations for this message with the provided
        command arguments.

        @param args: the command message arguments
        @param _no_expect: if True for a command message, do not expect the usual command
            expectation (defaults to False)
        """
        default_timeout = cls.timeout if cls.message_type is ArsdkMessageType.CMD else None
        default_float_tol = cls.float_tol
        timeout = kwds.pop('_timeout', default_timeout)
        float_tol = kwds.pop('_float_tol', default_float_tol)
        no_expect = kwds.pop('_no_expect', False)
        send_command = kwds.pop('_send_command', True)
        policy = kwds.pop('_policy', "check_wait")
        if isinstance(policy, (bytes, str)):
            policy = ExpectPolicy[policy]
        else:
            raise RuntimeError("policy argument must be a string")

        if not send_command and no_expect:
            raise RuntimeError(
                "Invalid argument combination " +
                "Message._expect(send_command=False, no_expect=True)")

        args = cls._argsmap_from_args(*args, **kwds)
        # enum conversion
        args = OrderedDict(starmap(
            lambda name, value: (name, cls.args_enum[name][value])
            if (name in cls.args_enum and isinstance(value, (bytes, str))) else (name, value),
            args.items()
        ))

        # bitfield conversion
        args = OrderedDict(starmap(
            lambda name, value: (name, cls.args_bitfield[name](value))
            if name in cls.args_bitfield else (name, value),
            args.items()
        ))

        # int -> float conversion
        args = OrderedDict(starmap(
            lambda name, value: (name, float(value))
            if isinstance(value, int) and cls.args_type[name] is float else (name, value),
            args.items()
        ))

        if policy != ExpectPolicy.check:
            if not send_command and cls.message_type == ArsdkMessageType.CMD:
                expectations = ArsdkWhenAllExpectations(cls._expectation.copy().expectations)
            else:
                expectations = cls._expectation.copy()
                if cls.message_type == ArsdkMessageType.CMD:
                    expectations.no_expect(no_expect)

            expectations._fill_default_arguments(cls, args)

            if policy == ExpectPolicy.check_wait and cls.message_type is ArsdkMessageType.EVT:
                check_expectation = ArsdkCheckStateExpectation(cls, args)
                expectations = ArsdkCheckWaitStateExpectation(check_expectation, expectations)
            expectations.set_timeout(timeout)
            expectations.set_float_tol(float_tol)
            return expectations
        else:
            expectations = ArsdkCheckStateExpectation(cls, args)
            expectations.set_float_tol(float_tol)
            return expectations

    @classmethod
    def _encode_args(cls, *args):
        """
        Encode python message arguments to ctypes. This also perform the necessary enum, bitfield
        and unicode conversions.
        """
        if len(args) != len(cls.obj.args):
            raise TypeError("{}() takes exactly {} arguments ({} given)".format(
                cls.FullName, len(cls.obj.args), len(args)))

        encoded_args = args
        # enum conversion (string --> enum type)
        encoded_args = list(starmap(
            lambda name, value: cls.args_enum[name][value]
            if (name in cls.args_enum and isinstance(value, (bytes, str))) else value,
            zip(cls.args_name, encoded_args)
        ))

        # enum conversion (enum type --> integer)
        encoded_args = list(starmap(
            lambda name, value: value._value_
            if (name in cls.args_enum) and isinstance(value, ArsdkEnum) else value,
            zip(cls.args_name, encoded_args)
        ))

        # bitfield conversion ([string, enum list, bitfield] --> integer)
        encoded_args = list(starmap(
            lambda name, value: cls.args_bitfield[name](value).to_int()
            if name in cls.args_bitfield else value,
            zip(cls.args_name, encoded_args)
        ))

        # unicode -> str utf-8 encoding
        encoded_args = list(map(
            lambda a: a.encode('utf-8') if isinstance(a, str) else a, encoded_args))

        # python -> ctypes -> struct_arsdk_value argv conversion
        encode_args_len = len(cls.arsdk_type_args)
        argv = (od.struct_arsdk_value * encode_args_len)()
        for (i, arg, sdktype, value_attr, ctype) in zip(
            range(encode_args_len), encoded_args, cls.arsdk_type_args, cls.arsdk_value_attr, cls.encode_ctypes_args):
            argv[i].type = sdktype
            setattr(argv[i].data, value_attr, ctype(arg))
        return argv

    @classmethod
    def _decode_args(cls, message_buffer):
        """
        Decode a ctypes message buffer into a list of python typed arguments. This also perform the
        necessary enum, bitfield and unicode conversions.
        """
        decoded_args = list(map(lambda ctype: ctypes.pointer(ctype()), cls.decode_ctypes_args))
        decoded_args_type = list(map(lambda ctype: ctypes.POINTER(ctype), cls.decode_ctypes_args))
        od.arsdk_cmd_dec.argtypes = od.arsdk_cmd_dec.argtypes[:2] + decoded_args_type

        res = od.arsdk_cmd_dec(message_buffer, cls.arsdk_desc, *decoded_args)

        # ctypes -> python type conversion
        decoded_args = list(map(
            lambda a: a.contents.value, decoded_args
        ))

        # bytes utf-8 -> str conversion
        decoded_args = list(map(
            lambda a: str(a, 'utf-8')
            if isinstance(a, bytes) else a,
            decoded_args
        ))

        # enum conversion
        decoded_args = list(starmap(
            lambda name, value: cls.args_enum[name](value)
            if name in cls.args_enum and value in cls.args_enum[name]._value2member_map_
            else value,
            zip(cls.args_name, decoded_args)
        ))

        # bitfield conversion
        decoded_args = list(map(
            lambda t: cls.args_bitfield[t[0]](t[1])
            if t[0] in cls.args_bitfield else t[1],
            zip(cls.args_name, decoded_args)
        ))

        return (res, decoded_args)


class ArsdkMessageType(OrderedEnum):
    CMD, EVT = range(2)

    @classmethod
    def from_arsdk(cls, value):
        return {
            arsdkparser.ArCmd: cls.CMD,
            arsdkparser.ArEvt: cls.EVT,
        }[value]


class ArsdkMessageCallbackType(OrderedEnum):
    STANDARD, LIST, MAP = range(3)

    @classmethod
    def from_arsdk(cls, value):
        return {
            arsdkparser.ArCmdListType.NONE: cls.STANDARD,
            arsdkparser.ArCmdListType.LIST: cls.LIST,
            arsdkparser.ArCmdListType.MAP: cls.MAP,
        }[value]


class ArsdkMessageBufferType(OrderedEnum):
    NON_ACK, ACK, HIGH_PRIO = range(3)

    @classmethod
    def from_arsdk(cls, value):
        return {
            arsdkparser.ArCmdBufferType.NON_ACK: cls.NON_ACK,
            arsdkparser.ArCmdBufferType.ACK: cls.ACK,
            arsdkparser.ArCmdBufferType.HIGH_PRIO: cls.HIGH_PRIO,
        }[value]


_PRESET_SETTINGS_VIDEO = {
    "max_altitude": {"is_set": 1, "value": 150.},
    "max_tilt": {"is_set": 1, "value": 35.},
    "max_distance": {"is_set": 1, "value": 2000.},
    "no_fly_over_max_distance": {"is_set": 1, "value": 0},
    "max_vertical_speed": {"is_set": 1, "value": 6.},
    "max_rotation_speed": {"is_set": 1, "value": 150.},
    "max_pitch_roll_rotation_speed": {"is_set": 1, "value": 300.},
    "return_home_delay": {"is_set": 1, "value": 120},
    "home_type": {"is_set": 1, "value": 0},
    "video_stabilization_mode": {"is_set": 1, "value": 0},
    "banked_turn": {"is_set": 1, "value": 1},
}


class ArsdkMessages(object):
    """
    A python class to represent arsdk messages commands and events alike.
    """

    _single = None

    @classmethod
    def get(cls):
        if cls._single is None:
            cls._single = cls()
        return cls._single

    _default_arguments = {
        'Ardrone3.GPSSettings.SendControllerGPS':
            dict(horizontalAccuracy=1.0, verticalAccuracy=1.0),
        'Ardrone3.NetworkSettings.WifiSelection': dict(channel=0),
        'Ardrone3.PictureSettings.VideoAutorecordSelection': dict(mass_storage_id=0),
        'Common.Mavlink.Start': dict(type="'flightPlan'"),
        'Generic.SetDroneSettings': dict(preset=_PRESET_SETTINGS_VIDEO),
        'Gimbal.Reset_orientation': dict(gimbal_id=0),
        'Gimbal.Start_offsets_update': dict(gimbal_id=0),
        'Gimbal.Stop_offsets_update': dict(gimbal_id=0),
    }

    def __init__(self, arsdk_enums=None):
        """
        ArsdkMessages constructor
        @type arsdk_enums: olympe.arsdkng.Enums
        """
        self.enums = arsdk_enums
        if self.enums is None:
            self.enums = ArsdkEnums.get()
        self._ctx = self.enums._ctx
        self.BY_NAME = OrderedDict()
        self.By_Name = OrderedDict()
        self.ByName = OrderedDict()
        self.by_id = OrderedDict()
        self.by_id_name = OrderedDict()
        self.by_prefix = OrderedDict()
        self.by_feature = OrderedDict()
        self._feature_name_by_id = OrderedDict()

        self._populate_messages()
        self._resolve_expectations()
        self._resolve_doc()

    def _populate_messages(self):
        for featureId in sorted(self._ctx.featuresById.keys()):
            featureObj = self._ctx.featuresById[featureId]
            # Workaround messages from the "generic" feature may contain
            # "multisettings" arguments that Olympe doesn't handle.
            # Here we simply ignore these messages to avoid any further error
            if featureObj.name == "generic":
                continue
            if featureObj.classes and len(featureObj.classes) != 0:
                for classId in sorted(featureObj.classesById.keys()):
                    classObj = featureObj.classesById[classId]
                    for msgId in sorted(classObj.cmdsById.keys()):
                        msgObj = classObj.cmdsById[msgId]
                        self._add_arsdk_message(
                            msgObj,
                            [featureObj.name, classObj.name, msgObj.name],
                            [featureId, classId, msgId],
                        )

            elif len(featureObj.getMsgs()) != 0:
                for msgId in sorted(featureObj.getMsgsById().keys()):
                    msgObj = featureObj.getMsgsById()[msgId]
                    self._add_arsdk_message(
                        msgObj,
                        [featureObj.name, None, msgObj.name],
                        [featureId, None, msgId],
                    )

    def _add_arsdk_message(self, msgObj, name_path, id_path):

        message = ArsdkMessageMeta.__new__(
            ArsdkMessageMeta, msgObj, name_path, id_path, self.enums)
        self.BY_NAME[message.FULL_NAME] = message
        self.By_Name[message.Full_Name] = message
        self.ByName[message.FullName] = message
        self.by_id[message.id] = message
        self.by_id_name[message.id_name] = message
        feature_id = message.id & 0xFF000000 >> 24
        class_id = message.id & 0x00FF0000 >> 16
        self._feature_name_by_id[(feature_id, class_id)] = (
            message.feature_name, message.class_name)
        if message.prefix not in self.by_prefix:
            self.by_prefix[message.prefix] = OrderedDict()
        self.by_prefix[message.prefix][message.name] = message
        if message.feature_name not in self.by_feature:
            self.by_feature[message.feature_name] = OrderedDict()
        if message.class_name is not None:
            if message.class_name not in self.by_feature[message.feature_name]:
                self.by_feature[message.feature_name][message.class_name] = OrderedDict()
            self.by_feature[message.feature_name][message.class_name][message.name] = message
        else:
            self.by_feature[message.feature_name][message.name] = message

    def walk_enums(self):
        for prefix, messages in self.by_prefix.items():
            for message_name, message in messages.items():
                for argname, enum in message.args_enum.items():
                    for enum_label, enum_value in enum.__members__.items():
                        yield prefix, message_name, argname, enum_label, enum_value

    def walk_args(self):
        for prefix, messages in self.by_prefix.items():
            for message_name, message in messages.items():
                for argname in message.args_pos.keys():
                    yield prefix, message_name, message, argname

    def unknown_message_info(self, message_id):
        feature_id = message_id & 0xFF000000 >> 24
        class_id = message_id & 0x00FF0000 >> 16
        msg_id = message_id & 0x0000FFFF
        feature_name, class_name = self._feature_name_by_id.get(
            (feature_id, class_id),
            (None, None)
        )
        if feature_name is None:
            return (None, None, message_id)
        else:
            return (feature_name, class_name, msg_id)

    def _resolve_expectations(self):
        for message in self.by_id.values():
            message._resolve_expectations(self)

    def _resolve_doc(self):
        for message in self.by_id.values():
            message._resolve_doc(self)