import time
from snappi_ixnetwork.timer import Timer
from snappi_ixnetwork.customfield import CustomField

class TrafficItem(CustomField):
    """TrafficItem configuration

    Args
    ----
    - ixnetworkapi (Api): instance of the ixnetworkapi class
    
    """
    _RESULT_COLUMNS = [
        ('frames_tx', 'Tx Frames', int),
        ('frames_rx', 'Rx Frames', int),
        ('frames_tx_rate', 'Tx Frame Rate', float),
        ('frames_rx_rate', 'Rx Frame Rate', float),
        ('bytes_tx', 'Tx Bytes', int),
        ('bytes_rx', 'Rx Bytes', int),
        ('loss', 'Loss %', float),
        ('bytes_tx_rate', 'Tx Rate (Bps)', float),
        ('bytes_rx_rate', 'Rx Rate (Bps)', float),
    ]

    _RESULT_LATENCY_STORE_FORWARD = [
        ('minimum_ns', 'Store-Forward Min Latency (ns)', int),
        ('maximum_ns', 'Store-Forward Max Latency (ns)', int),
        ('average_ns', 'Store-Forward Avg Latency (ns)', int),
    ]
    
    _RESULT_LATENCY_CUT_THROUGH = [
        ('minimum_ns', 'Cut-Through Min Latency (ns)', int),
        ('maximum_ns', 'Cut-Through Max Latency (ns)', int),
        ('average_ns', 'Cut-Through Avg Latency (ns)', int),
    ]
    
    _RESULT_TIMESTAMP = [
        ('first_timestamp_ns', 'First TimeStamp', int),
        ('last_timestamp_ns', 'Last TimeStamp', int)
    ]

    _STACK_IGNORE = ['ethernet.fcs', 'pfcPause.fcs']

    _TYPE_TO_HEADER = {
        'ethernet': 'ethernet',
        'pfcPause': 'pfcpause',
        'vlan': 'vlan',
        'ipv4': 'ipv4',
        'ipv6': 'ipv6',
        'tcp': 'tcp',
        'udp': 'udp',
        'gtpu': 'gtpv1',
        'gTPuOptionalFields': 'gtpv1option',
        'custom': 'custom'
    }

    _HEADER_TO_TYPE = {
        'ethernet': 'ethernet',
        'pfcpause': 'pfcPause',
        'vlan': 'vlan',
        'ipv4': 'ipv4',
        'ipv6': 'ipv6',
        'tcp': 'tcp',
        'udp': 'udp',
        'gtpv1': 'gtpu',
        'gtpv1option': 'gTPuOptionalFields',
        'custom': 'custom'
    }

    _BIT_RATE_UNITS_TYPE = {
        'bps': 'bitsPerSec',
        'kbps': 'kbitsPerSec',
        'mbps': 'mbitsPerSec',
        'gbps': 'mbytesPerSec'
    }

    _LATENCY = {
        'cut_through' : 'cutThrough',
        'store_forward' : 'storeForward'
    }

    _PFCPAUSE = {
        'dst': 'pfcPause.header.header.dstAddress',
        'src': 'pfcPause.header.header.srcAddress',
        'ether_type': 'pfcPause.header.header.ethertype',
        'control_op_code': 'pfcPause.header.macControl.controlOpcode',
        'class_enable_vector':
        'pfcPause.header.macControl.priorityEnableVector',
        'pause_class_0': 'pfcPause.header.macControl.pauseQuanta.pfcQueue0',
        'pause_class_1': 'pfcPause.header.macControl.pauseQuanta.pfcQueue1',
        'pause_class_2': 'pfcPause.header.macControl.pauseQuanta.pfcQueue2',
        'pause_class_3': 'pfcPause.header.macControl.pauseQuanta.pfcQueue3',
        'pause_class_4': 'pfcPause.header.macControl.pauseQuanta.pfcQueue4',
        'pause_class_5': 'pfcPause.header.macControl.pauseQuanta.pfcQueue5',
        'pause_class_6': 'pfcPause.header.macControl.pauseQuanta.pfcQueue6',
        'pause_class_7': 'pfcPause.header.macControl.pauseQuanta.pfcQueue7',
    }

    _ETHERNET = {
        'dst': 'ethernet.header.destinationAddress',
        'src': 'ethernet.header.sourceAddress',
        'ether_type': 'ethernet.header.etherType',
        'pfc_queue': 'ethernet.header.pfcQueue',
    }

    _VLAN = {
        'id': 'vlan.header.vlanTag.vlanID',
        'cfi': 'vlan.header.vlanTag.cfi',
        'priority': 'vlan.header.vlanTag.vlanUserPriority',
        'protocol': 'vlan.header.protocolID'
    }

    _IPV4 = {
        'version': 'ipv4.header.version',
        'header_length': 'ipv4.header.headerLength',
        'priority': '_ipv4_priority',
        'total_length': 'ipv4.header.totalLength',
        'identification': 'ipv4.header.identification',
        'reserved': 'ipv4.header.flags.reserved',
        'dont_fragment': 'ipv4.header.flags.fragment',
        'more_fragments': 'ipv4.header.flags.lastFragment',
        'fragment_offset': 'ipv4.header.fragmentOffset',
        'time_to_live': 'ipv4.header.ttl',
        'protocol': 'ipv4.header.protocol',
        'header_checksum': 'ipv4.header.checksum',
        'src': 'ipv4.header.srcIp',
        'dst': 'ipv4.header.dstIp',
    }

    _IPV6 = {
        'version' : 'ipv6.header.versionTrafficClassFlowLabel.version',
        'traffic_class' : 'ipv6.header.versionTrafficClassFlowLabel.trafficClass',
        'flow_label' : 'ipv6.header.versionTrafficClassFlowLabel.flowLabel',
        'payload_length' : 'ipv6.header.payloadLength',
        'next_header' : 'ipv6.header.nextHeader',
        'hop_limit' : 'ipv6.header.hopLimit',
        'src' : 'ipv6.header.srcIP',
        'dst' : 'ipv6.header.dstIP'
    }
    
    _TOS = {
        "precedence": "ipv4.header.priority.tos.precedence",
        "delay": "ipv4.header.priority.tos.delay",
        "throughput": "ipv4.header.priority.tos.throughput",
        "reliability": "ipv4.header.priority.tos.reliability",
        "monetary": "ipv4.header.priority.tos.monetary",
        "unused": "ipv4.header.priority.tos.unused"
    }

    _TCP = {
        "src_port": "tcp.header.srcPort",
        "dst_port": "tcp.header.dstPort",
        "ecn_ns": "tcp.header.ecn.nsBit",
        "ecn_cwr": "tcp.header.ecn.cwrBit",
        "ecn_echo": "tcp.header.ecn.ecnEchoBit",
        "ctl_urg": "tcp.header.controlBits.urgBit",
        "ctl_ack": "tcp.header.controlBits.ackBit",
        "ctl_psh": "tcp.header.controlBits.pshBit",
        "ctl_rst": "tcp.header.controlBits.rstBit",
        "ctl_syn": "tcp.header.controlBits.synBit",
        "ctl_fin": "tcp.header.controlBits.finBit",
    }

    _UDP = {
        "src_port": "udp.header.srcPort",
        "dst_port": "udp.header.dstPort",
        "length": "udp.header.length",
        "checksum": "udp.header.checksum",
    }
    
    _GTPV1 = {
        "version" : "gtpu.header.version",
        "protocol_type" : "gtpu.header.pt",
        "reserved" : "gtpu.header.reserved",
        "e_flag" : "gtpu.header.e",
        "s_flag" : "gtpu.header.s",
        "pn_flag" : "gtpu.header.n",
        "message_type" : "tpu.header.type",
        "message_length" : "gtpu.header.totalLength",
        "teid" : "gtpu.header.teid"
    }

    _GTPV1OPTION = {
        "squence_number" : "gTPuOptionalFields.header.sequenceNumber",
        "n_pdu_number" : "gTPuOptionalFields.header.npduNumber",
        "next_extension_header_type" : "gTPuOptionalFields.header.nextExtHdrField"
    }

    _CUSTOM = '_custom_headers'

    def __init__(self, ixnetworkapi):
        self._api = ixnetworkapi
        self.has_latency = False
        self._flow_timeout = 10

    def config(self):
        """Configure config.flows onto Ixnetwork.Traffic.TrafficItem
        
        CRUD
        ----
        - DELETE any TrafficItem.Name that does not exist in config.flows
        - CREATE TrafficItem for any config.flows[*].name that does not exist
        - UPDATE TrafficItem for any config.flows[*].name that exists
        """
        ixn_traffic_item = self._api._traffic_item
        self.flows_has_latency = []
        self.flows_has_timestamp = []
        self.flows_has_loss = []
        self.latency_mode = None
        self._api._remove(ixn_traffic_item, self._api.snappi_config.flows)
        if len(self._api.snappi_config.flows) > 0:
            for flow in self._api.snappi_config.flows:
                self._endpoint_validation(flow)
                args = {
                    'Name': flow.name,
                    'TrafficItemType': 'l2L3',
                    'TrafficType': self._get_traffic_type(flow),
                    'SrcDestMesh': self._get_mesh_type(flow)
                }
                ixn_traffic_item.find(Name='^%s$' % self._api.special_char(flow.name),
                                      TrafficType=args['TrafficType'])
                if len(ixn_traffic_item) == 0:
                    ixn_traffic_item.add(**args)
                else:
                    self._update(ixn_traffic_item, **args)
                self._configure_endpoint(ixn_traffic_item.EndpointSet,
                                         flow.tx_rx)
                metrics = flow._properties.get('metrics')
                if metrics is not None \
                        and metrics.enable is True:
                    self._configure_tracking(flow, ixn_traffic_item.Tracking)
                    latency = metrics._properties.get('latency')
                    if latency is not None and latency.enable is True:
                        self.flows_has_latency.append(flow.name)
                        self._process_latency(latency)
                    timestamps = metrics._properties.get('timestamps')
                    if timestamps is True:
                        self.flows_has_timestamp.append(flow.name)
                    loss = metrics._properties.get('loss')
                    if loss is True:
                        self.flows_has_loss.append(flow.name)
                ixn_ce = ixn_traffic_item.ConfigElement.find()
                hl_stream_count = len(ixn_traffic_item.HighLevelStream.find())
                self._configure_stack(ixn_ce, flow.packet)
                self._configure_size(ixn_ce, flow.size)
                self._configure_rate(ixn_ce, flow.rate)
                self._configure_tx_control(ixn_ce, hl_stream_count, flow.duration)
            self._configure_options()
            self._configure_latency()

    def _process_latency(self, latency):
        if self.latency_mode is None:
            if latency.mode is not None:
                self.latency_mode = latency.mode
            else:
                self.latency_mode = 'store_forward'
        else:
            if latency.mode is not None and \
                    self.latency_mode != latency.mode:
                raise Exception("Latency mode needs to be same for all flows")

    def _configure_latency(self):
        ixn_latency = self._api._traffic.Statistics.Latency
        if self.latency_mode is not None:
            self.has_latency = True
            ixn_CpdpConvergence = self._api._traffic.Statistics.CpdpConvergence
            ixn_CpdpConvergence.Enabled = False
            ixn_latency.Enabled = True
            ixn_latency.Mode = TrafficItem._LATENCY[
                self.latency_mode]
        else:
            self.has_latency = False
            ixn_latency.Enabled = False
    
    def _configure_tracking(self, flow, ixn_tracking):
        """Set tracking options"""
        ixn_tracking.find()
        tracking_options = ['trackingenabled0']
        if flow.tx_rx.choice == 'device':
            tracking_options.append('sourceDestPortPair0')
        if set(tracking_options) != set(ixn_tracking.TrackBy):
            ixn_tracking.update(TrackBy=tracking_options)

    def _configure_options(self):
        enable_min_frame_size = False
        for flow in self._api.snappi_config.flows:
            if (len(flow.packet) == 1 and flow.packet[
                    0].parent.choice == 'pfcpause'):
                enable_min_frame_size = True
                break
        if self._api._traffic.EnableMinFrameSize != enable_min_frame_size:
            self._api._traffic.EnableMinFrameSize = enable_min_frame_size

    def _endpoint_validation(self, flow):
        if flow.tx_rx.choice is None:
            raise ValueError('%s Flow.tx_rx property cannot be None' %
                             flow.name)
        if flow.tx_rx.choice == 'device':
            device = flow.tx_rx.device
            if not isinstance(device.tx_names, list) or \
                    not isinstance(device.rx_names, list):
                raise ValueError("device tx_names and rx_names must be a list "
                                 "in flow %s" % flow.name)
            if len(device.tx_names) != len(set(device.tx_names)):
                raise ValueError("All names in device tx_names "
                                 "must be unique for flow %s" % flow.name)
            if len(device.rx_names) != len(set(device.rx_names)):
                raise ValueError("All names in device rx_names "
                                 "must be unique for flow %s" % flow.name)
    
    def _get_mesh_type(self, flow):
        if flow.tx_rx.choice == 'port':
            mesh_type = 'oneToOne'
        else:
            device = flow.tx_rx.device
            if device.mode == 'mesh' or \
                    device.mode is None:
                mesh_type = 'manyToMany'
            else:
                mesh_type = 'oneToOne'
                if len(device.tx_names) != len(device.rx_names):
                    raise ValueError("Length of device tx_names and rx_names "
                             "must be same for device mode ONE_TO_ONE in flow %s" % flow.name)
        return mesh_type
    
    def _get_traffic_type(self, flow):
        if flow.tx_rx.choice == 'port':
            encap = 'raw'
        else:
            encap_list = []
            device = flow.tx_rx.device
            if device.tx_names is not None:
                for tx_name in device.tx_names:
                    encap_list.append(self._api.get_device_encap(
                            tx_name))
            if device.rx_names is not None:
                for rx_name in device.rx_names:
                    encap_list.append(self._api.get_device_encap(
                            rx_name))
            if len(set(encap_list)) == 1:
                encap = encap_list[0]
            else:
                raise Exception("All devices identified in tx_names and rx_names "
                                "must be of same type in flow %s" %flow.name)
        return encap

    def _configure_endpoint(self, ixn_endpoint_set, endpoint):
        """Transform flow.tx_rx to /trafficItem/endpointSet
        The model allows for only one endpointSet per traffic item
        """
        args = {'Sources': [], 'Destinations': []}
        if (endpoint.choice == "port"):
            args['Sources'].append(
                self._api.get_ixn_object(
                    endpoint.port.tx_name).Protocols.find().href)
            if endpoint.port.rx_name != None:
                args['Destinations'].append(
                    self._api.get_ixn_object(
                        endpoint.port.rx_name).Protocols.find().href)
        else:
            for device_name in endpoint.device.tx_names:
                args['Sources'].append(self._api.get_ixn_href(device_name))
            for device_name in endpoint.device.rx_names:
                args['Destinations'].append(self._api.get_ixn_href(device_name))
        ixn_endpoint_set.find()
        if len(ixn_endpoint_set) > 1:
            ixn_endpoint_set.remove()
        if len(ixn_endpoint_set) == 0:
            ixn_endpoint_set.add(**args)
        elif ixn_endpoint_set.Sources != args[
                'Sources'] or ixn_endpoint_set.Destinations != args[
                    'Destinations'] or len(ixn_endpoint_set.parent.ConfigElement.find()) == 0:
            self._update(ixn_endpoint_set, **args)

    def _update(self, ixn_object, **kwargs):
        from ixnetwork_restpy.base import Base
        update = False
        for name, value in kwargs.items():
            if isinstance(value, list) and len(value) > 0 and isinstance(
                    value[0], Base):
                value = [item.href for item in value]
            elif isinstance(value, Base):
                value = value.href
            if getattr(ixn_object, name) != value:
                update = True
        if update is True:
            ixn_object.update(**kwargs)

    def _configure_stack(self, ixn_stream, headers):
        """Transform flow.packets[0..n] to /traffic/trafficItem/configElement/stack
        The len of the headers list is the definitive list which means add/remove
        any stack items so that the stack list matches the headers list.
        If the headers list is empty then use the traffic generator default stack.
        """
        headers = self.adjust_header(headers)
        ixn_stack = ixn_stream.Stack.find()
        for i in range(0, len(headers)):
            header = headers[i]
            if len(ixn_stack) <= i:
                stack = self._add_stack(ixn_stream, stack, header)
            else:
                stack_type_id = ixn_stack[i].StackTypeId
                if stack_type_id in self._STACK_IGNORE:
                    stack = self._add_stack(ixn_stream, stack, header)
                elif stack_type_id not in TrafficItem._TYPE_TO_HEADER:
                    stack = self._add_stack(ixn_stream, ixn_stack[i], header)
                elif TrafficItem._TYPE_TO_HEADER[
                        stack_type_id] != header.choice:
                    stack = self._add_stack(ixn_stream, ixn_stack[i], header)
                else:
                    stack = ixn_stack[i]
            self._configure_field(stack.Field, header)

        # scan and compare new stack to overcome IxNetwork stack serialization
        # then remove additional stack
        if len(headers) == 0:
            return
        stacks_to_remove = []
        ixn_stack = ixn_stream.Stack.find()
        header_index = 0
        for i in range(0, len(ixn_stack)):
            stack_type_id = ixn_stack[i].StackTypeId
            if stack_type_id in self._STACK_IGNORE:
                continue
            if len(headers) <= header_index:
                stacks_to_remove.append(ixn_stack[i])
                continue
            if TrafficItem._TYPE_TO_HEADER[
                    stack_type_id] != headers[header_index].choice:
                stacks_to_remove.append(ixn_stack[i])
            else:
                header_index += 1
        for stack in stacks_to_remove[::-1]:
            stack.Remove()

    def _add_stack(self, ixn_stream, ixn_stack, header):
        type_id = '^%s$' % TrafficItem._HEADER_TO_TYPE[header.choice]
        template = self._api._traffic.ProtocolTemplate.find(
            StackTypeId=type_id)
        stack_href = ixn_stack.AppendProtocol(template)
        return ixn_stream.Stack.read(stack_href)

    def _configure_field(self, ixn_field, header, field_choice=False):
        """Transform flow.packets[0..n].header.choice to /traffic/trafficItem/configElement/stack/field
        """
        field_map = getattr(self, '_%s' % header.choice.upper())
        packet = getattr(header, header.choice)
        if isinstance(field_map, dict) is False:
            method = getattr(self, field_map)
            method(ixn_field, packet)
            return

        for packet_field_name in dir(packet):
            if packet_field_name in field_map:
                pattern = getattr(packet, packet_field_name)
                field_type_id = field_map[packet_field_name]
                self._configure_pattern(ixn_field, field_type_id, pattern,
                                        field_choice)

    def _configure_pattern(self,
                           ixn_field,
                           field_type_id,
                           pattern,
                           field_choice=False):
        custom_field = getattr(self, field_type_id, None)
        if custom_field is not None:
            if pattern.choice is not None:
                custom_field(ixn_field, pattern)
            return
    
        ixn_field = ixn_field.find(FieldTypeId=field_type_id)
        self._set_default(ixn_field, field_choice)
    
        if pattern.choice is None:
            return
    
        if pattern.choice == 'value':
            ixn_field.update(Auto=False,
                             ActiveFieldChoice=field_choice,
                             ValueType='singleValue',
                             SingleValue=pattern.value)
        elif pattern.choice == 'values':
            ixn_field.update(Auto=False,
                             ActiveFieldChoice=field_choice,
                             ValueType='valueList',
                             ValueList=pattern.values)
        elif pattern.choice == 'increment':
            ixn_field.update(Auto=False,
                             ValueType='increment',
                             ActiveFieldChoice=field_choice,
                             StartValue=pattern.increment.start,
                             StepValue=pattern.increment.step,
                             CountValue=pattern.increment.count)
        elif pattern.choice == 'decrement':
            ixn_field.update(Auto=False,
                             ValueType='decrement',
                             ActiveFieldChoice=field_choice,
                             StartValue=pattern.decrement.start,
                             StepValue=pattern.decrement.step,
                             CountValue=pattern.decrement.count)
        elif pattern.choice == 'random':
            ixn_field.update(Auto=False,
                             ActiveFieldChoice=field_choice,
                             ValueType='repeatableRandomRange',
                             MinValue=pattern.random.min,
                             MaxValue=pattern.random.max,
                             StepValue=pattern.random.step,
                             Seed=pattern.random.seed,
                             CountValue=pattern.random.count)
        else:
            # TBD: add to set_config errors - invalid pattern specified
            pass
    
        if pattern.metric_group is not None:
            ixn_field.TrackingEnabled = True
            self._api.ixn_objects[pattern.metric_group] = ixn_field.href

    def _set_default(self, ixn_field, field_choice):
        """We are setting all the field to default. Otherwise test is keeping the same value from previous run."""
        if ixn_field.ReadOnly:
            return
        
        if ixn_field.SupportsAuto:
            if ixn_field.Auto is not True:
                ixn_field.Auto = True
        else:
            ixn_field.update(Auto=False,
                             ActiveFieldChoice=field_choice,
                             ValueType='singleValue',
                             SingleValue=ixn_field.DefaultValue)

    def _configure_size(self, ixn_stream, size):
        """ Transform frameSize flows.size to /traffic/trafficItem[*]/configElement[*]/frameSize
        """
        if size.choice is None:
            return
        ixn_frame_size = ixn_stream.FrameSize
        args = {}
        if size.choice == 'fixed':
            args['Type'] = "fixed"
            args['FixedSize'] = size.fixed
        elif size.choice == 'increment':
            args['Type'] = "increment"
            args['IncrementFrom'] = size.increment.start
            args['IncrementTo'] = size.increment.end
            args['IncrementStep'] = size.increment.step
        elif size.choice == 'random':
            args['Type'] = "random"
            args['RandomMin'] = size.random.min
            args['RandomMax'] = size.random.max
        else:
            print('Warning - We need to implement this %s choice' %
                  size.choice)
        self._update(ixn_frame_size, **args)

    def _configure_rate(self, ixn_stream, rate):
        """ Transform frameRate flows.rate to /traffic/trafficItem[*]/configElement[*]/frameRate
        """
        if rate.choice is None:
            return
        ixn_frame_rate = ixn_stream.FrameRate
        args = {}
        value = None
        if rate.choice == 'percentage':
            args['Type'] = 'percentLineRate'
            value = rate.percentage
        elif rate.choice == 'pps':
            args['Type'] = 'framesPerSecond'
            value = rate.pps
        else:
            args['Type'] = 'bitsPerSecond'
            args['BitRateUnitsType'] = TrafficItem._BIT_RATE_UNITS_TYPE[
                rate.choice]
            value = getattr(rate, rate.choice)
        args['Rate'] = value
        self._update(ixn_frame_rate, **args)

    def _configure_delay(self, delay, args):
        if delay.choice is not None:
            value = getattr(delay, delay.choice, None)
            if value is None:
                raise Exception("Delay must be of type <int>")
            if isinstance(value, float):
                self._api.warning("Cast Delay to <int> due to software limitation")
            if delay.choice == 'microseconds':
                args['StartDelayUnits'] = 'nanoseconds'
                args['StartDelay'] = value * 1000
            else:
                args['StartDelayUnits'] = delay.choice
                args['StartDelay'] = value
    
    def _configure_tx_control(self, ixn_stream, hl_stream_count, duration):
        """Transform duration flows.duration to /traffic/trafficItem[*]/configElement[*]/TransmissionControl
        """
        if duration.choice is None:
            return
        ixn_tx_control = ixn_stream.TransmissionControl
        args = {}
        if duration.choice == 'continuous':
            args['Type'] = 'continuous'
            args['MinGapBytes'] = duration.continuous.gap
            self._configure_delay(duration.continuous.delay, args)
        elif duration.choice == 'fixed_packets':
            args['Type'] = 'fixedFrameCount'
            args['FrameCount'] = duration.fixed_packets.packets / hl_stream_count
            args['MinGapBytes'] = duration.fixed_packets.gap
            self._configure_delay(duration.fixed_packets.delay, args)
        elif duration.choice == 'fixed_seconds':
            args['Type'] = 'fixedDuration'
            args['Duration'] = duration.fixed_seconds.seconds
            args['MinGapBytes'] = duration.fixed_seconds.gap
            self._configure_delay(duration.fixed_seconds.delay, args)
        elif duration.choice == 'burst':
            if duration.burst.bursts is not None \
                    and int(duration.burst.bursts) > 0:
                args['Type'] = 'burstFixedDuration'
                args['RepeatBurst'] = duration.burst.bursts
            else:
                args['Type'] = 'custom'
            args['BurstPacketCount'] = duration.burst.packets
            args['MinGapBytes'] = duration.burst.gap
            args['EnableInterBurstGap'] = True
            inter_burst_gap = duration.burst.inter_burst_gap
            if inter_burst_gap.choice is not None:
                value = getattr(inter_burst_gap, inter_burst_gap.choice, None)
                if value is None:
                    raise Exception("Inter packet gap mush be of type some <int> value")
                if inter_burst_gap.choice == 'microseconds':
                    args['InterBurstGap'] = value * 1000
                    args['InterBurstGapUnits'] = 'nanoseconds'
                else:
                    args['InterBurstGap'] = value
                    args['InterBurstGapUnits'] = inter_burst_gap.choice
        self._update(ixn_tx_control, **args)

    def transmit(self, request):
        """Set flow transmit
        1) If start then start any device protocols that are traffic dependent
        2) If start then generate and apply traffic
        3) Execute requested transmit action (start|stop|pause|resume)
        """
        regex = ''
        flow_names = [flow.name for flow in self._api._config.flows]
        if request and request.flow_names:
            flow_names = request.flow_names
        if len(flow_names) == 1:
            regex = '^%s$' % self._api.special_char(flow_names)[0]
        elif len(flow_names) > 1:
            regex = '^(%s)$' % '|'.join(
                self._api.special_char(flow_names)
            )

        if request.state == 'start':
            if len(self._api._topology.find()) > 0:
                with Timer(self._api, 'Devices start'):
                    self._api._ixnetwork.StartAllProtocols('sync')
                    self._api.check_protocol_statistics()
            if len(self._api._traffic_item.find()) == 0:
                return
            self._api._traffic_item.find(State='^unapplied$')
            if len(self._api._traffic_item) > 0:
                with Timer(self._api, 'Flows generate/apply'):
                    self._api._traffic_item.Generate()
                    self._api._traffic.Apply()
            self._api._traffic_item.find(State='^started$')
            if len(self._api._traffic_item) == 0:
                with Timer(self._api, 'Flows clear statistics'):
                    self._api._ixnetwork.ClearStats(
                        ['waitForPortStatsRefresh', 'waitForTrafficStatsRefresh'])
            self._api.capture._start_capture()
        self._api._traffic_item.find(Name=regex)
        if len(self._api._traffic_item) > 0:
            if request.state == 'start':
                self._api._traffic_item.find(Name=regex, Suspend=True,
                                             State='^started$')
                if len(self._api._traffic_item) > 0:
                    with Timer(self._api, 'Flows resume'):
                        self._api._traffic_item.PauseStatelessTraffic(False)
                self._api._traffic_item.find(Name=regex, State='^stopped$')
                if len(self._api._traffic_item) > 0:
                    with Timer(self._api, 'Flows start'):
                        self._api._traffic_item.StartStatelessTrafficBlocking()
            elif request.state == 'stop':
                self._api._traffic_item.find(Name=regex, State='^started$')
                if len(self._api._traffic_item) > 0:
                    with Timer(self._api, 'Flows stop'):
                        self._api._traffic_item.StopStatelessTrafficBlocking()
            elif request.state == 'pause':
                self._api._traffic_item.find(Name=regex, State='^started$')
                if len(self._api._traffic_item) > 0:
                    with Timer(self._api, 'Flows pause'):
                        self._api._traffic_item.PauseStatelessTraffic(True)
        if request.state == 'stop':
            if len(self._api._topology.find()) > 0:
                with Timer(self._api, 'Devices stop'):
                    self._api._ixnetwork.StopAllProtocols('sync')

    def _set_result_value(self,
                          row,
                          column_name,
                          column_value,
                          column_type=str):
        if len(self._column_names
               ) > 0 and column_name not in self._column_names:
            return
        try:
            row[column_name] = column_type(column_value)
        except:
            if column_type.__name__ in ['float', 'int']:
                row[column_name] = 0
            else:
                row[column_type] = column_value

    def _get_state(self, state):
        """IxNetwork traffic item states
            error, locked, started, 
            startedWaitingForStats, startedWaitingForStreams, stopped, 
            stoppedWaitingForStats, txStopWatchExpected, unapplied
        """
        started_states = [
            'txStopWatchExpected', 'locked', 'started',
            'startedWaitingForStats', 'startedWaitingForStreams',
            'stoppedWaitingForStats'
        ]
        if state in started_states:
            return 'started'
        else:
            return 'stopped'
    
    def results(self, request):
        """Return flow results
        """
        # setup parameters
        self._column_names = request._properties.get('metric_names')
        if self._column_names is None:
            self._column_names = []
        elif not isinstance(self._column_names, list):
            msg = "Invalid format of column_names passed {},\
                    expected list".format(self._column_names)
            raise Exception(msg)

        flow_names = request._properties.get('flow_names')
        has_request_flow = True
        if flow_names is None or len(flow_names) == 0:
            has_request_flow = False
            flow_names = [flow.name for flow in self._api._config.flows]
        elif not isinstance(flow_names, list):
            msg = "Invalid format of flow_names passed {},\
                    expected list".format(flow_names)
            raise Exception(msg)
        final_flow_names = []
        for flow in self._api._config.flows:
            metrics = flow._properties.get('metrics')
            if metrics is None:
                continue
            if metrics.enable is True \
                    and flow.name in flow_names:
                final_flow_names.append(flow.name)
        if len(final_flow_names) == 0:
            msg = """
            To fetch flow metrics at least one flow shall have metric enabled
            """
            raise Exception(msg.strip())
        diff = set(flow_names).difference(final_flow_names)
        if len(diff) > 0 and has_request_flow is True:
            raise Exception("Flow metrics is not enabled on flows {}".format(
                    "".join(list(diff))))
        flow_names = final_flow_names
        regfilter = {'property': 'name', 'regex': '.*'}
        regfilter['regex'] = '^(%s)$' % '|'.join(
            self._api.special_char(flow_names)
        )

        flow_count = len(flow_names)
        ixn_page = self._api._ixnetwork.Statistics.View.find(Caption="Flow Statistics").Page
        if ixn_page.PageSize < flow_count:
            ixn_page.PageSize = flow_count

        # initialize result values
        flow_rows = {}
        for traffic_item in self._api.select_traffic_items(
                traffic_item_filters=[regfilter]).values():
            for stream in traffic_item['highLevelStream']:
                for rx_port_name in stream['rxPortNames']:
                    flow_row = {}
                    self._set_result_value(flow_row, 'name', traffic_item['name'])
                    self._set_result_value(flow_row, 'transmit', self._get_state(traffic_item['state']))
                    self._set_result_value(flow_row, 'port_tx', stream['txPortName'])
                    self._set_result_value(flow_row, 'port_rx', rx_port_name)
                    # init all columns with corresponding zero-values so that
                    # the underlying dictionary contains all requested columns
                    # in an event of unwanted exceptions
                    for external_name, _, external_type in self._RESULT_COLUMNS:
                        self._set_result_value(
                            flow_row, external_name, 0, external_type
                        )
                    flow_rows[traffic_item['name'] + stream['txPortName'] + rx_port_name] = flow_row
                    
        flow_stat = self._api.assistant.StatViewAssistant(
            'Flow Statistics')
        for row in flow_stat.Rows:
            name = row['Traffic Item']
            if len(flow_names) > 0 and name not in flow_names:
                continue
            tx_port = row['Tx Port']
            rx_port = row['Rx Port']
            if name + tx_port + rx_port in flow_rows:
                flow_row = flow_rows[name + tx_port + rx_port]
                if float(row['Tx Frame Rate']) > 0 or int(row['Tx Frames']) == 0:
                    flow_row['transmit'] = 'started'
                else:
                    flow_row['transmit'] = 'stopped'
                for external_name, internal_name, external_type in self._RESULT_COLUMNS:
                    # keep plugging values for next columns even if the
                    # current one raises exception
                    try:
                        self._set_result_value(
                            flow_row, external_name, row[internal_name],
                            external_type
                        )
                    except Exception:
                        # TODO print a warning maybe ?
                        pass
                if name in self.flows_has_latency:
                    self._construct_latency(flow_row, row)
                if name in self.flows_has_timestamp:
                    self._construct_timestamp(flow_row, row)
                if name not in self.flows_has_loss:
                    flow_row.pop('loss')
        return list(flow_rows.values())

    def _construct_latency(self, flow_row, row):
        if self.latency_mode == 'store_forward':
            latency_map = TrafficItem._RESULT_LATENCY_STORE_FORWARD
        else:
            latency_map = TrafficItem._RESULT_LATENCY_CUT_THROUGH
        latency_result = {}
        for external_name, internal_name, external_type in latency_map:
            if internal_name not in row.Columns:
                raise Exception("Could not fetch column %s in latency metrics" %internal_name)
            try:
                self._set_result_value(latency_result, external_name,
                                       row[internal_name], external_type)
            except Exception as exception_err:
                self._api.warning("Could not fetch latency metrics: %s" % exception_err)
                pass
        if len(latency_result) > 0:
            flow_row['latency'] = latency_result
    
    def _construct_timestamp(self, flow_row, row):
        timestamp_result = {}
        for external_name, internal_name, external_type in TrafficItem._RESULT_TIMESTAMP:
            if internal_name not in row.Columns:
                raise Exception("Could not fetch column %s in timestamp metrics" %internal_name)
            
            try:
                val = row[internal_name].split(':')
                mul = [3600, 60, 1]
                sv = sum([
                    int(float(v) * 10 ** 9 + 0.1) * mul[i]
                    for i, v in enumerate(val)
                ])
                self._set_result_value(timestamp_result, external_name,
                                       sv, external_type)
            except Exception as exception_err:
                self._api.warning("Could not fetch timestamp metrics: %s" % exception_err)
                pass
        if len(timestamp_result) > 0:
            flow_row['timestamps'] = timestamp_result