#!/usr/bin/env python3
#
#  Copyright (c) 2016, The OpenThread Authors.
#  All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#  1. Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#  2. Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#  3. Neither the name of the copyright holder nor the
#     names of its contributors may be used to endorse or promote products
#     derived from this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
#  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
#  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
#  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
#  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
#  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
#  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
#  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
#  POSSIBILITY OF SUCH DAMAGE.
#

import io
import ipaddress
import struct

import coap
import common
import dtls
import ipv6
import mac802154
import mle

from net_crypto import (
    CryptoEngine,
    MacCryptoMaterialCreator,
    MleCryptoMaterialCreator,
)

from enum import IntEnum


class DropPacketException(Exception):
    pass


class MessageType(IntEnum):
    MLE = 0
    COAP = 1
    ICMP = 2
    ACK = 3
    BEACON = 4
    DATA = 5
    COMMAND = 6
    DTLS = 7


class Message(object):

    def __init__(self):
        self._type = None
        self._channel = None
        self._mac_header = None
        self._ipv6_packet = None
        self._coap = None
        self._mle = None
        self._icmp = None
        self._dtls = None

    def _extract_udp_datagram(self, udp_datagram):
        if isinstance(udp_datagram.payload, mle.MleMessage):
            self._type = MessageType.MLE
            self._mle = udp_datagram.payload

        elif isinstance(udp_datagram.payload, (coap.CoapMessage, coap.CoapMessageProxy)):
            self._type = MessageType.COAP
            self._coap = udp_datagram.payload

        # DTLS message factory returns a list of messages
        elif isinstance(udp_datagram.payload, list):
            self._type = MessageType.DTLS
            self._dtls = udp_datagram.payload

    def _extract_upper_layer_protocol(self, upper_layer_protocol):
        if isinstance(upper_layer_protocol, ipv6.ICMPv6):
            self._type = MessageType.ICMP
            self._icmp = upper_layer_protocol

        elif isinstance(upper_layer_protocol, ipv6.UDPDatagram):
            self._extract_udp_datagram(upper_layer_protocol)

    def try_extract_dtls_messages(self):
        """Extract multiple dtls messages that are sent in a single UDP datagram
        """
        if self.type != MessageType.DTLS:
            return [self.clone()]

        assert isinstance(self.dtls, list)
        ret = []
        for dtls in self.dtls:
            msg = self.clone()
            msg._dtls = dtls
            ret.append(msg)
        return ret

    def clone(self):
        msg = Message()
        msg._type = self.type
        msg._channel = self.channel
        msg._mac_header = self.mac_header
        msg._ipv6_packet = self.ipv6_packet
        msg._coap = self.coap
        msg._mle = self.mle
        msg._icmp = self.icmp
        msg._dtls = self.dtls
        return msg

    @property
    def type(self):
        return self._type

    @type.setter
    def type(self, value):
        self._type = value

    @property
    def channel(self):
        return self._channel

    @channel.setter
    def channel(self, value):
        self._channel = value

    @property
    def mac_header(self):
        return self._mac_header

    @mac_header.setter
    def mac_header(self, value):
        self._mac_header = value

        if self._mac_header.frame_type == mac802154.MacHeader.FrameType.BEACON:
            self._type = MessageType.BEACON

        elif self._mac_header.frame_type == mac802154.MacHeader.FrameType.ACK:
            self._type = MessageType.ACK

        elif self._mac_header.frame_type == mac802154.MacHeader.FrameType.DATA:
            self._type = MessageType.DATA
        elif (self._mac_header.frame_type == mac802154.MacHeader.FrameType.COMMAND):
            self._type = MessageType.COMMAND
        else:
            raise ValueError('Invalid mac frame type %d' % self._mac_header.frame_type)

    @property
    def ipv6_packet(self):
        return self._ipv6_packet

    @ipv6_packet.setter
    def ipv6_packet(self, value):
        self._ipv6_packet = value
        self._extract_upper_layer_protocol(value.upper_layer_protocol)

    @property
    def coap(self):
        return self._coap

    @property
    def mle(self):
        return self._mle

    @property
    def icmp(self):
        return self._icmp

    @icmp.setter
    def icmp(self, value):
        self._icmp = value

    @property
    def dtls(self):
        return self._dtls

    def get_mle_message_tlv(self, tlv_class_type):
        if self.type != MessageType.MLE:
            raise ValueError("Invalid message type. Expected MLE message.")

        for tlv in self.mle.command.tlvs:
            if isinstance(tlv, tlv_class_type):
                return tlv

    def assertMleMessageIsType(self, command_type):
        if self.type != MessageType.MLE:
            raise ValueError("Invalid message type. Expected MLE message.")

        assert self.mle.command.type == command_type

    def assertMleMessageContainsTlv(self, tlv_class_type):
        """To confirm if Mle message contains the TLV type.

        Args:
            tlv_class_type: tlv's type.

        Returns:
            mle.Route64: If contains the TLV, return it.
        """
        if self.type != MessageType.MLE:
            raise ValueError("Invalid message type. Expected MLE message.")

        contains_tlv = False
        for tlv in self.mle.command.tlvs:
            if isinstance(tlv, tlv_class_type):
                contains_tlv = True
                break

        assert contains_tlv
        return tlv

    def assertAssignedRouterQuantity(self, router_quantity):
        """Confirm if Leader contains the Route64 TLV with router_quantity assigned Router IDs.

        Args:
            router_quantity: the quantity of router.
        """
        tlv = self.assertMleMessageContainsTlv(mle.Route64)
        router_id_mask = tlv.router_id_mask

        count = 0
        for i in range(1, 65):
            count += router_id_mask & 1
            router_id_mask = router_id_mask >> 1
        assert count == router_quantity

    def assertMleMessageDoesNotContainTlv(self, tlv_class_type):
        if self.type != MessageType.MLE:
            raise ValueError("Invalid message type. Expected MLE message.")

        contains_tlv = False
        for tlv in self.mle.command.tlvs:
            if isinstance(tlv, tlv_class_type):
                contains_tlv = True
                break

        assert contains_tlv is False

    def assertMleMessageContainsOptionalTlv(self, tlv_class_type):
        if self.type != MessageType.MLE:
            raise ValueError("Invalid message type. Expected MLE message.")

        contains_tlv = False
        for tlv in self.mle.command.tlvs:
            if isinstance(tlv, tlv_class_type):
                contains_tlv = True
                break

        if contains_tlv:
            print("MleMessage contains optional TLV: {}".format(tlv_class_type))
        else:
            print("MleMessage doesn't contain optional TLV: {}".format(tlv_class_type))

    def get_coap_message_tlv(self, tlv_class_type):
        if self.type != MessageType.COAP:
            raise ValueError("Invalid message type. Expected CoAP message.")

        for tlv in self.coap.payload:
            if isinstance(tlv, tlv_class_type):
                return tlv

    def assertCoapMessageContainsTlv(self, tlv_class_type):
        if self.type != MessageType.COAP:
            raise ValueError("Invalid message type. Expected CoAP message.")

        contains_tlv = False
        for tlv in self.coap.payload:
            if isinstance(tlv, tlv_class_type):
                contains_tlv = True
                break

        assert contains_tlv

    def assertCoapMessageDoesNotContainTlv(self, tlv_class_type):
        if self.type != MessageType.COAP:
            raise ValueError("Invalid message type. Expected COAP message.")

        contains_tlv = False
        for tlv in self.coap.payload:
            if isinstance(tlv, tlv_class_type):
                contains_tlv = True
                break

        assert contains_tlv is False

    def assertCoapMessageContainsOptionalTlv(self, tlv_class_type):
        if self.type != MessageType.COAP:
            raise ValueError("Invalid message type. Expected CoAP message.")

        for tlv in self.coap.payload:
            if isinstance(tlv, tlv_class_type):
                break

        print("CoapMessage doesn't contain optional TLV: {}".format(tlv_class_type))

    def assertCoapMessageRequestUriPath(self, uri_path):
        if self.type != MessageType.COAP:
            raise ValueError("Invalid message type. Expected CoAP message.")

        assert uri_path == self.coap.uri_path

    def assertCoapMessageCode(self, code):
        if self.type != MessageType.COAP:
            raise ValueError("Invalid message type. Expected CoAP message.")

        assert code == self.coap.code

    def assertSentToNode(self, node):
        sent_to_node = False
        dst_addr = self.ipv6_packet.ipv6_header.destination_address

        for addr in node.get_addrs():
            if dst_addr == ipaddress.ip_address(addr):
                sent_to_node = True

        if self.mac_header.dest_address.type == common.MacAddressType.SHORT:
            mac_address = common.MacAddress.from_rloc16(node.get_addr16())
            if self.mac_header.dest_address == mac_address:
                sent_to_node = True

        elif self.mac_header.dest_address.type == common.MacAddressType.LONG:
            mac_address = common.MacAddress.from_eui64(bytearray(node.get_addr64(), encoding="utf-8"))
            if self.mac_header.dest_address == mac_address:
                sent_to_node = True

        assert sent_to_node

    def assertSentToDestinationAddress(self, ipv6_address):
        assert (self.ipv6_packet.ipv6_header.destination_address == ipaddress.ip_address(ipv6_address))

    def assertSentFromSourceAddress(self, ipv6_address):
        assert (self.ipv6_packet.ipv6_header.source_address == ipaddress.ip_address(ipv6_address))

    def assertSentWithHopLimit(self, hop_limit):
        assert self.ipv6_packet.ipv6_header.hop_limit == hop_limit

    def isMacAddressTypeLong(self):
        return self.mac_header.dest_address.type == common.MacAddressType.LONG

    def get_dst_udp_port(self):
        assert isinstance(self.ipv6_packet.upper_layer_protocol, ipv6.UDPDatagram)
        return self.ipv6_packet.upper_layer_protocol.header.dst_port

    def is_data_poll(self):
        return self._type == MessageType.COMMAND and \
            self._mac_header.command_type == mac802154.MacHeader.CommandIdentifier.DATA_REQUEST

    def __repr__(self):
        if (self.type == MessageType.DTLS and self.dtls.content_type == dtls.ContentType.HANDSHAKE):
            return "Message(type={})".format(str(self.dtls.handshake_type))
        return "Message(type={})".format(MessageType(self.type).name)


class MessagesSet(object):

    def __init__(self, messages, commissioning_messages=()):
        self._messages = messages
        self._commissioning_messages = commissioning_messages

    @property
    def messages(self):
        return self._messages

    @property
    def commissioning_messages(self):
        return self._commissioning_messages

    def next_data_poll(self):
        while True:
            message = self.next_message_of(MessageType.COMMAND, False)
            if not message:
                break
            elif message.is_data_poll():
                return message

    def next_coap_message(self, code, uri_path=None, assert_enabled=True):
        message = None

        while self.messages:
            m = self.messages.pop(0)

            if m.type != MessageType.COAP:
                continue

            if uri_path is not None and m.coap.uri_path != uri_path:
                continue

            else:
                if not m.coap.code.is_equal_dotted(code):
                    continue

            message = m
            break

        if assert_enabled:
            assert (message is not None), "Could not find CoapMessage with code: {}".format(code)

        return message

    def last_mle_message(self, command_type, assert_enabled=True):
        """Get the last Mle Message with specified type from existing capture.

        Args:
            command_type: the specified mle type.
            assert_enabled: interrupt or not when get the mle.

        Returns:
            message.Message: the last Mle Message with specified type.
        """
        message = None
        size = len(self.messages)

        for i in range(size - 1, -1, -1):
            m = self.messages[i]

            if m.type != MessageType.MLE:
                continue

            # for command_type in command_types:
            if m.mle.command.type == command_type:
                message = m
                break

        if assert_enabled:
            assert (message is not None), "Could not find MleMessage with type: {}".format(command_type)

        return message

    def next_mle_message(self, command_type, assert_enabled=True, sent_to_node=None):
        message = self.next_mle_message_of_one_of_command_types(command_type)

        if assert_enabled:
            assert (message is not None), "Could not find MleMessage of the type: {}".format(command_type)

        if sent_to_node is not None:
            message.assertSentToNode(sent_to_node)

        return message

    def next_mle_message_of_one_of_command_types(self, *command_types):
        message = None

        while self.messages:
            m = self.messages.pop(0)

            if m.type != MessageType.MLE:
                continue

            command_found = False

            for command_type in command_types:
                if m.mle.command.type == command_type:
                    command_found = True
                    break

            if command_found:
                message = m
                break

        return message

    def next_message(self, assert_enabled=True):
        message = self.messages.pop(0)
        if assert_enabled:
            assert message is not None, "Could not find next Message"
        return message

    def next_message_of(self, message_type, assert_enabled=True):
        message = None

        while self.messages:
            m = self.messages.pop(0)
            if m.type != message_type:
                continue

            message = m
            break

        if assert_enabled:
            assert (message is not None), "Could not find Message of the type: {}".format(message_type)

        return message

    def next_data_message(self):
        return self.next_message_of(MessageType.DATA)

    def next_command_message(self):
        return self.next_message_of(MessageType.COMMAND)

    def next_dtls_message(self, content_type, handshake_type=None):
        while self.messages:
            msg = self.messages.pop(0)
            if msg.type != MessageType.DTLS:
                continue
            if msg.dtls.content_type != content_type:
                continue
            if (content_type == dtls.ContentType.HANDSHAKE and msg.dtls.handshake_type != handshake_type):
                continue
            return msg

        t = (handshake_type if content_type == dtls.ContentType.HANDSHAKE else content_type)
        raise ValueError("Could not find DTLS message of type: {}".format(str(t)))

    def contains_icmp_message(self):
        for m in self.messages:
            if m.type == MessageType.ICMP:
                return True

        return False

    def get_icmp_message(self, icmp_type):
        for m in self.messages:
            if m.type != MessageType.ICMP:
                continue

            if m.icmp.header.type == icmp_type:
                return m

        return None

    def contains_mle_message(self, command_type):
        for m in self.messages:
            if m.type != MessageType.MLE:
                continue

            if m.mle.command.type == command_type:
                return True

        return False

    def does_not_contain_coap_message(self):
        for m in self.messages:
            if m.type != MessageType.COAP:
                continue

            return False

        return True

    def clone(self):
        """Make a copy of current MessageSet.
        """
        return MessagesSet(self.messages[:], self.commissioning_messages[:])

    def __repr__(self):
        return str(self.messages)


class KeyManager:

    def __init__(self):
        self.mac_crypto_engines = set()
        self.mle_crypto_engines = set()

    def add_key(self, key):
        self.mac_crypto_engines.add(CryptoEngine(MacCryptoMaterialCreator(key)))
        self.mle_crypto_engines.add(CryptoEngine(MleCryptoMaterialCreator(key)))


class MessageFactory:

    def __init__(self, lowpan_parser, key_manager):
        self._lowpan_parser = lowpan_parser
        self.key_manager = key_manager

    def _add_device_descriptors(self, message):
        for tlv in message.mle.command.tlvs:

            if isinstance(tlv, mle.SourceAddress):
                mac802154.DeviceDescriptors.add(tlv.address, message.mac_header.src_address)

            if isinstance(tlv, mle.Address16):
                mac802154.DeviceDescriptors.add(tlv.address, message.mac_header.dest_address)

    def _parse_mac_frame(self, data):
        mac_frame = mac802154.MacFrame()
        mac_frame.parse(data, self.key_manager.mac_crypto_engines)
        return mac_frame

    def set_lowpan_context(self, cid, prefix):
        self._lowpan_parser.set_lowpan_context(cid, prefix)

    def create(self, data):
        try:
            message = Message()
            message.channel = struct.unpack(">B", data.read(1))

            # Parse MAC header
            mac_frame = self._parse_mac_frame(data)
            message.mac_header = mac_frame.header

            if message.mac_header.frame_type != mac802154.MacHeader.FrameType.DATA:
                return [message]

            message_info = common.MessageInfo()
            message_info.source_mac_address = message.mac_header.src_address
            message_info.destination_mac_address = message.mac_header.dest_address

            # Create stream with 6LoWPAN datagram
            lowpan_payload = io.BytesIO(mac_frame.payload.data)

            ipv6_packet = self._lowpan_parser.parse(lowpan_payload, message_info)
            if ipv6_packet is None:
                return [message]

            message.ipv6_packet = ipv6_packet

            if message.type == MessageType.MLE:
                self._add_device_descriptors(message)

            return message.try_extract_dtls_messages()

        except mac802154.KeyIdMode0Exception:
            print('Received packet with key_id_mode = 0, cannot be handled in test scripts')
            raise DropPacketException
