import struct
from collections import defaultdict
from types import GeneratorType

from . import DataError, Tag, CommError
from .bytes_ import (pack_dint, pack_uint, pack_udint, pack_usint, unpack_usint, unpack_uint, unpack_dint,
                     UNPACK_DATA_FUNCTION, PACK_DATA_FUNCTION, DATA_FUNCTION_SIZE, print_bytes_msg)
from .clx import LogixDriver
from .const import (SUCCESS, EXTENDED_SYMBOL, ENCAPSULATION_COMMAND, DATA_TYPE, BITS_PER_INT_TYPE,
                    REPLY_INFO, TAG_SERVICES_REQUEST, PADDING_BYTE, ELEMENT_ID, DATA_ITEM, ADDRESS_ITEM,
                    CLASS_ID, CLASS_CODE, INSTANCE_ID, INSUFFICIENT_PACKETS, REPLY_START, MULTISERVICE_READ_OVERHEAD,
                    MULTISERVICE_WRITE_OVERHEAD, TAG_SERVICES_REPLY, get_service_status, get_extended_status)


class LogixDriverLegacy(LogixDriver):

    def _send(self, message):
        """
        socket send
        :return: true if no error otherwise false
        """
        try:
            if self.debug:
                self.__log.debug(print_bytes_msg(message, '>>> SEND >>>'))
            self._sock.send(message)
        except Exception as e:
            raise CommError(e)

    def _receive(self):
        """
        socket receive
        :return: reply data
        """
        try:
            reply = self._sock.receive()
        except Exception as e:
            raise CommError(e)
        else:
            if self.debug:
                self.__log.debug(print_bytes_msg(reply, '<<< RECEIVE <<<'))
            return reply

    def _create_tag_rp(self, tag):
        """ Creates a request pad

        It returns the request packed wrapped around the tag passed.
        If any error it returns none
        """
        tags = tag.split('.')
        if tags:
            base, *attrs = tags

            if self.use_instance_ids and base in self.tags:
                rp = [CLASS_ID['8-bit'],
                      CLASS_CODE['Symbol Object'],
                      INSTANCE_ID['16-bit'], b'\x00',
                      pack_uint(self.tags[base]['instance_id'])]
            else:
                base_tag, index = self._find_tag_index(base)
                base_len = len(base_tag)
                rp = [EXTENDED_SYMBOL,
                      pack_usint(base_len),
                      base_tag]
                if base_len % 2:
                    rp.append(PADDING_BYTE)
                if index is None:
                    return None
                else:
                    rp += index

            for attr in attrs:
                attr, index = self._find_tag_index(attr)
                tag_length = len(attr)
                # Create the request path
                attr_path = [EXTENDED_SYMBOL,
                             pack_usint(tag_length),
                             attr]
                # Add pad byte because total length of Request path must be word-aligned
                if tag_length % 2:
                    attr_path.append(PADDING_BYTE)
                # Add any index
                if index is None:
                    return None
                else:
                    attr_path += index
                rp += attr_path

            # At this point the Request Path is completed,
            request_path = b''.join(rp)
            request_path = bytes([len(request_path) // 2]) + request_path

            return request_path

        return None

    def _check_reply(self, reply):
        """ check the replayed message for error

            return the status error if unsuccessful, else None
        """
        try:
            if reply is None:
                return f'{REPLY_INFO[unpack_dint(reply[:2])]} without reply'
            # Get the type of command
            typ = unpack_uint(reply[:2])

            # Encapsulation status check
            if unpack_dint(reply[8:12]) != SUCCESS:
                return get_service_status(unpack_dint(reply[8:12]))

            # Command Specific Status check
            if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]):
                status = unpack_usint(reply[42:43])
                if status != SUCCESS:
                    return f"send_rr_data reply:{get_service_status(status)} - " \
                           f"Extend status:{get_extended_status(reply, 42)}"
                else:
                    return None
            elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]):
                service = reply[46]
                status = _unit_data_status(reply)
                # return None
                if status == INSUFFICIENT_PACKETS and service in (TAG_SERVICES_REPLY['Read Tag'],
                                                                  TAG_SERVICES_REPLY['Multiple Service Packet'],
                                                                  TAG_SERVICES_REPLY['Read Tag Fragmented'],
                                                                  TAG_SERVICES_REPLY['Write Tag Fragmented'],
                                                                  TAG_SERVICES_REPLY['Get Instance Attributes List'],
                                                                  TAG_SERVICES_REPLY['Get Attributes']):
                    return None
                if status == SUCCESS:
                    return None

                return f"send_unit_data reply:{get_service_status(status)} - " \
                       f"Extend status:{get_extended_status(reply, 48)}"

        except Exception as e:
            raise DataError(e)

    def read_tag(self, *tags):
        """ read tag from a connected plc

        Possible combination can be passed to this method:
                - ('Counts') a single tag name
                - (['ControlWord']) a list with one tag or many
                - (['parts', 'ControlWord', 'Counts'])

        At the moment there is not a strong validation for the argument passed. The user should verify
        the correctness of the format passed.

        :return: None is returned in case of error otherwise the tag list is returned
        """

        if not self._forward_open():
            self.__log.warning("Target did not connected. read_tag will not be executed.")
            raise DataError("Target did not connected. read_tag will not be executed.")

        if len(tags) == 1:
            if isinstance(tags[0], (list, tuple, GeneratorType)):
                return self._read_tag_multi(tags[0])
            else:
                return self._read_tag_single(tags[0])
        else:
            return self._read_tag_multi(tags)

    def _read_tag_multi(self, tags):
        tag_bits = defaultdict(list)
        rp_list, tags_read = [[]], [[]]
        request_len = 0
        for tag in tags:
            tag, bit = self._prep_bools(tag, 'BOOL', bits_only=True)
            read = bit is None or tag not in tag_bits
            if bit is not None:
                tag_bits[tag].append(bit)
            if read:
                rp = self._create_tag_rp(tag)
                if rp is None:
                    raise DataError(f"Cannot create tag {tag} request packet. read_tag will not be executed.")
                else:
                    tag_req_len = len(rp) + MULTISERVICE_READ_OVERHEAD
                    if tag_req_len + request_len >= self.connection_size:
                        rp_list.append([])
                        tags_read.append([])
                        request_len = 0
                    rp_list[-1].append(bytes([TAG_SERVICES_REQUEST['Read Tag']]) + rp + b'\x01\x00')
                    tags_read[-1].append(tag)
                    request_len += tag_req_len

        replies = []
        for req_list, tags_ in zip(rp_list, tags_read):
            message_request = self.build_multiple_service(req_list, self._sequence())
            msg = self.build_common_packet_format(DATA_ITEM['Connected'], b''.join(message_request),
                                                  ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, )
            print(msg)
            success, reply = self.send_unit_data(msg)
            if not success:
                raise DataError(f"send_unit_data returned not valid data - {reply}")

            replies += self._parse_multiple_request_read(reply, tags_, tag_bits)
        return replies

    def _read_tag_single(self, tag):
        tag, bit = self._prep_bools(tag, 'BOOL', bits_only=True)
        rp = self._create_tag_rp(tag)
        if rp is None:
            self.__log.warning(f"Cannot create tag {tag} request packet. read_tag will not be executed.")
            return None
        else:
            # Creating the Message Request Packet
            message_request = [
                pack_uint(self._sequence()),
                bytes([TAG_SERVICES_REQUEST['Read Tag']]),  # the Request Service
                # bytes([len(rp) // 2]),  # the Request Path Size length in word
                rp,  # the request path
                b'\x01\x00',
            ]
        request = self.build_common_packet_format(DATA_ITEM['Connected'], b''.join(message_request),
                                                  ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid, )
        success, reply = self.send_unit_data(request)

        if success:
            data_type = unpack_uint(reply[50:52])
            typ = DATA_TYPE[data_type]
            try:
                value = UNPACK_DATA_FUNCTION[typ](reply[52:])
                if bit is not None:
                    value = bool(value & (1 << bit)) if bit < BITS_PER_INT_TYPE[typ] else None
                return Tag(tag, value, typ)
            except Exception as e:
                raise DataError(e)
        else:
            return Tag(tag, None, None, reply)

    @staticmethod
    def _parse_multiple_request_read(reply, tags, tag_bits=None):
        """ parse the message received from a multi request read:

        For each tag parsed, the information extracted includes the tag name, the value read and the data type.
        Those information are appended to the tag list as tuple

        :return: the tag list
        """
        offset = 50
        position = 50
        tag_bits = tag_bits or {}
        try:
            number_of_service_replies = unpack_uint(reply[offset:offset + 2])
            tag_list = []
            for index in range(number_of_service_replies):
                position += 2
                start = offset + unpack_uint(reply[position:position + 2])
                general_status = unpack_usint(reply[start + 2:start + 3])
                tag = tags[index]
                if general_status == SUCCESS:
                    typ = DATA_TYPE[unpack_uint(reply[start + 4:start + 6])]
                    value_begin = start + 6
                    value_end = value_begin + DATA_FUNCTION_SIZE[typ]
                    value = UNPACK_DATA_FUNCTION[typ](reply[value_begin:value_end])
                    if tag in tag_bits:
                        for bit in tag_bits[tag]:
                            val = bool(value & (1 << bit)) if bit < BITS_PER_INT_TYPE[typ] else None
                            tag_list.append(Tag(f'{tag}.{bit}', val, 'BOOL'))
                    else:
                        tag_list.append(Tag(tag, value, typ))
                else:
                    tag_list.append(Tag(tag, None, None, get_service_status(general_status)))

            return tag_list
        except Exception as e:
            raise DataError(e)

    def read_array(self, tag, counts, raw=False):
        """ read array of atomic data type from a connected plc

        At the moment there is not a strong validation for the argument passed. The user should verify
        the correctness of the format passed.

        :param tag: the name of the tag to read
        :param counts: the number of element to read
        :param raw: the value should output as raw-value (hex)
        :return: None is returned in case of error otherwise the tag list is returned
        """

        if not self._target_is_connected:
            if not self._forward_open():
                self.__log.warning("Target did not connected. read_tag will not be executed.")
                raise DataError("Target did not connected. read_tag will not be executed.")

        offset = 0
        last_idx = 0
        tags = b'' if raw else []

        while offset != -1:
            rp = self._create_tag_rp(tag)
            if rp is None:
                self.__log.warning(f"Cannot create tag {tag} request packet. read_tag will not be executed.")
                return None
            else:
                # Creating the Message Request Packet
                message_request = [
                    pack_uint(self._sequence()),
                    bytes([TAG_SERVICES_REQUEST["Read Tag Fragmented"]]),  # the Request Service
                    # bytes([len(rp) // 2]),  # the Request Path Size length in word
                    rp,  # the request path
                    pack_uint(counts),
                    pack_dint(offset)
                ]
            msg = self.build_common_packet_format(DATA_ITEM['Connected'],
                                                  b''.join(message_request),
                                                  ADDRESS_ITEM['Connection Based'],
                                                  addr_data=self._target_cid, )
            success, reply = self.send_unit_data(msg)
            if not success:
                raise DataError(f"send_unit_data returned not valid data - {reply}")

            last_idx, offset = self._parse_fragment(reply, last_idx, offset, tags, raw)

        return tags

    def _parse_fragment(self, reply, last_idx, offset, tags, raw=False):
        """ parse the fragment returned by a fragment service."""

        try:
            status = _unit_data_status(reply)
            data_type = unpack_uint(reply[REPLY_START:REPLY_START + 2])
            fragment_returned = reply[REPLY_START + 2:]
        except Exception as e:
            raise DataError(e)

        fragment_returned_length = len(fragment_returned)
        idx = 0
        while idx < fragment_returned_length:
            try:
                typ = DATA_TYPE[data_type]
                if raw:
                    value = fragment_returned[idx:idx + DATA_FUNCTION_SIZE[typ]]
                else:
                    value = UNPACK_DATA_FUNCTION[typ](fragment_returned[idx:idx + DATA_FUNCTION_SIZE[typ]])
                idx += DATA_FUNCTION_SIZE[typ]
            except Exception as e:
                raise DataError(e)
            if raw:
                tags += value
            else:
                tags.append((last_idx, value))
                last_idx += 1

        if status == SUCCESS:
            offset = -1
        elif status == 0x06:
            offset += fragment_returned_length
        else:
            self.__log.warning('{0}: {1}'.format(get_service_status(status), get_extended_status(reply, 48)))
            offset = -1

        return last_idx, offset

    @staticmethod
    def _prep_bools(tag, typ, bits_only=True):
        """
        if tag is a bool and a bit of an integer, returns the base tag and the bit value,
        else returns the tag name and None

        """
        if typ != 'BOOL':
            return tag, None
        if not bits_only and tag.endswith(']'):
            try:
                base, idx = tag[:-1].rsplit(sep='[', maxsplit=1)
                idx = int(idx)
                base = f'{base}[{idx // 32}]'
                return base, idx
            except Exception:
                return tag, None
        else:
            try:
                base, bit = tag.rsplit('.', maxsplit=1)
                bit = int(bit)
                return base, bit
            except Exception:
                return tag, None

    @staticmethod
    def _dword_to_boolarray(tag, bit):
        base, tmp = tag.rsplit(sep='[', maxsplit=1)
        i = int(tmp[:-1])
        return f'{base}[{(i * 32) + bit}]'

    def _write_tag_multi_write(self, tags):
        rp_list = [[]]
        tags_added = [[]]
        request_len = 0
        for name, value, typ in tags:
            name, bit = self._prep_bools(name, typ, bits_only=False)  # check if bool & if bit of int or bool array
            # Create the request path to wrap the tag name
            rp = self._create_tag_rp(name, multi_requests=True)
            if rp is None:
                self.__log.warning(f"Cannot create tag {tags} req. packet. write_tag will not be executed")
                return None
            else:
                try:
                    if bit is not None:  # then it is a boolean array
                        rp = self._create_tag_rp(name, multi_requests=True)
                        request = bytes([TAG_SERVICES_REQUEST["Read Modify Write Tag"]]) + rp
                        request += b''.join(self._make_write_bit_data(bit, value, bool_ary='[' in name))
                        if typ == 'BOOL' and name.endswith(']'):
                            name = self._dword_to_boolarray(name, bit)
                        else:
                            name = f'{name}.{bit}'
                    else:
                        request = (bytes([TAG_SERVICES_REQUEST["Write Tag"]]) +
                                   rp +
                                   pack_uint(DATA_TYPE[typ]) +
                                   b'\x01\x00' +
                                   PACK_DATA_FUNCTION[typ](value))

                    tag_req_len = len(request) + MULTISERVICE_WRITE_OVERHEAD
                    if tag_req_len + request_len >= self.connection_size:
                        rp_list.append([])
                        tags_added.append([])
                        request_len = 0
                    rp_list[-1].append(request)
                    request_len += tag_req_len
                except (LookupError, struct.error) as e:
                    self.__warning(f"Tag:{name} type:{typ} removed from write list. Error:{e}.")

                    # The tag in idx position need to be removed from the rp list because has some kind of error
                else:
                    tags_added[-1].append((name, value, typ))

        # Create the message request
        replies = []
        for req_list, tags_ in zip(rp_list, tags_added):
            message_request = self.build_multiple_service(req_list, self._sequence())
            msg = self.build_common_packet_format(DATA_ITEM['Connected'],
                                                  b''.join(message_request),
                                                  ADDRESS_ITEM['Connection Based'],
                                                  addr_data=self._target_cid, )
            success, reply = self.send_unit_data(msg)
            if success:
                replies += self._parse_multiple_request_write(tags_, reply)
            else:
                raise DataError(f"send_unit_data returned not valid data - {reply}")
        return replies

    def _write_tag_single_write(self, tag, value, typ):
        name, bit = self._prep_bools(tag, typ,
                                     bits_only=False)  # check if we're writing a bit of a integer rather than a BOOL

        rp = self._create_tag_rp(name)
        if rp is None:
            self.__log.warning(f"Cannot create tag {tag} request packet. write_tag will not be executed.")
            return None
        else:
            # Creating the Message Request Packet
            message_request = [
                pack_uint(self._sequence()),
                bytes([TAG_SERVICES_REQUEST["Read Modify Write Tag"]
                       if bit is not None else TAG_SERVICES_REQUEST["Write Tag"]]),
                # bytes([len(rp) // 2]),  # the Request Path Size length in word
                rp,  # the request path
            ]
            if bit is not None:
                try:
                    message_request += self._make_write_bit_data(bit, value, bool_ary='[' in name)
                except Exception as err:
                    raise DataError(f'Unable to write bit, invalid bit number {repr(err)}')
            else:
                message_request += [
                    pack_uint(DATA_TYPE[typ]),  # data type
                    pack_uint(1),  # Add the number of tag to write
                    PACK_DATA_FUNCTION[typ](value)
                ]
            request = self.build_common_packet_format(DATA_ITEM['Connected'], b''.join(message_request),
                                                      ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid)
            success, reply = self.send_unit_data(request)
            return Tag(tag, value, typ, None if success else reply)

    @staticmethod
    def _make_write_bit_data(bit, value, bool_ary=False):
        or_mask, and_mask = 0x00000000, 0xFFFFFFFF

        if bool_ary:
            mask_size = 4
            bit = bit % 32
        else:
            mask_size = 1 if bit < 8 else 2 if bit < 16 else 4

        if value:
            or_mask |= (1 << bit)
        else:
            and_mask &= ~(1 << bit)

        return [pack_uint(mask_size), pack_udint(or_mask)[:mask_size], pack_udint(and_mask)[:mask_size]]

    @staticmethod
    def _parse_multiple_request_write(tags, reply):
        """ parse the message received from a multi request writ:

        For each tag parsed, the information extracted includes the tag name and the status of the writing.
        Those information are appended to the tag list as tuple

        :return: the tag list
        """
        offset = 50
        position = 50

        try:
            number_of_service_replies = unpack_uint(reply[offset:offset + 2])
            tag_list = []
            for index in range(number_of_service_replies):
                position += 2
                start = offset + unpack_uint(reply[position:position + 2])
                general_status = unpack_usint(reply[start + 2:start + 3])
                error = None if general_status == SUCCESS else get_service_status(general_status)
                tag_list.append(Tag(*tags[index], error))
            return tag_list
        except Exception as e:
            raise DataError(e)

    def write_tag(self, tag, value=None, typ=None):
        """ write tag/tags from a connected plc

        Possible combination can be passed to this method:
                - ('tag name', Value, data type)  as single parameters or inside a tuple
                - ([('tag name', Value, data type), ('tag name2', Value, data type)]) as array of tuples

        At the moment there is not a strong validation for the argument passed. The user should verify
        the correctness of the format passed.

        The type accepted are:
            - BOOL
            - SINT
            - INT
            - DINT
            - REAL
            - LINT
            - BYTE
            - WORD
            - DWORD
            - LWORD

        :param tag: tag name, or an array of tuple containing (tag name, value, data type)
        :param value: the value to write or none if tag is an array of tuple or a tuple
        :param typ: the type of the tag to write or none if tag is an array of tuple or a tuple
        :return: None is returned in case of error otherwise the tag list is returned
        """

        if not self._target_is_connected:
            if not self._forward_open():
                self.__log.warning("Target did not connected. write_tag will not be executed.")
                raise DataError("Target did not connected. write_tag will not be executed.")

        if isinstance(tag, (list, tuple, GeneratorType)):
            return self._write_tag_multi_write(tag)
        else:
            if isinstance(tag, tuple):
                name, value, typ = tag
            else:
                name = tag
            return self._write_tag_single_write(name, value, typ)

    def write_array(self, tag, values, data_type, raw=False):
        """ write array of atomic data type from a connected plc
        At the moment there is not a strong validation for the argument passed. The user should verify
        the correctness of the format passed.
        :param tag: the name of the tag to read
        :param data_type: the type of tag to write
        :param values: the array of values to write, if raw: the frame with bytes
        :param raw: indicates that the values are given as raw values (hex)
        """

        if not isinstance(values, list):
            self.__log.warning("A list of tags must be passed to write_array.")
            raise DataError("A list of tags must be passed to write_array.")

        if not self._target_is_connected:
            if not self._forward_open():
                self.__log.warning("Target did not connected. write_array will not be executed.")
                raise DataError("Target did not connected. write_array will not be executed.")

        array_of_values = b''
        byte_size = 0
        byte_offset = 0

        for i, value in enumerate(values):
            array_of_values += value if raw else PACK_DATA_FUNCTION[data_type](value)
            byte_size += DATA_FUNCTION_SIZE[data_type]

            if byte_size >= 450 or i == len(values) - 1:
                # create the message and send the fragment
                rp = self._create_tag_rp(tag)
                if rp is None:
                    self.__log.warning(f"Cannot create tag {tag} request packet write_array will not be executed.")
                    return None
                else:
                    # Creating the Message Request Packet
                    message_request = [
                        pack_uint(self._sequence()),
                        bytes([TAG_SERVICES_REQUEST["Write Tag Fragmented"]]),  # the Request Service
                        bytes([len(rp) // 2]),  # the Request Path Size length in word
                        rp,  # the request path
                        pack_uint(DATA_TYPE[data_type]),  # Data type to write
                        pack_uint(len(values)),  # Number of elements to write
                        pack_dint(byte_offset),
                        array_of_values  # Fragment of elements to write
                    ]
                    byte_offset += byte_size

                msg = self.build_common_packet_format(
                    DATA_ITEM['Connected'],
                    b''.join(message_request),
                    ADDRESS_ITEM['Connection Based'],
                    addr_data=self._target_cid,
                )

                success, reply = self.send_unit_data(msg)
                if not success:
                    raise DataError(f"send_unit_data returned not valid data - {reply}")

                array_of_values = b''
                byte_size = 0
        return True

    def write_string(self, tag, value, size=82):
        """
            Rockwell define different string size:
                STRING  STRING_12   STRING_16   STRING_20   STRING_40   STRING_8
            by default we assume size 82 (STRING)
        """
        data_tag = ".".join((tag, "DATA"))
        len_tag = ".".join((tag, "LEN"))

        # create an empty array
        data_to_send = [0] * size
        for idx, val in enumerate(value):
            try:
                unsigned = ord(val)
                data_to_send[idx] = unsigned - 256 if unsigned > 127 else unsigned
            except IndexError:
                break

        str_len = len(value)
        if str_len > size:
            str_len = size

        result_len = self.write_tag(len_tag, str_len, 'DINT')
        result_data = self.write_array(data_tag, data_to_send, 'SINT')
        return result_data and result_len

    def read_string(self, tag, str_len=None):
        data_tag = f'{tag}.DATA'
        if str_len is None:
            len_tag = f'{tag}.LEN'
            tmp = self.read_tag(len_tag)
            length, _ = tmp or (None, None)
        else:
            length = str_len

        if length:
            values = self.read_array(data_tag, length)
            if values:
                _, values = zip(*values)
                chars = ''.join(chr(v + 256) if v < 0 else chr(v) for v in values)
                string, *_ = chars.split('\x00', maxsplit=1)
                return string
        return None

    def _check_reply(self, reply):
        raise NotImplementedError("The method has not been implemented")

    def nop(self):
        """ No replay command

        A NOP provides a way for either an originator or target to determine if the TCP connection is still open.
        """
        message = self.build_header(ENCAPSULATION_COMMAND['nop'], 0)
        self._send(message)

    def send_unit_data(self, message):
        """ SendUnitData send encapsulated connected messages.

        :param message: The message to be send to the target
        :return: the replay received from the target
        """
        msg = self.build_header(ENCAPSULATION_COMMAND["send_unit_data"], len(message))
        msg += message
        self._send(msg)
        reply = self._receive()
        status = self._check_reply(reply)
        return (True, reply) if status is None else (False, status)

    def build_header(self, command, length):
        """ Build the encapsulate message header

        The header is 24 bytes fixed length, and includes the command and the length of the optional data portion.

         :return: the header
        """
        try:
            h = command
            h += pack_uint(length)  # Length UINT
            h += pack_dint(self._session)  # Session Handle UDINT
            h += pack_dint(0)  # Status UDINT
            h += self.attribs['context']  # Sender Context 8 bytes
            h += pack_dint(self.attribs['option'])  # Option UDINT
            return h
        except Exception as e:
            raise CommError(e)

    @staticmethod
    def create_tag_rp(tag, multi_requests=False):
        """ Create tag Request Packet

        It returns the request packed wrapped around the tag passed.
        If any error it returns none
        """
        tags = tag.encode().split(b'.')
        rp = []
        index = []
        for tag in tags:
            add_index = False
            # Check if is an array tag
            if b'[' in tag:
                # Remove the last square bracket
                tag = tag[:len(tag) - 1]
                # Isolate the value inside bracket
                inside_value = tag[tag.find(b'[') + 1:]
                # Now split the inside value in case part of multidimensional array
                index = inside_value.split(b',')
                # Flag the existence of one o more index
                add_index = True
                # Get only the tag part
                tag = tag[:tag.find(b'[')]
            tag_length = len(tag)

            # Create the request path
            rp.append(EXTENDED_SYMBOL)  # ANSI Ext. symbolic segment
            rp.append(bytes([tag_length]))  # Length of the tag

            # Add the tag to the Request path
            rp += [bytes([char]) for char in tag]
            # Add pad byte because total length of Request path must be word-aligned
            if tag_length % 2:
                rp.append(PADDING_BYTE)
            # Add any index
            if add_index:
                for idx in index:
                    val = int(idx)
                    if val <= 0xff:
                        rp.append(ELEMENT_ID["8-bit"])
                        rp.append(pack_usint(val))
                    elif val <= 0xffff:
                        rp.append(ELEMENT_ID["16-bit"] + PADDING_BYTE)
                        rp.append(pack_uint(val))
                    elif val <= 0xfffffffff:
                        rp.append(ELEMENT_ID["32-bit"] + PADDING_BYTE)
                        rp.append(pack_dint(val))
                    else:
                        # Cannot create a valid request packet
                        return None

        # At this point the Request Path is completed,
        if multi_requests:
            request_path = bytes([len(rp) // 2]) + b''.join(rp)
        else:
            request_path = b''.join(rp)
        return request_path

    @staticmethod
    def build_common_packet_format(message_type, message, addr_type, addr_data=None, timeout=10):
        """ build_common_packet_format

        It creates the common part for a CIP message. Check Volume 2 (page 2.22) of CIP specification  for reference
        """
        msg = pack_dint(0)  # Interface Handle: shall be 0 for CIP
        msg += pack_uint(timeout)  # timeout
        msg += pack_uint(2)  # Item count: should be at list 2 (Address and Data)
        msg += addr_type  # Address Item Type ID

        if addr_data is not None:
            msg += pack_uint(len(addr_data))  # Address Item Length
            msg += addr_data
        else:
            msg += b'\x00\x00'  # Address Item Length
        msg += message_type  # Data Type ID
        msg += pack_uint(len(message))  # Data Item Length
        msg += message
        return msg

    @staticmethod
    def build_multiple_service(rp_list, sequence=None):
        mr = [
            bytes([TAG_SERVICES_REQUEST["Multiple Service Packet"]]),  # the Request Service
            pack_usint(2),  # the Request Path Size length in word
            CLASS_ID["8-bit"],
            CLASS_CODE["Message Router"],
            INSTANCE_ID["8-bit"],
            b'\x01',  # Instance 1
            pack_uint(len(rp_list))  # Number of service contained in the request
        ]
        if sequence is not None:
            mr.insert(0, pack_uint(sequence))
        # Offset calculation
        offset = (len(rp_list) * 2) + 2
        for index, rp in enumerate(rp_list):
            mr.append(pack_uint(offset))  # Starting offset
            offset += len(rp)

        mr += rp_list
        return mr

    @staticmethod
    def parse_multiple_request(message, tags, typ):
        """ parse_multi_request
        This function should be used to parse the message replayed to a multi request service rapped around the
        send_unit_data message.


        :param message: the full message returned from the PLC
        :param tags: The list of tags to be read
        :param typ: to specify if multi request service READ or WRITE
        :return: a list of tuple in the format [ (tag name, value, data type), ( tag name, value, data type) ].
                 In case of error the tuple will be (tag name, None, None)
        """
        offset = 50
        position = 50
        number_of_service_replies = unpack_uint(message[offset:offset + 2])
        tag_list = []
        for index in range(number_of_service_replies):
            position += 2
            start = offset + unpack_uint(message[position:position + 2])
            general_status = unpack_usint(message[start + 2:start + 3])

            if general_status == 0:
                if typ == "READ":
                    data_type = unpack_uint(message[start + 4:start + 6])
                    try:
                        value_begin = start + 6
                        value_end = value_begin + DATA_FUNCTION_SIZE[DATA_TYPE[data_type]]
                        value = message[value_begin:value_end]
                        tag_list.append((tags[index],
                                         UNPACK_DATA_FUNCTION[DATA_TYPE[data_type]](value),
                                         DATA_TYPE[data_type]))
                    except LookupError:
                        tag_list.append((tags[index], None, None))
                else:
                    tag_list.append((tags[index] + ('GOOD',)))
            else:
                if typ == "READ":
                    tag_list.append((tags[index], None, None))
                else:
                    tag_list.append((tags[index] + ('BAD',)))
        return tag_list


def _unit_data_status(reply):
    return unpack_usint(reply[48:49])

