# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
BGP Monitoring Protocol draft-ietf-grow-bmp-07
"""

import struct

from os_ken.lib import addrconv
from os_ken.lib.packet import packet_base
from os_ken.lib.packet import stream_parser
from os_ken.lib.packet.bgp import BGPMessage
from os_ken.lib.type_desc import TypeDisp


VERSION = 3

BMP_MSG_ROUTE_MONITORING = 0
BMP_MSG_STATISTICS_REPORT = 1
BMP_MSG_PEER_DOWN_NOTIFICATION = 2
BMP_MSG_PEER_UP_NOTIFICATION = 3
BMP_MSG_INITIATION = 4
BMP_MSG_TERMINATION = 5

BMP_PEER_TYPE_GLOBAL = 0
BMP_PEER_TYPE_L3VPN = 1

BMP_INIT_TYPE_STRING = 0
BMP_INIT_TYPE_SYSDESCR = 1
BMP_INIT_TYPE_SYSNAME = 2

BMP_TERM_TYPE_STRING = 0
BMP_TERM_TYPE_REASON = 1

BMP_TERM_REASON_ADMIN = 0
BMP_TERM_REASON_UNSPEC = 1
BMP_TERM_REASON_OUT_OF_RESOURCE = 2
BMP_TERM_REASON_REDUNDANT_CONNECTION = 3

BMP_STAT_TYPE_REJECTED = 0
BMP_STAT_TYPE_DUPLICATE_PREFIX = 1
BMP_STAT_TYPE_DUPLICATE_WITHDRAW = 2
BMP_STAT_TYPE_INV_UPDATE_DUE_TO_CLUSTER_LIST_LOOP = 3
BMP_STAT_TYPE_INV_UPDATE_DUE_TO_AS_PATH_LOOP = 4
BMP_STAT_TYPE_INV_UPDATE_DUE_TO_ORIGINATOR_ID = 5
BMP_STAT_TYPE_INV_UPDATE_DUE_TO_AS_CONFED_LOOP = 6
BMP_STAT_TYPE_ADJ_RIB_IN = 7
BMP_STAT_TYPE_LOC_RIB = 8
BMP_STAT_TYPE_ADJ_RIB_OUT = 14
BMP_STAT_TYPE_EXPORT_RIB = 15

BMP_PEER_DOWN_REASON_UNKNOWN = 0
BMP_PEER_DOWN_REASON_LOCAL_BGP_NOTIFICATION = 1
BMP_PEER_DOWN_REASON_LOCAL_NO_NOTIFICATION = 2
BMP_PEER_DOWN_REASON_REMOTE_BGP_NOTIFICATION = 3
BMP_PEER_DOWN_REASON_REMOTE_NO_NOTIFICATION = 4


class BMPMessage(packet_base.PacketBase, TypeDisp):
    r"""Base class for BGP Monitoring Protocol messages.

    An instance has the following attributes at least.
    Most of them are same to the on-wire counterparts but in host byte
    order.
    __init__ takes the corresponding args in this order.

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    ========================== ===============================================
    """

    _HDR_PACK_STR = '!BIB'  # version, padding, len, type, padding
    _HDR_LEN = struct.calcsize(_HDR_PACK_STR)

    def __init__(self, type_, len_=None, version=VERSION):
        self.version = version
        self.len = len_
        self.type = type_

    @classmethod
    def parse_header(cls, buf):
        if len(buf) < cls._HDR_LEN:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), cls._HDR_LEN))
        (version, len_, type_) = struct.unpack_from(cls._HDR_PACK_STR,
                                                    bytes(buf))

        return version, len_, type_

    @classmethod
    def parser(cls, buf):
        version, msglen, type_ = cls.parse_header(buf)

        if version != VERSION:
            raise ValueError("not supportted bmp version: %d" % version)

        if len(buf) < msglen:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), msglen))

        binmsg = buf[cls._HDR_LEN:msglen]
        rest = buf[msglen:]
        subcls = cls._lookup_type(type_)

        if subcls == cls._UNKNOWN_TYPE:
            raise ValueError("unknown bmp type: %d" % type_)

        kwargs = subcls.parser(binmsg)
        return subcls(len_=msglen,
                      type_=type_, version=version, **kwargs), rest

    def serialize(self):
        # fixup
        tail = self.serialize_tail()
        self.len = self._HDR_LEN + len(tail)

        hdr = bytearray(struct.pack(self._HDR_PACK_STR, self.version,
                                    self.len, self.type))
        return hdr + tail

    def __len__(self):
        # XXX destructive
        buf = self.serialize()
        return len(buf)


class BMPPeerMessage(BMPMessage):
    r"""BMP Message with Per Peer Header

    Following BMP Messages contain Per Peer Header after Common BMP Header.

    - BMP_MSG_TYPE_ROUTE_MONITRING
    - BMP_MSG_TYPE_STATISTICS_REPORT
    - BMP_MSG_PEER_UP_NOTIFICATION

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    peer_type                  The type of the peer.
    is_post_policy             Indicate the message reflects the post-policy
    is_adj_rib_out             Indicate the message reflects Adj-RIB-Out (defaults
                               to Adj-RIB-In)
    peer_distinguisher         Use for L3VPN router which can have multiple
                               instance.
    peer_address               The remote IP address associated with the TCP
                               session.
    peer_as                    The Autonomous System number of the peer.
    peer_bgp_id                The BGP Identifier of the peer
    timestamp                  The time when the encapsulated routes were
                               received.
    ========================== ===============================================
    """

    _PEER_HDR_PACK_STR = '!BBQ16sI4sII'
    _TYPE = {
        'ascii': [
            'peer_address',
            'peer_bgp_id'
        ]
    }

    def __init__(self, peer_type, is_post_policy, peer_distinguisher,
                 peer_address, peer_as, peer_bgp_id, timestamp,
                 version=VERSION, type_=None, len_=None, is_adj_rib_out=False):
        super(BMPPeerMessage, self).__init__(version=version,
                                             len_=len_,
                                             type_=type_)
        self.peer_type = peer_type
        self.is_post_policy = is_post_policy
        self.is_adj_rib_out = is_adj_rib_out
        self.peer_distinguisher = peer_distinguisher
        self.peer_address = peer_address
        self.peer_as = peer_as
        self.peer_bgp_id = peer_bgp_id
        self.timestamp = timestamp

    @classmethod
    def parser(cls, buf):
        (peer_type, peer_flags, peer_distinguisher,
         peer_address, peer_as, peer_bgp_id,
         timestamp1, timestamp2) = struct.unpack_from(cls._PEER_HDR_PACK_STR,
                                                      bytes(buf))

        rest = buf[struct.calcsize(cls._PEER_HDR_PACK_STR):]

        if peer_flags & (1 << 4):
            is_adj_rib_out = True
        else:
            is_adj_rib_out = False

        if peer_flags & (1 << 6):
            is_post_policy = True
        else:
            is_post_policy = False

        if peer_flags & (1 << 7):
            peer_address = addrconv.ipv6.bin_to_text(peer_address)
        else:
            peer_address = addrconv.ipv4.bin_to_text(peer_address[-4:])

        peer_bgp_id = addrconv.ipv4.bin_to_text(peer_bgp_id)

        timestamp = float(timestamp1) + timestamp2 * (10 ** -6)

        return {
            "peer_type": peer_type,
            "is_post_policy": is_post_policy,
            "peer_distinguisher": peer_distinguisher,
            "peer_address": peer_address,
            "peer_as": peer_as,
            "peer_bgp_id": peer_bgp_id,
            "timestamp": timestamp,
            "is_adj_rib_out": is_adj_rib_out,
        }, rest

    def serialize_tail(self):
        flags = 0

        if self.is_adj_rib_out:
            flags |= (1 << 4)

        if self.is_post_policy:
            flags |= (1 << 6)

        if ':' in self.peer_address:
            flags |= (1 << 7)
            peer_address = addrconv.ipv6.text_to_bin(self.peer_address)
        else:
            peer_address = struct.pack(
                '!12x4s', addrconv.ipv4.text_to_bin(self.peer_address))

        peer_bgp_id = addrconv.ipv4.text_to_bin(self.peer_bgp_id)

        t1, t2 = [int(t) for t in ("%.6f" % self.timestamp).split('.')]

        msg = bytearray(struct.pack(self._PEER_HDR_PACK_STR, self.peer_type,
                                    flags, self.peer_distinguisher,
                                    peer_address, self.peer_as,
                                    peer_bgp_id, t1, t2))
        return msg


@BMPMessage.register_type(BMP_MSG_ROUTE_MONITORING)
class BMPRouteMonitoring(BMPPeerMessage):
    r"""BMP Route Monitoring Message

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    peer_type                  The type of the peer.
    peer_flags                 Provide more information about the peer.
    peer_distinguisher         Use for L3VPN router which can have multiple
                               instance.
    peer_address               The remote IP address associated with the TCP
                               session.
    peer_as                    The Autonomous System number of the peer.
    peer_bgp_id                The BGP Identifier of the peer
    timestamp                  The time when the encapsulated routes were
                               received.
    bgp_update                 BGP Update PDU
    ========================== ===============================================
    """

    def __init__(self, bgp_update, peer_type, is_post_policy,
                 peer_distinguisher, peer_address, peer_as, peer_bgp_id,
                 timestamp, version=VERSION, type_=BMP_MSG_ROUTE_MONITORING,
                 len_=None, is_adj_rib_out=False):
        super(BMPRouteMonitoring,
              self).__init__(peer_type=peer_type,
                             is_post_policy=is_post_policy,
                             peer_distinguisher=peer_distinguisher,
                             peer_address=peer_address,
                             peer_as=peer_as,
                             peer_bgp_id=peer_bgp_id,
                             timestamp=timestamp,
                             len_=len_,
                             type_=type_,
                             version=version,
                             is_adj_rib_out=is_adj_rib_out)
        self.bgp_update = bgp_update

    @classmethod
    def parser(cls, buf):
        kwargs, buf = super(BMPRouteMonitoring, cls).parser(buf)

        bgp_update, _, buf = BGPMessage.parser(buf)

        kwargs['bgp_update'] = bgp_update

        return kwargs

    def serialize_tail(self):
        msg = super(BMPRouteMonitoring, self).serialize_tail()
        msg += self.bgp_update.serialize()

        return msg


@BMPMessage.register_type(BMP_MSG_STATISTICS_REPORT)
class BMPStatisticsReport(BMPPeerMessage):
    r"""BMP Statistics Report Message

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    peer_type                  The type of the peer.
    peer_flags                 Provide more information about the peer.
    peer_distinguisher         Use for L3VPN router which can have multiple
                               instance.
    peer_address               The remote IP address associated with the TCP
                               session.
    peer_as                    The Autonomous System number of the peer.
    peer_bgp_id                The BGP Identifier of the peer
    timestamp                  The time when the encapsulated routes were
                               received.
    stats                      Statistics (one or more stats encoded as a TLV)
    ========================== ===============================================
    """

    _TLV_PACK_STR = '!HH'
    _MIN_LEN = struct.calcsize(_TLV_PACK_STR)

    def __init__(self, stats, peer_type, is_post_policy, peer_distinguisher,
                 peer_address, peer_as, peer_bgp_id, timestamp,
                 version=VERSION, type_=BMP_MSG_STATISTICS_REPORT, len_=None,
                 is_adj_rib_out=False):
        super(BMPStatisticsReport,
              self).__init__(peer_type=peer_type,
                             is_post_policy=is_post_policy,
                             peer_distinguisher=peer_distinguisher,
                             peer_address=peer_address,
                             peer_as=peer_as,
                             peer_bgp_id=peer_bgp_id,
                             timestamp=timestamp,
                             len_=len_,
                             type_=type_,
                             version=version,
                             is_adj_rib_out=is_adj_rib_out)
        self.stats = stats

    @classmethod
    def parser(cls, buf):
        kwargs, rest = super(BMPStatisticsReport, cls).parser(buf)

        stats_count, = struct.unpack_from('!I', bytes(rest))

        buf = rest[struct.calcsize('!I'):]

        stats = []

        while len(buf):
            if len(buf) < cls._MIN_LEN:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._MIN_LEN))
            (type_, len_) = struct.unpack_from(cls._TLV_PACK_STR,
                                               bytes(buf))

            if len(buf) < (cls._MIN_LEN + len_):
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._MIN_LEN + len_))

            value = buf[cls._MIN_LEN:cls._MIN_LEN + len_]

            if type_ == BMP_STAT_TYPE_REJECTED or \
               type_ == BMP_STAT_TYPE_DUPLICATE_PREFIX or \
               type_ == BMP_STAT_TYPE_DUPLICATE_WITHDRAW or \
               type_ == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_CLUSTER_LIST_LOOP or \
               type_ == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_AS_PATH_LOOP or \
               type_ == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_ORIGINATOR_ID or \
               type_ == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_AS_CONFED_LOOP:
                value, = struct.unpack_from('!I', bytes(value))
            elif type_ == BMP_STAT_TYPE_ADJ_RIB_IN or \
                    type_ == BMP_STAT_TYPE_LOC_RIB or \
                    type_ == BMP_STAT_TYPE_ADJ_RIB_OUT or \
                    type_ == BMP_STAT_TYPE_EXPORT_RIB:
                value, = struct.unpack_from('!Q', bytes(value))

            buf = buf[cls._MIN_LEN + len_:]

            stats.append({'type': type_, 'len': len_, 'value': value})

        kwargs['stats'] = stats

        return kwargs

    def serialize_tail(self):
        msg = super(BMPStatisticsReport, self).serialize_tail()

        stats_count = len(self.stats)

        msg += bytearray(struct.pack('!I', stats_count))

        for v in self.stats:
            t = v['type']
            if t == BMP_STAT_TYPE_REJECTED or \
               t == BMP_STAT_TYPE_DUPLICATE_PREFIX or \
               t == BMP_STAT_TYPE_DUPLICATE_WITHDRAW or \
               t == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_CLUSTER_LIST_LOOP or \
               t == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_AS_PATH_LOOP or \
               t == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_ORIGINATOR_ID or \
               t == BMP_STAT_TYPE_INV_UPDATE_DUE_TO_AS_CONFED_LOOP:
                valuepackstr = 'I'
            elif t == BMP_STAT_TYPE_ADJ_RIB_IN or \
                    t == BMP_STAT_TYPE_LOC_RIB or \
                    t == BMP_STAT_TYPE_ADJ_RIB_OUT or \
                    t == BMP_STAT_TYPE_EXPORT_RIB:
                valuepackstr = 'Q'
            else:
                continue

            v['len'] = struct.calcsize(valuepackstr)
            msg += bytearray(struct.pack(self._TLV_PACK_STR + valuepackstr,
                                         t, v['len'], v['value']))

        return msg


@BMPMessage.register_type(BMP_MSG_PEER_DOWN_NOTIFICATION)
class BMPPeerDownNotification(BMPPeerMessage):
    r"""BMP Peer Down Notification Message

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    reason                     Reason indicates why the session was closed.
    data                       vary by the reason.
    ========================== ===============================================
    """

    def __init__(self, reason, data, peer_type, is_post_policy,
                 peer_distinguisher, peer_address, peer_as, peer_bgp_id,
                 timestamp, version=VERSION,
                 type_=BMP_MSG_PEER_DOWN_NOTIFICATION, len_=None,
                 is_adj_rib_out=False):

        super(BMPPeerDownNotification,
              self).__init__(peer_type=peer_type,
                             is_post_policy=is_post_policy,
                             peer_distinguisher=peer_distinguisher,
                             peer_address=peer_address,
                             peer_as=peer_as,
                             peer_bgp_id=peer_bgp_id,
                             timestamp=timestamp,
                             len_=len_,
                             type_=type_,
                             version=version,
                             is_adj_rib_out=is_adj_rib_out)

        self.reason = reason
        self.data = data

    @classmethod
    def parser(cls, buf):
        kwargs, buf = super(BMPPeerDownNotification, cls).parser(buf)
        reason, = struct.unpack_from('!B', bytes(buf))
        buf = buf[struct.calcsize('!B'):]

        if reason == BMP_PEER_DOWN_REASON_LOCAL_BGP_NOTIFICATION:
            data, _, rest = BGPMessage.parser(buf)
        elif reason == BMP_PEER_DOWN_REASON_LOCAL_NO_NOTIFICATION:
            data = struct.unpack_from('!H', bytes(buf))
        elif reason == BMP_PEER_DOWN_REASON_REMOTE_BGP_NOTIFICATION:
            data, _, rest = BGPMessage.parser(buf)
        elif reason == BMP_PEER_DOWN_REASON_REMOTE_NO_NOTIFICATION:
            data = None
        else:
            reason = BMP_PEER_DOWN_REASON_UNKNOWN
            data = buf

        kwargs['reason'] = reason
        kwargs['data'] = data

        return kwargs

    def serialize_tail(self):
        msg = super(BMPPeerDownNotification, self).serialize_tail()
        msg += struct.pack('!B', self.reason)

        if self.reason == BMP_PEER_DOWN_REASON_LOCAL_BGP_NOTIFICATION:
            msg += self.data.serialize()
        elif self.reason == BMP_PEER_DOWN_REASON_LOCAL_NO_NOTIFICATION:
            msg += struct.pack('!H', self.data)
        elif self.reason == BMP_PEER_DOWN_REASON_REMOTE_BGP_NOTIFICATION:
            msg += self.data.serialize()
        elif self.reason == BMP_PEER_DOWN_REASON_UNKNOWN:
            msg += str(self.data)

        return msg


@BMPMessage.register_type(BMP_MSG_PEER_UP_NOTIFICATION)
class BMPPeerUpNotification(BMPPeerMessage):
    r"""BMP Peer Up Notification Message

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    peer_type                  The type of the peer.
    peer_flags                 Provide more information about the peer.
    peer_distinguisher         Use for L3VPN router which can have multiple
                               instance.
    peer_address               The remote IP address associated with the TCP
                               session.
    peer_as                    The Autonomous System number of the peer.
    peer_bgp_id                The BGP Identifier of the peer
    timestamp                  The time when the encapsulated routes were
                               received.
    local_address              The local IP address associated with the
                               peering TCP session.
    local_port                 The local port number associated with the
                               peering TCP session.
    remote_port                The remote port number associated with the
                               peering TCP session.
    sent_open_message          The full OPEN message transmitted by the
                               monitored router to its peer.
    received_open_message      The full OPEN message received by the monitored
                               router from its peer.
    ========================== ===============================================
    """

    _PACK_STR = '!16sHH'
    _MIN_LEN = struct.calcsize(_PACK_STR)

    def __init__(self, local_address, local_port, remote_port,
                 sent_open_message, received_open_message,
                 peer_type, is_post_policy, peer_distinguisher,
                 peer_address, peer_as, peer_bgp_id, timestamp,
                 version=VERSION, type_=BMP_MSG_PEER_UP_NOTIFICATION,
                 len_=None, is_adj_rib_out=False):
        super(BMPPeerUpNotification,
              self).__init__(peer_type=peer_type,
                             is_post_policy=is_post_policy,
                             peer_distinguisher=peer_distinguisher,
                             peer_address=peer_address,
                             peer_as=peer_as,
                             peer_bgp_id=peer_bgp_id,
                             timestamp=timestamp,
                             len_=len_,
                             type_=type_,
                             version=version,
                             is_adj_rib_out=is_adj_rib_out)
        self.local_address = local_address
        self.local_port = local_port
        self.remote_port = remote_port
        self.sent_open_message = sent_open_message
        self.received_open_message = received_open_message

    @classmethod
    def parser(cls, buf):
        kwargs, rest = super(BMPPeerUpNotification, cls).parser(buf)

        (local_address, local_port,
         remote_port) = struct.unpack_from(cls._PACK_STR, bytes(rest))

        if '.' in kwargs['peer_address']:
            local_address = addrconv.ipv4.bin_to_text(local_address[-4:])
        elif ':' in kwargs['peer_address']:
            local_address = addrconv.ipv6.bin_to_text(local_address)
        else:
            raise ValueError("invalid local_address: %s" % local_address)

        kwargs['local_address'] = local_address
        kwargs['local_port'] = local_port
        kwargs['remote_port'] = remote_port

        rest = rest[cls._MIN_LEN:]

        sent_open_msg, _, rest = BGPMessage.parser(rest)
        received_open_msg, _, rest = BGPMessage.parser(rest)

        kwargs['sent_open_message'] = sent_open_msg
        kwargs['received_open_message'] = received_open_msg

        return kwargs

    def serialize_tail(self):
        msg = super(BMPPeerUpNotification, self).serialize_tail()

        if '.' in self.local_address:
            local_address = struct.pack(
                '!12x4s', addrconv.ipv4.text_to_bin(self.local_address))
        elif ':' in self.local_address:
            local_address = addrconv.ipv6.text_to_bin(self.local_address)
        else:
            raise ValueError("invalid local_address: %s" % self.local_address)

        msg += struct.pack(self._PACK_STR, local_address,
                           self.local_port, self.remote_port)

        msg += self.sent_open_message.serialize()
        msg += self.received_open_message.serialize()

        return msg


@BMPMessage.register_type(BMP_MSG_INITIATION)
class BMPInitiation(BMPMessage):
    r"""BMP Initiation Message

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    info                       One or more piece of information encoded as a
                               TLV
    ========================== ===============================================
    """

    _TLV_PACK_STR = '!HH'
    _MIN_LEN = struct.calcsize(_TLV_PACK_STR)

    def __init__(self, info, type_=BMP_MSG_INITIATION, len_=None,
                 version=VERSION):
        super(BMPInitiation, self).__init__(type_, len_, version)
        self.info = info

    @classmethod
    def parser(cls, buf):
        info = []
        while len(buf):
            if len(buf) < cls._MIN_LEN:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._MIN_LEN))
            (type_, len_) = struct.unpack_from(cls._TLV_PACK_STR,
                                               bytes(buf))

            if len(buf) < (cls._MIN_LEN + len_):
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._MIN_LEN + len_))

            value = buf[cls._MIN_LEN:cls._MIN_LEN + len_]

            if type_ == BMP_INIT_TYPE_STRING:
                value = value.decode('utf-8')

            buf = buf[cls._MIN_LEN + len_:]

            info.append({'type': type_, 'len': len_, 'value': value})

        return {'info': info}

    def serialize_tail(self):
        msg = bytearray()

        for v in self.info:
            if v['type'] == BMP_INIT_TYPE_STRING:
                value = v['value'].encode('utf-8')
            else:
                value = v['value']

            v['len'] = len(value)
            msg += struct.pack(self._TLV_PACK_STR, v['type'], v['len'])
            msg += value

        return msg


@BMPMessage.register_type(BMP_MSG_TERMINATION)
class BMPTermination(BMPMessage):
    r"""BMP Termination Message

    ========================== ===============================================
    Attribute                  Description
    ========================== ===============================================
    version                    Version. this packet lib defines BMP ver. 3
    len                        Length field.  Ignored when encoding.
    type                       Type field.  one of BMP\_MSG\_ constants.
    info                       One or more piece of information encoded as a
                               TLV
    ========================== ===============================================
    """

    _TLV_PACK_STR = '!HH'
    _MIN_LEN = struct.calcsize(_TLV_PACK_STR)

    def __init__(self, info, type_=BMP_MSG_TERMINATION, len_=None,
                 version=VERSION):
        super(BMPTermination, self).__init__(type_, len_, version)
        self.info = info

    @classmethod
    def parser(cls, buf):
        info = []
        while len(buf):
            if len(buf) < cls._MIN_LEN:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._MIN_LEN))
            (type_, len_) = struct.unpack_from(cls._TLV_PACK_STR,
                                               bytes(buf))

            if len(buf) < (cls._MIN_LEN + len_):
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._MIN_LEN + len_))

            value = buf[cls._MIN_LEN:cls._MIN_LEN + len_]
            if type_ == BMP_TERM_TYPE_STRING:
                value = value.decode('utf-8')
            elif type_ == BMP_TERM_TYPE_REASON:
                value, = struct.unpack_from('!H', bytes(value))

            buf = buf[cls._MIN_LEN + len_:]

            info.append({'type': type_, 'len': len_, 'value': value})

        return {'info': info}

    def serialize_tail(self):
        msg = bytearray()

        for v in self.info:
            if v['type'] == BMP_TERM_TYPE_STRING:
                value = v['value'].encode('utf-8')
            elif v['type'] == BMP_TERM_TYPE_REASON:
                value = struct.pack('!H', v['value'])
            v['len'] = len(value)
            msg += struct.pack(self._TLV_PACK_STR, v['type'], v['len'])
            msg += value

        return msg
