# Copyright 2015-2017 Cisco Systems, Inc.
# All rights reserved.
#
#    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 Update Message"""

import struct
import traceback
import logging
import binascii

import netaddr

from yabgp.common import exception as excep
from yabgp.common import constants as bgp_cons
from yabgp.message.attribute import AttributeFlag
from yabgp.message.attribute.origin import Origin
from yabgp.message.attribute.aspath import ASPath
from yabgp.message.attribute.nexthop import NextHop
from yabgp.message.attribute.med import MED
from yabgp.message.attribute.localpref import LocalPreference
from yabgp.message.attribute.atomicaggregate import AtomicAggregate
from yabgp.message.attribute.aggregator import Aggregator
from yabgp.message.attribute.community import Community
from yabgp.message.attribute.originatorid import OriginatorID
from yabgp.message.attribute.clusterlist import ClusterList
from yabgp.message.attribute.mpreachnlri import MpReachNLRI
from yabgp.message.attribute.mpunreachnlri import MpUnReachNLRI
from yabgp.message.attribute.tunnelencaps import TunnelEncaps
from yabgp.message.attribute.extcommunity import ExtCommunity
from yabgp.message.attribute.pmsitunnel import PMSITunnel
from yabgp.message.attribute.linkstate.linkstate import LinkState
from yabgp.message.attribute.nlri.evpn import EVPN
from yabgp.message.attribute.largecommunity import LargeCommunity

LOG = logging.getLogger()


class Update(object):
    """
    An UPDATE message is used to advertise feasible routes that share
    common path attributes to a peer, or to withdraw multiple unfeasible
    routes from service (RFC 4271 page 15)
    """

    def __init__(self):
        """
        +----------------------------------------------------+
        |     Withdrawn Routes Length (2 octets)             |
        +----------------------------------------------------+
        |     Withdrawn Routes (variable)                    |
        +----------------------------------------------------+
        |     Total Path Attribute Length (2 octets)         |
        +----------------------------------------------------+
        |     Path Attributes (variable)                     |
        +----------------------------------------------------+
        |  Network Layer Reachability Information (variable) |
        +----------------------------------------------------+

        @ Withdrawn Routes Length:
            This 2-octets unsigned integer indicates the total length of
        the Withdrawn Routes field in octets. Its value allows the
        length of the Network Layer Reachability Information field to
        be determined, as specified below.
            A value of 0 indicates that no routes are being withdrawn from
        service, and that the WITHDRAWN ROUTES field is not present in
        this UPDATE message.

        @ Withdrawn Routes:
            This is a variable-length field that contains a list of IP
        address prefixes for the routes that are being withdrawn from
        service. Each IP address prefix is encoded as a 2-tuple of the
        form <length, prefix>, whose fields are described below:
                    +---------------------------+
                    |     Length (1 octet)      |
                    +---------------------------+
                    |     Prefix (variable)     |
                    +---------------------------+
            The use and the meaning of these fields are as follows:
            a) Length:
            The Length field indicates the length in bits of the IP
            address prefix. A length of zero indicates a prefix that
            matches all IP addresses (with prefix, itself, of zero
            octets).
            b) Prefix:
            The Prefix field contains an IP address prefix, followed by
            the minimum number of trailing bits needed to make the end
            of the field fall on an octet boundary. Note that the value
            of trailing bits is irrelevant.

        @ Total Path Attribute Length:
            This 2-octet unsigned integer indicates the total length of the
        Path Attributes field in octets. Its value allows the length
        of the Network Layer Reachability field to be determined as
        specified below.
            A value of 0 indicates that neither the Network Layer
        Reachability Information field nor the Path Attribute field is
        present in this UPDATE message.

        @ Path Attributes:
            (path attributes details see RFC 4271 and some other RFCs)

        @ Network Layer Reachability Information:
            This variable length field contains a list of IP address
        prefixes. The length, in octets, of the Network Layer
        Reachability Information is not encoded explicitly, but can be
        calculated as:
        UPDATE message Length - 23 - Total Path Attributes Length
        - Withdrawn Routes Length
        where UPDATE message Length is the value encoded in the fixedsize
        BGP header, Total Path Attribute Length, and Withdrawn
        Routes Length are the values encoded in the variable part of
        the UPDATE message, and 23 is a combined length of the fixedsize
        BGP header, the Total Path Attribute Length field, and the
        Withdrawn Routes Length field.
        Reachability information is encoded as one or more 2-tuples of
        the form <length, prefix>, whose fields are described below:
                    +---------------------------+
                    |      Length (1 octet)     |
                    +---------------------------+
                    |      Prefix (variable)    |
                    +---------------------------+
        The use and the meaning of these fields are as follows:
            a) Length:
                The Length field indicates the length in bits of the IP
            address prefix. A length of zero indicates a prefix that
            matches all IP addresses (with prefix, itself, of zero
            octets).
            b) Prefix:
                The Prefix field contains an IP address prefix, followed by
            enough trailing bits to make the end of the field fall on an
            octet boundary. Note that the value of the trailing bits is
            irrelevant.
        """

    @classmethod
    def parse(cls, t, msg_hex, asn4=False, add_path_remote=False, add_path_local=False):

        """
        Parse BGP Update message
        :param t: timestamp
        :param msg_hex: raw message
        :param asn4: support 4 bytes AS or not
        :param add_path_remote: if the remote peer can send add path NLRI
        :param add_path_local: if the local can send add path NLRI
        :return: message after parsing.
        """
        results = {
            "withdraw": [],
            "attr": None,
            "nlri": [],
            'time': t,
            'hex': msg_hex,
            'sub_error': None,
            'err_data': None}

        # get every part of the update message
        withdraw_len = struct.unpack('!H', msg_hex[:2])[0]
        withdraw_prefix_data = msg_hex[2:withdraw_len + 2]
        attr_len = struct.unpack('!H', msg_hex[withdraw_len + 2:withdraw_len + 4])[0]
        attribute_data = msg_hex[withdraw_len + 4:withdraw_len + 4 + attr_len]
        nlri_data = msg_hex[withdraw_len + 4 + attr_len:]
        try:
            # parse withdraw prefixes
            results['withdraw'] = cls.parse_prefix_list(withdraw_prefix_data, add_path_remote)

            # parse nlri
            results['nlri'] = cls.parse_prefix_list(nlri_data, add_path_remote)
        except Exception as e:
            LOG.error(e)
            error_str = traceback.format_exc()
            LOG.debug(error_str)
            results['sub_error'] = bgp_cons.ERR_MSG_UPDATE_INVALID_NETWORK_FIELD
            results['err_data'] = ''
        try:
            # parse attributes
            results['attr'] = cls.parse_attributes(attribute_data, asn4)
        except excep.UpdateMessageError as e:
            LOG.error(e)
            results['sub_error'] = e.sub_error
            results['err_data'] = e.data
        except Exception as e:
            LOG.error(e)
            error_str = traceback.format_exc()
            LOG.debug(error_str)
            results['sub_error'] = e
            results['err_data'] = e

        return results

    @classmethod
    def construct(cls, msg_dict, asn4=False, addpath=False):
        """construct BGP update message

        :param msg_dict: update message string
        :param asn4: support 4 bytes asn or not
        :param addpath: support add path or not
        """
        attr_hex = b''
        nlri_hex = b''
        withdraw_hex = b''
        if msg_dict.get('attr'):
            attr_hex = cls.construct_attributes(msg_dict['attr'], asn4)
        if msg_dict.get('nlri'):
            nlri_hex = cls.construct_prefix_v4(msg_dict['nlri'], addpath)
        if msg_dict.get('withdraw'):
            withdraw_hex = cls.construct_prefix_v4(msg_dict['withdraw'], addpath)
        if nlri_hex and attr_hex:
            msg_body = struct.pack('!H', 0) + struct.pack('!H', len(attr_hex)) + attr_hex + nlri_hex
            return cls.construct_header(msg_body)
        elif attr_hex and not nlri_hex:
            msg_body = struct.pack('!H', 0) + struct.pack('!H', len(attr_hex)) + attr_hex + nlri_hex
            return cls.construct_header(msg_body)
        elif withdraw_hex:
            msg_body = struct.pack('!H', len(withdraw_hex)) + withdraw_hex + struct.pack('!H', 0)
            return cls.construct_header(msg_body)

    @staticmethod
    def parse_prefix_list(data, addpath=False):
        """
        Parses an RFC4271 encoded blob of BGP prefixes into a list

        :param data: hex data
        :param addpath: support addpath or not
        :return: prefix_list
        """
        prefixes = []
        postfix = data
        while len(postfix) > 0:
            # for python2 and python3
            if addpath:
                path_id = struct.unpack('!I', postfix[0:4])[0]
                postfix = postfix[4:]
            if isinstance(postfix[0], int):
                prefix_len = postfix[0]
            else:
                prefix_len = ord(postfix[0])
            if prefix_len > 32:
                LOG.warning('Prefix Length larger than 32')
                raise excep.UpdateMessageError(
                    sub_error=bgp_cons.ERR_MSG_UPDATE_INVALID_NETWORK_FIELD,
                    data=repr(data)
                )
            octet_len, remainder = int(prefix_len / 8), prefix_len % 8
            if remainder > 0:
                # prefix length doesn't fall on octet boundary
                octet_len += 1
            tmp = postfix[1:octet_len + 1]
            # for python2 and python3
            if isinstance(postfix[0], int):
                prefix_data = [i for i in tmp]