import collections
import array
import struct

__author__ = 'Giacomo Tanganelli'

""" CoAP Parameters """

ACK_TIMEOUT = 2  # standard 2

SEPARATE_TIMEOUT = ACK_TIMEOUT / 2

ACK_RANDOM_FACTOR = 1.5

MAX_RETRANSMIT = 4

MAX_TRANSMIT_SPAN = ACK_TIMEOUT * (pow(2, (MAX_RETRANSMIT + 1)) - 1) * ACK_RANDOM_FACTOR

MAX_LATENCY = 120  # 2 minutes

PROCESSING_DELAY = ACK_TIMEOUT

MAX_RTT = (2 * MAX_LATENCY) + PROCESSING_DELAY

EXCHANGE_LIFETIME = MAX_TRANSMIT_SPAN + (2 * MAX_LATENCY) + PROCESSING_DELAY

DISCOVERY_URL = "/.well-known/core"

ALL_COAP_NODES = "224.0.1.187"

ALL_COAP_NODES_IPV6 = "FF00::FD"

MAX_PAYLOAD = 1024

MAX_NON_NOTIFICATIONS = 10

BLOCKWISE_SIZE = 1024

"""  Message Format """

# number of bits used for the encoding of the CoAP version field.
VERSION_BITS = 2

# number of bits used for the encoding of the message type field.
TYPE_BITS = 2

# number of bits used for the encoding of the token length field.
TOKEN_LENGTH_BITS = 4

# number of bits used for the encoding of the request method/response code field.
CODE_BITS = 8

# number of bits used for the encoding of the message ID.
MESSAGE_ID_BITS = 16

# number of bits used for the encoding of the option delta field.
OPTION_DELTA_BITS = 4

# number of bits used for the encoding of the option delta field.
OPTION_LENGTH_BITS = 4

# One byte which indicates indicates the end of options and the start of the payload.
PAYLOAD_MARKER = 0xFF

# CoAP version supported by this Californium version.
VERSION = 1

# The lowest value of a request code.
REQUEST_CODE_LOWER_BOUND = 1

# The highest value of a request code.
REQUEST_CODE_UPPER_BOUND = 31

# The lowest value of a response code.
RESPONSE_CODE_LOWER_BOUND = 64

# The highest value of a response code.
RESPONSE_CODE_UPPER_BOUND = 191

corelinkformat = {
    'ct': 'content_type',
    'rt': 'resource_type',
    'if': 'interface_type',
    'sz': 'maximum_size_estimated',
    'obs': 'observing'
}

# The integer.
INTEGER = 0
# The string.
STRING = 1
# The opaque.
OPAQUE = 2
# The unknown.
UNKNOWN = 3

# Cache modes
FORWARD_PROXY = 0
REVERSE_PROXY = 1

OptionItem = collections.namedtuple('OptionItem', 'number name value_type repeatable default')


class OptionRegistry(object):
    """
    All CoAP options. Every option is represented as: (NUMBER, NAME, VALUE_TYPE, REPEATABLE, DEFAULT)
    """
    def __init__(self):
        pass

    RESERVED =      OptionItem(0, "Reserved",       UNKNOWN, True, None)
    IF_MATCH =      OptionItem(1, "If-Match",       OPAQUE,  True, None)
    URI_HOST =      OptionItem(3, "Uri-Host",       STRING,  True, None)
    ETAG =          OptionItem(4, "ETag",           OPAQUE,  True, None)
    IF_NONE_MATCH = OptionItem(5, "If-None-Match",  OPAQUE, False, None)
    OBSERVE =       OptionItem(6, "Observe",        INTEGER, False, 0)
    URI_PORT =      OptionItem(7, "Uri-Port",       INTEGER, False, 5683)
    LOCATION_PATH = OptionItem(8, "Location-Path",  STRING,  True, None)
    URI_PATH =      OptionItem(11, "Uri-Path",      STRING,  True, None)
    CONTENT_TYPE =  OptionItem(12, "Content-Type",  INTEGER, False, 0)
    MAX_AGE =       OptionItem(14, "Max-Age",       INTEGER, False, 60)
    URI_QUERY =     OptionItem(15, "Uri-Query",     STRING,  True, None)
    ACCEPT =        OptionItem(17, "Accept",        INTEGER, False, 0)
    LOCATION_QUERY = OptionItem(20,"Location-Query",STRING,  True, None)
    BLOCK2 =        OptionItem(23, "Block2",        INTEGER, False, None)
    BLOCK1 =        OptionItem(27, "Block1",        INTEGER, False, None)
    PROXY_URI =     OptionItem(35, "Proxy-Uri",     STRING,  False, None)
    PROXY_SCHEME =  OptionItem(39, "Proxy-Schema",  STRING,  False, None)
    SIZE1 =         OptionItem(60, "Size1",         INTEGER, False, None)
    NO_RESPONSE =   OptionItem(258, "No-Response",  INTEGER, False, None)
    RM_MESSAGE_SWITCHING = OptionItem(65524, "Routing", OPAQUE, False, None)

    LIST = {
        0: RESERVED,
        1: IF_MATCH,
        3: URI_HOST,
        4: ETAG,
        5: IF_NONE_MATCH,
        6: OBSERVE,
        7: URI_PORT,
        8: LOCATION_PATH,
        11: URI_PATH,
        12: CONTENT_TYPE,
        14: MAX_AGE,
        15: URI_QUERY,
        17: ACCEPT,
        20: LOCATION_QUERY,
        23: BLOCK2,
        27: BLOCK1,
        35: PROXY_URI,
        39: PROXY_SCHEME,
        60: SIZE1,
        258: NO_RESPONSE,
        65524: RM_MESSAGE_SWITCHING

    }

    @staticmethod
    def get_option_flags(option_num):
        """
        Get Critical, UnSafe, NoCacheKey flags from the option number
        as per RFC 7252, section 5.4.6

        :param option_num: option number
        :return: option flags
        :rtype: 3-tuple (critical, unsafe, no-cache)
        """
        opt_bytes = array.array('B', '\0\0')
        if option_num < 256:
            s = struct.Struct("!B")
            s.pack_into(opt_bytes, 0, option_num)
        else:
            s = struct.Struct("H")
            s.pack_into(opt_bytes, 0, option_num)
        critical = (opt_bytes[0] & 0x01) > 0
        unsafe = (opt_bytes[0] & 0x02) > 0
        nocache = ((opt_bytes[0] & 0x1e) == 0x1c)
        return (critical, unsafe, nocache)

Types = {
    'CON': 0,
    'NON': 1,
    'ACK': 2,
    'RST': 3,
    'None': None
}

CodeItem = collections.namedtuple('CodeItem', 'number name')


class Codes(object):
    """
    CoAP codes. Every code is represented as (NUMBER, NAME)
    """
    ERROR_LOWER_BOUND = 128

    EMPTY = CodeItem(0, 'EMPTY')
    GET = CodeItem(1, 'GET')
    POST = CodeItem(2, 'POST')
    PUT = CodeItem(3, 'PUT')
    DELETE = CodeItem(4, 'DELETE')

    CREATED = CodeItem(65, 'CREATED')
    DELETED = CodeItem(66, 'DELETED')
    VALID = CodeItem(67, 'VALID')
    CHANGED = CodeItem(68, 'CHANGED')
    CONTENT = CodeItem(69, 'CONTENT')
    CONTINUE = CodeItem(95, 'CONTINUE')

    BAD_REQUEST = CodeItem(128, 'BAD_REQUEST')
    FORBIDDEN = CodeItem(131, 'FORBIDDEN')
    NOT_FOUND = CodeItem(132, 'NOT_FOUND')
    METHOD_NOT_ALLOWED = CodeItem(133, 'METHOD_NOT_ALLOWED')
    NOT_ACCEPTABLE = CodeItem(134, 'NOT_ACCEPTABLE')
    REQUEST_ENTITY_INCOMPLETE = CodeItem(136, 'REQUEST_ENTITY_INCOMPLETE')
    PRECONDITION_FAILED = CodeItem(140, 'PRECONDITION_FAILED')
    REQUEST_ENTITY_TOO_LARGE = CodeItem(141, 'REQUEST_ENTITY_TOO_LARGE')
    UNSUPPORTED_CONTENT_FORMAT = CodeItem(143, 'UNSUPPORTED_CONTENT_FORMAT')

    INTERNAL_SERVER_ERROR = CodeItem(160, 'INTERNAL_SERVER_ERROR')
    NOT_IMPLEMENTED = CodeItem(161, 'NOT_IMPLEMENTED')
    BAD_GATEWAY = CodeItem(162, 'BAD_GATEWAY')
    SERVICE_UNAVAILABLE = CodeItem(163, 'SERVICE_UNAVAILABLE')
    GATEWAY_TIMEOUT = CodeItem(164, 'GATEWAY_TIMEOUT')
    PROXY_NOT_SUPPORTED = CodeItem(165, 'PROXY_NOT_SUPPORTED')

    LIST = {
        0: EMPTY,
        1: GET,
        2: POST,
        3: PUT,
        4: DELETE,

        65: CREATED,
        66: DELETE,
        67: VALID,
        68: CHANGED,
        69: CONTENT,
        95: CONTINUE,

        128: BAD_REQUEST,
        131: FORBIDDEN,
        132: NOT_FOUND,
        133: METHOD_NOT_ALLOWED,
        134: NOT_ACCEPTABLE,
        136: REQUEST_ENTITY_INCOMPLETE,
        140: PRECONDITION_FAILED,
        141: REQUEST_ENTITY_TOO_LARGE,
        143: UNSUPPORTED_CONTENT_FORMAT,

        160: INTERNAL_SERVER_ERROR,
        161: NOT_IMPLEMENTED,
        162: BAD_GATEWAY,
        163: SERVICE_UNAVAILABLE,
        164: GATEWAY_TIMEOUT,
        165: PROXY_NOT_SUPPORTED

    }


Content_types = {
    "text/plain": 0,
    "application/link-format": 40,
    "application/xml": 41,
    "application/octet-stream": 42,
    "application/exi": 47,
    "application/json": 50,
    "application/cbor": 60
}

COAP_PREFACE = "coap://"
LOCALHOST = "127.0.0.1"
HC_PROXY_DEFAULT_PORT = 8080  # TODO there is a standard for this?
COAP_DEFAULT_PORT = 5683
DEFAULT_HC_PATH = "/"
BAD_REQUEST = 400  # "Bad Request" error code
NOT_IMPLEMENTED = 501  # "Not Implemented" error code

# Dictionary to map CoAP to HTTP requests code
CoAP_HTTP = {

    "CREATED": "201",
    "DELETED": "200",
    "VALID": "304",
    "CHANGED": "200",
    "CONTENT": "200",
    "BAD_REQUEST": "400",
    "FORBIDDEN": "403",
    "NOT_FOUND": "404",
    "METHOD_NOT_ALLOWED": "400",
    "NOT_ACCEPTABLE": "406",
    "PRECONDITION_FAILED": "412",
    "REQUEST_ENTITY_TOO_LARGE": "413",
    "UNSUPPORTED_CONTENT_FORMAT": "415",
    "INTERNAL_SERVER_ERROR": "500",
    "NOT_IMPLEMENTED": "501",
    "BAD_GATEWAY": "502",
    "SERVICE_UNAVAILABLE": "503",
    "GATEWAY_TIMEOUT": "504",
    "PROXY_NOT_SUPPORTED": "502"

}