# Copyright (C) 2013 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.

"""
RFC 2328 OSPF version 2
"""

from functools import reduce
import logging
import struct

import six

from os_ken.lib import addrconv
from os_ken.lib.packet import packet_base
from os_ken.lib.packet import packet_utils
from os_ken.lib.packet import stream_parser
from os_ken.lib.stringify import StringifyMixin
from os_ken.lib import type_desc


LOG = logging.getLogger(__name__)

_VERSION = 2

OSPF_MSG_UNKNOWN = 0
OSPF_MSG_HELLO = 1
OSPF_MSG_DB_DESC = 2
OSPF_MSG_LS_REQ = 3
OSPF_MSG_LS_UPD = 4
OSPF_MSG_LS_ACK = 5

OSPF_UNKNOWN_LSA = 0
OSPF_ROUTER_LSA = 1
OSPF_NETWORK_LSA = 2
OSPF_SUMMARY_LSA = 3
OSPF_ASBR_SUMMARY_LSA = 4
OSPF_AS_EXTERNAL_LSA = 5
OSPF_AS_NSSA_LSA = 7       # RFC 3101
OSPF_OPAQUE_LINK_LSA = 9   # RFC 5250
OSPF_OPAQUE_AREA_LSA = 10  # RFC 5250
OSPF_OPAQUE_AS_LSA = 11    # RFC 5250

OSPF_OPTION_T = 1        # Obsolete
OSPF_OPTION_E = 1 << 1   # RFC 2328
OSPF_OPTION_MC = 1 << 2  # RFC 1584
OSPF_OPTION_NP = 1 << 3  # RFC 3101
OSPF_OPTION_EA = 1 << 4  # Obsolete
OSPF_OPTION_DC = 1 << 5  # RFC 2370
OSPF_OPTION_DN = 1 << 7  # RFC 2567

LSA_LINK_TYPE_P2P = 1
LSA_LINK_TYPE_TRANSIT = 2
LSA_LINK_TYPE_STUB = 3
LSA_LINK_TYPE_VL = 4

ROUTER_LSA_BORDER = 0x01  # The router is an ABR
ROUTER_LSA_EXTERNAL = 0x02  # The router is an ASBR
ROUTER_LSA_VIRTUAL = 0x04  # The router has a VL in this area
ROUTER_LSA_NT = 0x10  # The router always translates Type-7
ROUTER_LSA_SHORTCUT = 0x20  # Shortcut-ABR specific flag

AS_EXTERNAL_METRIC = 0x80

OSPF_OPAQUE_TYPE_UNKNOWN = 0
OSPF_OPAQUE_TYPE_EXTENDED_PREFIX_LSA = 7
OSPF_OPAQUE_TYPE_EXTENDED_LINK_LSA = 8

OSPF_EXTENDED_PREFIX_TLV = 1
OSPF_EXTENDED_PREFIX_SID_SUBTLV = 2


class InvalidChecksum(Exception):
    pass


class LSAHeader(StringifyMixin):
    _HDR_PACK_STR = '!HBB4s4sIHH'
    _HDR_LEN = struct.calcsize(_HDR_PACK_STR)

    def __init__(self, ls_age=0, options=0, type_=OSPF_UNKNOWN_LSA,
                 id_='0.0.0.0', adv_router='0.0.0.0', ls_seqnum=0,
                 checksum=0, length=0, opaque_type=OSPF_OPAQUE_TYPE_UNKNOWN,
                 opaque_id=0):
        self.ls_age = ls_age
        self.options = options
        self.type_ = type_
        if self.type_ < OSPF_OPAQUE_LINK_LSA:
            self.id_ = id_
        else:
            self.opaque_type = opaque_type
            self.opaque_id = opaque_id
        self.adv_router = adv_router
        self.ls_seqnum = ls_seqnum
        self.checksum = checksum
        self.length = length

    @classmethod
    def parser(cls, buf):
        if len(buf) < cls._HDR_LEN:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), cls._HDR_LEN))
        (ls_age, options, type_, id_, adv_router, ls_seqnum, checksum,
         length,) = struct.unpack_from(cls._HDR_PACK_STR, six.binary_type(buf))
        adv_router = addrconv.ipv4.bin_to_text(adv_router)
        rest = buf[cls._HDR_LEN:]
        lsacls = LSA._lookup_type(type_)

        value = {
            "ls_age": ls_age,
            "options": options,
            "type_": type_,
            "adv_router": adv_router,
            "ls_seqnum": ls_seqnum,
            "checksum": checksum,
            "length": length,
        }

        if issubclass(lsacls, OpaqueLSA):
            (id_,) = struct.unpack_from('!I', id_)
            value['opaque_type'] = (id_ & 0xff000000) >> 24
            value['opaque_id'] = (id_ & 0xffffff)
        else:
            value['id_'] = addrconv.ipv4.bin_to_text(id_)

        return value, rest

    def serialize(self):
        if self.type_ < OSPF_OPAQUE_LINK_LSA:
            id_ = addrconv.ipv4.text_to_bin(self.id_)
        else:
            id_ = (self.opaque_type << 24) + self.opaque_id
            (id_,) = struct.unpack_from('4s', struct.pack('!I', id_))

        adv_router = addrconv.ipv4.text_to_bin(self.adv_router)
        return bytearray(
            struct.pack(self._HDR_PACK_STR, self.ls_age,
                        self.options, self.type_, id_, adv_router,
                        self.ls_seqnum, self.checksum, self.length))


class LSA(type_desc.TypeDisp, StringifyMixin):
    def __init__(self, ls_age=0, options=0, type_=OSPF_UNKNOWN_LSA,
                 id_='0.0.0.0', adv_router='0.0.0.0', ls_seqnum=0,
                 checksum=0, length=0, opaque_type=OSPF_OPAQUE_TYPE_UNKNOWN,
                 opaque_id=0):
        if type_ < OSPF_OPAQUE_LINK_LSA:
            self.header = LSAHeader(
                ls_age=ls_age,
                options=options,
                type_=type_,
                id_=id_,
                adv_router=adv_router,
                ls_seqnum=ls_seqnum)
        else:
            self.header = LSAHeader(
                ls_age=ls_age,
                options=options,
                type_=type_,
                adv_router=adv_router,
                ls_seqnum=ls_seqnum,
                opaque_type=opaque_type,
                opaque_id=opaque_id)

        if not (checksum or length):
            tail = self.serialize_tail()
            length = self.header._HDR_LEN + len(tail)
        if not checksum:
            head = self.header.serialize()
            checksum = packet_utils.fletcher_checksum(head[2:], 14)
        self.header.length = length
        self.header.checksum = checksum

    @classmethod
    def parser(cls, buf):
        hdr, rest = LSAHeader.parser(buf)
        if len(buf) < hdr['length']:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), hdr['length']))
        # exclude ls_age for checksum calculation
        csum = packet_utils.fletcher_checksum(buf[2:hdr['length']], 14)
        if csum != hdr['checksum']:
            raise InvalidChecksum("header has %d, but calculated value is %d"
                                  % (hdr['checksum'], csum))
        subcls = cls._lookup_type(hdr['type_'])
        body = rest[:hdr['length'] - LSAHeader._HDR_LEN]
        rest = rest[hdr['length'] - LSAHeader._HDR_LEN:]
        if issubclass(subcls, OpaqueLSA):
            kwargs = subcls.parser(body, hdr['opaque_type'])
        else:
            kwargs = subcls.parser(body)
        kwargs.update(hdr)
        return subcls(**kwargs), subcls, rest

    def serialize(self):
        tail = self.serialize_tail()
        self.header.length = self.header._HDR_LEN + len(tail)
        head = self.header.serialize()
        # exclude ls_age for checksum calculation
        csum = packet_utils.fletcher_checksum(head[2:] + tail, 14)
        self.header.checksum = csum
        struct.pack_into("!H", head, 16, csum)
        return head + tail

    def serialize_tail(self):
        # should be implemented in subclass
        return b''


@LSA.register_type(OSPF_ROUTER_LSA)
class RouterLSA(LSA):
    _PACK_STR = '!BBH'
    _PACK_LEN = struct.calcsize(_PACK_STR)  # 4bytes

    class Link(StringifyMixin):
        _PACK_STR = '!4s4sBBH'
        _PACK_LEN = struct.calcsize(_PACK_STR)  # 12bytes

        def __init__(self, id_='0.0.0.0', data='0.0.0.0',
                     type_=LSA_LINK_TYPE_STUB, tos=0, metric=10):
            self.id_ = id_
            self.data = data
            self.type_ = type_
            self.tos = tos
            self.metric = metric

        @classmethod
        def parser(cls, buf):
            if len(buf) < cls._PACK_LEN:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._PACK_LEN))
            link = buf[:cls._PACK_LEN]
            rest = buf[cls._PACK_LEN:]
            (id_, data, type_, tos, metric) = \
                struct.unpack_from(cls._PACK_STR, six.binary_type(link))
            id_ = addrconv.ipv4.bin_to_text(id_)
            data = addrconv.ipv4.bin_to_text(data)
            return cls(id_, data, type_, tos, metric), rest

        def serialize(self):
            id_ = addrconv.ipv4.text_to_bin(self.id_)
            data = addrconv.ipv4.text_to_bin(self.data)
            return bytearray(
                struct.pack(self._PACK_STR, id_, data, self.type_, self.tos,
                            self.metric))

    def __init__(self, ls_age=0, options=0, type_=OSPF_ROUTER_LSA,
                 id_='0.0.0.0', adv_router='0.0.0.0', ls_seqnum=0,
                 checksum=None, length=None, flags=0, links=None):
        links = links if links else []
        self.flags = flags
        self.links = links
        super(RouterLSA, self).__init__(ls_age, options, type_, id_,
                                        adv_router, ls_seqnum, checksum,
                                        length)

    @classmethod
    def parser(cls, buf):
        links = []
        hdr = buf[:cls._PACK_LEN]
        buf = buf[cls._PACK_LEN:]
        (flags, _, num) = struct.unpack_from(cls._PACK_STR,
                                             six.binary_type(hdr))
        while buf:
            link, buf = cls.Link.parser(buf)
            links.append(link)
        assert len(links) == num
        return {
            "flags": flags,
            "links": links,
        }

    def serialize_tail(self):
        head = bytearray(
            struct.pack(self._PACK_STR, self.flags, 0, len(self.links)))
        try:
            return head + reduce(lambda a, b: a + b,
                                 (link.serialize() for link in self.links))
        except TypeError:
            return head


@LSA.register_type(OSPF_NETWORK_LSA)
class NetworkLSA(LSA):
    _PACK_STR = '!4s'
    _PACK_LEN = struct.calcsize(_PACK_STR)

    def __init__(self, ls_age=0, options=0, type_=OSPF_NETWORK_LSA,
                 id_='0.0.0.0', adv_router='0.0.0.0', ls_seqnum=0,
                 checksum=None, length=None, mask='0.0.0.0', routers=None):
        routers = routers if routers else []
        self.mask = mask
        self.routers = routers
        super(NetworkLSA, self).__init__(ls_age, options, type_, id_,
                                         adv_router, ls_seqnum, checksum,
                                         length)

    @classmethod
    def parser(cls, buf):
        if len(buf) < cls._PACK_LEN:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), cls._PACK_LEN))
        binmask = buf[:cls._PACK_LEN]
        (mask,) = struct.unpack_from(cls._PACK_STR, six.binary_type(binmask))
        mask = addrconv.ipv4.bin_to_text(mask)
        buf = buf[cls._PACK_LEN:]
        routers = []
        while buf:
            binrouter = buf[:cls._PACK_LEN]
            (router,) = struct.unpack_from(cls._PACK_STR,
                                           six.binary_type(binrouter))
            router = addrconv.ipv4.bin_to_text(router)
            routers.append(router)
            buf = buf[cls._PACK_LEN:]
        return {
            "mask": mask,
            "routers": routers,
        }

    def serialize_tail(self):
        mask = addrconv.ipv4.text_to_bin(self.mask)
        routers = [addrconv.ipv4.text_to_bin(router)
                   for router in self.routers]
        return bytearray(
            struct.pack("!" + "4s" * (1 + len(routers)), mask, *routers))


@LSA.register_type(OSPF_SUMMARY_LSA)
class SummaryLSA(LSA):
    _PACK_STR = '!4sB3s'
    _PACK_LEN = struct.calcsize(_PACK_STR)

    def __init__(self, ls_age=0, options=0, type_=OSPF_SUMMARY_LSA,
                 id_='0.0.0.0', adv_router='0.0.0.0', ls_seqnum=0,
                 checksum=None, length=None, mask='0.0.0.0', tos=0, metric=0):
        self.mask = mask
        self.tos = tos
        self.metric = metric
        super(SummaryLSA, self).__init__(ls_age, options, type_, id_,
                                         adv_router, ls_seqnum, checksum,
                                         length)

    @classmethod
    def parser(cls, buf):
        if len(buf) < cls._PACK_LEN:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), cls._PACK_LEN))
        buf = buf[:cls._PACK_LEN]
        (mask, tos, metric) = struct.unpack_from(
            cls._PACK_STR, six.binary_type(buf))
        mask = addrconv.ipv4.bin_to_text(mask)
        metric = type_desc.Int3.to_user(metric)
        return {
            "mask": mask,
            "tos": tos,
            "metric": metric,
        }

    def serialize_tail(self):
        mask = addrconv.ipv4.text_to_bin(self.mask)
        metric = type_desc.Int3.from_user(self.metric)
        return bytearray(struct.pack(self._PACK_STR, mask, self.tos, metric))


@LSA.register_type(OSPF_ASBR_SUMMARY_LSA)
class ASBRSummaryLSA(LSA):
    pass


@LSA.register_type(OSPF_AS_EXTERNAL_LSA)
class ASExternalLSA(LSA):
    class ExternalNetwork(StringifyMixin):
        _PACK_STR = '!4sB3s4sI'
        _PACK_LEN = struct.calcsize(_PACK_STR)

        def __init__(self, mask='0.0.0.0', flags=0, metric=0,
                     fwd_addr='0.0.0.0', tag=0):
            self.mask = mask
            self.flags = flags
            self.metric = metric
            self.fwd_addr = fwd_addr
            self.tag = tag

        @classmethod
        def parser(cls, buf):
            if len(buf) < cls._PACK_LEN:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._PACK_LEN))
            ext_nw = buf[:cls._PACK_LEN]
            rest = buf[cls._PACK_LEN:]
            (mask, flags, metric, fwd_addr,
             tag) = struct.unpack_from(cls._PACK_STR, six.binary_type(ext_nw))
            mask = addrconv.ipv4.bin_to_text(mask)
            metric = type_desc.Int3.to_user(metric)
            fwd_addr = addrconv.ipv4.bin_to_text(fwd_addr)
            return cls(mask, flags, metric, fwd_addr, tag), rest

        def serialize(self):
            mask = addrconv.ipv4.text_to_bin(self.mask)
            metric = type_desc.Int3.from_user(self.metric)
            fwd_addr = addrconv.ipv4.text_to_bin(self.fwd_addr)
            return bytearray(
                struct.pack(self._PACK_STR, mask, self.flags, metric,
                            fwd_addr, self.tag))

    def __init__(self, ls_age=0, options=0, type_=OSPF_AS_EXTERNAL_LSA,
                 id_='0.0.0.0', adv_router='0.0.0.0', ls_seqnum=0,
                 checksum=None, length=None, extnws=None):
        extnws = extnws if extnws else []
        self.extnws = extnws
        super(ASExternalLSA, self).__init__(ls_age, options, type_, id_,
                                            adv_router, ls_seqnum, checksum,
                                            length)

    @classmethod
    def parser(cls, buf):
        extnws = []
        while buf:
            extnw, buf = cls.ExternalNetwork.parser(buf)
            extnws.append(extnw)
        return {
            "extnws": extnws,
        }

    def serialize_tail(self):
        return reduce(lambda a, b: a + b,
                      (extnw.serialize() for extnw in self.extnws))


@LSA.register_type(OSPF_AS_NSSA_LSA)
class NSSAExternalLSA(LSA):
    pass


class ExtendedPrefixTLV(StringifyMixin, type_desc.TypeDisp):
    pass


@ExtendedPrefixTLV.register_type(OSPF_EXTENDED_PREFIX_TLV)
class ExtendedPrefixTLV(ExtendedPrefixTLV):
    _VALUE_PACK_STR = '!HHBBBB4s'
    _VALUE_PACK_LEN = struct.calcsize(_VALUE_PACK_STR)
    _VALUE_FIELDS = ['route_type', 'prefix_length', 'address_family', '_pad'
                     'prefix']

    def __init__(self, type_=OSPF_EXTENDED_PREFIX_TLV, length=0, route_type=0,
                 address_family=0, prefix='0.0.0.0/0'):
        self.type_ = type_
        self.length = length
        self.route_type = route_type
        self.address_family = address_family
        self.prefix = prefix

    @classmethod
    def parser(cls, buf):
        rest = buf[cls._VALUE_PACK_LEN:]
        buf = buf[:cls._VALUE_PACK_LEN]
        (type_, length, route_type, prefix_length, address_family, _pad,
         prefix) = struct.unpack_from(cls._VALUE_PACK_STR, buf)

        prefix = addrconv.ipv4.bin_to_text(prefix)
        prefix = "%s/%d" % (prefix, prefix_length)
        return cls(type_, length, route_type, address_family, prefix), rest

    def serialize(self):
        prefix, prefix_length = self.prefix.split('/')
        prefix = addrconv.ipv4.text_to_bin(prefix)
        prefix_length = int(prefix_length)
        return struct.pack(self._VALUE_PACK_STR, OSPF_EXTENDED_PREFIX_TLV,
                           self._VALUE_PACK_LEN - 4, self.route_type,
                           prefix_length, self.address_family, 0, prefix)


@ExtendedPrefixTLV.register_type(OSPF_EXTENDED_PREFIX_SID_SUBTLV)
class PrefixSIDSubTLV(ExtendedPrefixTLV):
    _VALUE_PACK_STR = '!HHBBBBHHI'
    _VALUE_PACK_LEN = struct.calcsize(_VALUE_PACK_STR)
    _VALUE_FIELDS = ['flags', 'mt_id', 'algorithm', '_pad', 'range_size',
                     '_pad', 'index']

    def __init__(self, type_=OSPF_EXTENDED_PREFIX_SID_SUBTLV, length=0,
                 flags=0, mt_id=0, algorithm=0, range_size=0, index=0):
        super(PrefixSIDSubTLV, self).__init__()
        self.type_ = type_
        self.length = length
        self.flags = flags
        self.mt_id = mt_id
        self.algorithm = algorithm
        self.range_size = range_size
        self.index = index

    @classmethod
    def parser(cls, buf):
        rest = buf[cls._VALUE_PACK_LEN:]
        buf = buf[:cls._VALUE_PACK_LEN]
        (type_, length, flags, mt_id, algorithm, _pad, range_size, _pad,
         index) = struct.unpack_from(cls._VALUE_PACK_STR, buf)

        return cls(type_, length, flags, mt_id, algorithm, range_size,
                   index), rest

    def serialize(self):
        return struct.pack(self._VALUE_PACK_STR,
                           OSPF_EXTENDED_PREFIX_SID_SUBTLV,
                           self._VALUE_PACK_LEN - 4, self.flags, self.mt_id,
                           self.algorithm, 0, self.range_size, 0, self.index)


class ExtendedLinkTLV(StringifyMixin, type_desc.TypeDisp):
    pass


class OpaqueBody(StringifyMixin, type_desc.TypeDisp):
    def __init__(self, tlvs=None):
        tlvs = tlvs if tlvs else []
        self.tlvs = tlvs

    def serialize(self):
        return reduce(lambda a, b: a + b,
                      (tlv.serialize() for tlv in self.tlvs))


@OpaqueBody.register_type(OSPF_OPAQUE_TYPE_EXTENDED_PREFIX_LSA)
class ExtendedPrefixOpaqueBody(OpaqueBody):
    @classmethod
    def parser(cls, buf):
        buf = six.binary_type(buf)
        tlvs = []
        while buf:
            (type_, length) = struct.unpack_from('!HH', buf)
            if len(buf[struct.calcsize('!HH'):]) < length:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), length))
            tlvcls = ExtendedPrefixTLV._lookup_type(type_)
            if tlvcls:
                tlv, buf = tlvcls.parser(buf)
                tlvs.append(tlv)

        return cls(tlvs)


@OpaqueBody.register_type(OSPF_OPAQUE_TYPE_EXTENDED_LINK_LSA)
class ExtendedLinkOpaqueBody(OpaqueBody):
    @classmethod
    def parser(cls, buf):
        buf = six.binary_type(buf)
        tlvs = []
        while buf:
            (type_, length) = struct.unpack_from('!HH', buf)
            if len(buf[struct.calcsize('!HH'):]) < length:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), length))
            tlvcls = ExtendedLinkTLV._lookup_type(type_)
            if tlvcls:
                tlv, buf = tlvcls.parser(buf)
                tlvs.append(tlv)

        return cls(tlvs)


class OpaqueLSA(LSA):

    def __init__(self, data, *args, **kwargs):
        super(OpaqueLSA, self).__init__(*args, **kwargs)
        self.data = data

    @classmethod
    def parser(cls, buf, opaque_type=OSPF_OPAQUE_TYPE_UNKNOWN):
        opaquecls = OpaqueBody._lookup_type(opaque_type)
        if opaquecls:
            data = opaquecls.parser(buf)
        else:
            data = buf
        return {'data': data}

    def serialize_tail(self):
        if isinstance(self.data, OpaqueBody):
            return self.data.serialize()
        else:
            return self.data


@LSA.register_type(OSPF_OPAQUE_LINK_LSA)
class LocalOpaqueLSA(OpaqueLSA):
    def __init__(self, ls_age=0, options=0, type_=OSPF_OPAQUE_LINK_LSA,
                 adv_router='0.0.0.0', ls_seqnum=0, checksum=0, length=0,
                 opaque_type=OSPF_OPAQUE_TYPE_UNKNOWN, opaque_id=0, data=None):
        self.data = data
        super(LocalOpaqueLSA, self).__init__(ls_age, options, type_, 0,
                                             adv_router, ls_seqnum, checksum,
                                             length, opaque_type, opaque_id)


@LSA.register_type(OSPF_OPAQUE_AREA_LSA)
class AreaOpaqueLSA(OpaqueLSA):
    def __init__(self, ls_age=0, options=0, type_=OSPF_OPAQUE_AREA_LSA,
                 adv_router='0.0.0.0', ls_seqnum=0, checksum=0, length=0,
                 opaque_type=OSPF_OPAQUE_TYPE_UNKNOWN, opaque_id=0, data=None):
        self.data = data
        super(AreaOpaqueLSA, self).__init__(ls_age, options, type_, 0,
                                            adv_router, ls_seqnum, checksum,
                                            length, opaque_type, opaque_id)


@LSA.register_type(OSPF_OPAQUE_AS_LSA)
class ASOpaqueLSA(OpaqueLSA):
    def __init__(self, ls_age=0, options=0, type_=OSPF_OPAQUE_AS_LSA,
                 adv_router='0.0.0.0', ls_seqnum=0, checksum=0, length=0,
                 opaque_type=OSPF_OPAQUE_TYPE_UNKNOWN, opaque_id=0, data=None):
        self.data = data
        super(ASOpaqueLSA, self).__init__(ls_age, options, type_, 0,
                                          adv_router, ls_seqnum, checksum,
                                          length, opaque_type, opaque_id)


class OSPFMessage(packet_base.PacketBase, type_desc.TypeDisp):
    """Base class for OSPF version 2 messages.
    """

    _HDR_PACK_STR = '!BBH4s4sHHQ'
    _HDR_LEN = struct.calcsize(_HDR_PACK_STR)

    def __init__(self, type_, length=None, router_id='0.0.0.0',
                 area_id='0.0.0.0', au_type=1, authentication=0, checksum=None,
                 version=_VERSION):
        super(OSPFMessage, self).__init__()
        self.version = version
        self.type_ = type_
        self.length = length
        self.router_id = router_id
        self.area_id = area_id
        self.checksum = checksum
        self.au_type = au_type
        self.authentication = authentication

    @classmethod
    def _parser(cls, buf):
        if len(buf) < cls._HDR_LEN:
            raise stream_parser.StreamParser.TooSmallException(
                '%d < %d' % (len(buf), cls._HDR_LEN))
        (version, type_, length, router_id, area_id, checksum, au_type,
         authentication) = struct.unpack_from(cls._HDR_PACK_STR,
                                              six.binary_type(buf))

        # Exclude checksum and authentication field for checksum validation.
        if packet_utils.checksum(buf[:12] + buf[14:16] + buf[cls._HDR_LEN:]) \
                != checksum:
            raise InvalidChecksum

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

        router_id = addrconv.ipv4.bin_to_text(router_id)
        area_id = addrconv.ipv4.bin_to_text(area_id)
        binmsg = buf[cls._HDR_LEN:length]
        rest = buf[length:]
        subcls = cls._lookup_type(type_)
        kwargs = subcls.parser(binmsg)
        return subcls(length, router_id, area_id, au_type, int(authentication),
                      checksum, version, **kwargs), None, rest

    @classmethod
    def parser(cls, buf):
        try:
            return cls._parser(buf)
        except:
            return None, None, buf

    def serialize(self, payload=None, prev=None):
        tail = self.serialize_tail()
        self.length = self._HDR_LEN + len(tail)
        head = bytearray(
            struct.pack(self._HDR_PACK_STR, self.version,
                        self.type_, self.length,
                        addrconv.ipv4.text_to_bin(self.router_id),
                        addrconv.ipv4.text_to_bin(self.area_id), 0,
                        self.au_type, self.authentication))
        buf = head + tail
        csum = packet_utils.checksum(buf[:12] + buf[14:16] +
                                     buf[self._HDR_LEN:])
        self.checksum = csum
        struct.pack_into("!H", buf, 12, csum)
        return buf


# alias
ospf = OSPFMessage


@OSPFMessage.register_type(OSPF_MSG_HELLO)
class OSPFHello(OSPFMessage):

    _PACK_STR = '!4sHBBI4s4s'  # + neighbors
    _PACK_LEN = struct.calcsize(_PACK_STR)
    _MIN_LEN = OSPFMessage._HDR_LEN + _PACK_LEN

    def __init__(self, length=None, router_id='0.0.0.0', area_id='0.0.0.0',
                 au_type=1, authentication=0, checksum=None, version=_VERSION,
                 mask='0.0.0.0', hello_interval=10, options=0, priority=1,
                 dead_interval=40, designated_router='0.0.0.0',
                 backup_router='0.0.0.0', neighbors=None):
        neighbors = neighbors if neighbors else []
        super(OSPFHello, self).__init__(OSPF_MSG_HELLO, length, router_id,
                                        area_id, au_type, authentication,
                                        checksum, version)
        self.mask = mask
        self.hello_interval = hello_interval
        self.options = options
        self.priority = priority
        self.dead_interval = dead_interval
        self.designated_router = designated_router
        self.backup_router = backup_router
        self.neighbors = neighbors

    @classmethod
    def parser(cls, buf):
        (mask, hello_interval, options, priority, dead_interval,
         designated_router, backup_router) = struct.unpack_from(cls._PACK_STR,
                                                                six.binary_type(buf))
        mask = addrconv.ipv4.bin_to_text(mask)
        designated_router = addrconv.ipv4.bin_to_text(designated_router)
        backup_router = addrconv.ipv4.bin_to_text(backup_router)
        neighbors = []
        binneighbors = buf[cls._PACK_LEN:len(buf)]
        while binneighbors:
            n = binneighbors[:4]
            n = addrconv.ipv4.bin_to_text(six.binary_type(n))
            binneighbors = binneighbors[4:]
            neighbors.append(n)
        return {
            "mask": mask,
            "hello_interval": hello_interval,
            "options": options,
            "priority": priority,
            "dead_interval": dead_interval,
            "designated_router": designated_router,
            "backup_router": backup_router,
            "neighbors": neighbors,
        }

    def serialize_tail(self):
        head = bytearray(
            struct.pack(self._PACK_STR,
                        addrconv.ipv4.text_to_bin(self.mask),
                        self.hello_interval, self.options, self.priority,
                        self.dead_interval,
                        addrconv.ipv4.text_to_bin(self.designated_router),
                        addrconv.ipv4.text_to_bin(self.backup_router)))
        try:
            return head + reduce(lambda a, b: a + b,
                                 (addrconv.ipv4.text_to_bin(n)
                                  for n in self.neighbors))
        except TypeError:
            return head


@OSPFMessage.register_type(OSPF_MSG_DB_DESC)
class OSPFDBDesc(OSPFMessage):

    _PACK_STR = '!HBBI'  # + LSA_HEADERS
    _PACK_LEN = struct.calcsize(_PACK_STR)
    _MIN_LEN = OSPFMessage._HDR_LEN + _PACK_LEN

    def __init__(self, length=None, router_id='0.0.0.0', area_id='0.0.0.0',
                 au_type=1, authentication=0, checksum=None, version=_VERSION,
                 mtu=1500, options=0, i_flag=0, m_flag=0, ms_flag=0,
                 sequence_number=0, lsa_headers=None):
        lsa_headers = lsa_headers if lsa_headers else []
        super(OSPFDBDesc, self).__init__(OSPF_MSG_DB_DESC, length, router_id,
                                         area_id, au_type, authentication,
                                         checksum, version)
        self.mtu = mtu
        self.options = options
        self.i_flag = i_flag
        self.m_flag = m_flag
        self.ms_flag = ms_flag
        self.sequence_number = sequence_number
        self.lsa_headers = lsa_headers

    @classmethod
    def parser(cls, buf):
        (mtu, options, flags,
         sequence_number) = struct.unpack_from(cls._PACK_STR, six.binary_type(buf))
        i_flag = (flags >> 2) & 0x1
        m_flag = (flags >> 1) & 0x1
        ms_flag = flags & 0x1
        lsahdrs = []
        buf = buf[cls._PACK_LEN:]
        while buf:
            kwargs, buf = LSAHeader.parser(buf)
            lsahdrs.append(LSAHeader(**kwargs))
        return {
            "mtu": mtu,
            "options": options,
            "i_flag": i_flag,
            "m_flag": m_flag,
            "ms_flag": ms_flag,
            "sequence_number": sequence_number,
            "lsa_headers": lsahdrs,
        }

    def serialize_tail(self):
        flags = ((self.i_flag & 0x1) << 2) ^ \
                ((self.m_flag & 0x1) << 1) ^ \
                (self.ms_flag & 0x1)
        head = bytearray(
            struct.pack(self._PACK_STR, self.mtu, self.options, flags,
                        self.sequence_number))
        try:
            return head + reduce(lambda a, b: a + b,
                                 (hdr.serialize() for hdr in self.lsa_headers))
        except TypeError:
            return head


@OSPFMessage.register_type(OSPF_MSG_LS_REQ)
class OSPFLSReq(OSPFMessage):
    _MIN_LEN = OSPFMessage._HDR_LEN

    class Request(StringifyMixin):
        _PACK_STR = '!I4s4s'
        _PACK_LEN = struct.calcsize(_PACK_STR)

        def __init__(self, type_=OSPF_UNKNOWN_LSA, id_='0.0.0.0',
                     adv_router='0.0.0.0'):
            self.type_ = type_
            self.id = id_
            self.adv_router = adv_router

        @classmethod
        def parser(cls, buf):
            if len(buf) < cls._PACK_LEN:
                raise stream_parser.StreamParser.TooSmallException(
                    '%d < %d' % (len(buf), cls._PACK_LEN))
            link = buf[:cls._PACK_LEN]
            rest = buf[cls._PACK_LEN:]
            (type_, id_, adv_router) = struct.unpack_from(cls._PACK_STR,
                                                          six.binary_type(link))
            id_ = addrconv.ipv4.bin_to_text(id_)
            adv_router = addrconv.ipv4.bin_to_text(adv_router)
            return cls(type_, id_, adv_router), rest

        def serialize(self):
            id_ = addrconv.ipv4.text_to_bin(self.id)
            adv_router = addrconv.ipv4.text_to_bin(self.adv_router)
            return struct.pack(self._PACK_STR, self.type_, id_, adv_router)

    def __init__(self, length=None, router_id='0.0.0.0', area_id='0.0.0.0',
                 au_type=1, authentication=0, checksum=None, version=_VERSION,
                 lsa_requests=None):
        lsa_requests = lsa_requests if lsa_requests else []
        super(OSPFLSReq, self).__init__(OSPF_MSG_LS_REQ, length, router_id,
                                        area_id, au_type, authentication,
                                        checksum, version)
        self.lsa_requests = lsa_requests

    @classmethod
    def parser(cls, buf):
        reqs = []
        while buf:
            req, buf = cls.Request.parser(buf)
            reqs.append(req)
        return {
            "lsa_requests": reqs,
        }

    def serialize_tail(self):
        return reduce(lambda a, b: a + b,
                      (req.serialize() for req in self.lsa_requests))


@OSPFMessage.register_type(OSPF_MSG_LS_UPD)
class OSPFLSUpd(OSPFMessage):
    _PACK_STR = '!I'
    _PACK_LEN = struct.calcsize(_PACK_STR)
    _MIN_LEN = OSPFMessage._HDR_LEN + _PACK_LEN

    def __init__(self, length=None, router_id='0.0.0.0', area_id='0.0.0.0',
                 au_type=1, authentication=0, checksum=None, version=_VERSION,
                 lsas=None):
        lsas = lsas if lsas else []
        super(OSPFLSUpd, self).__init__(OSPF_MSG_LS_UPD, length, router_id,
                                        area_id, au_type, authentication,
                                        checksum, version)
        self.lsas = lsas

    @classmethod
    def parser(cls, buf):
        binnum = buf[:cls._PACK_LEN]
        (num,) = struct.unpack_from(cls._PACK_STR, six.binary_type(binnum))

        buf = buf[cls._PACK_LEN:]
        lsas = []
        while buf:
            lsa, _cls, buf = LSA.parser(buf)
            lsas.append(lsa)
        assert len(lsas) == num
        return {
            "lsas": lsas,
        }

    def serialize_tail(self):
        head = bytearray(struct.pack(self._PACK_STR, len(self.lsas)))
        try:
            return head + reduce(lambda a, b: a + b,
                                 (lsa.serialize() for lsa in self.lsas))
        except TypeError:
            return head


@OSPFMessage.register_type(OSPF_MSG_LS_ACK)
class OSPFLSAck(OSPFMessage):
    _MIN_LEN = OSPFMessage._HDR_LEN

    def __init__(self, length=None, router_id='0.0.0.0', area_id='0.0.0.0',
                 au_type=1, authentication=0, checksum=None, version=_VERSION,
                 lsa_headers=None):
        lsa_headers = lsa_headers if lsa_headers else []
        super(OSPFLSAck, self).__init__(OSPF_MSG_LS_ACK, length, router_id,
                                        area_id, au_type, authentication,
                                        checksum, version)
        self.lsa_headers = lsa_headers

    @classmethod
    def parser(cls, buf):
        lsahdrs = []
        while buf:
            kwargs, buf = LSAHeader.parser(buf)
            lsahdrs.append(LSAHeader(**kwargs))
        return {
            "lsa_headers": lsahdrs,
        }

    def serialize_tail(self):
        return reduce(lambda a, b: a + b,
                      (hdr.serialize() for hdr in self.lsa_headers))
