import sys
import struct
from array import array
from pyroute2.common import basestring
from pyroute2.protocols import msg

BOOTREQUEST = 1
BOOTREPLY = 2

DHCPDISCOVER = 1
DHCPOFFER = 2
DHCPREQUEST = 3
DHCPDECLINE = 4
DHCPACK = 5
DHCPNAK = 6
DHCPRELEASE = 7
DHCPINFORM = 8


if not hasattr(array, 'tobytes'):
    # Python2 and Python3 versions of array differ,
    # but we need here a consistent API w/o warnings
    class array(array):
        tobytes = array.tostring


class option(msg):

    code = 0
    data_length = 0
    policy = None
    value = None

    def __init__(self, content=None, buf=b'', offset=0, value=None, code=0):
        msg.__init__(self, content=content, buf=buf,
                     offset=offset, value=value)
        self.code = code

    @property
    def length(self):
        if self.data_length is None:
            return None
        if self.data_length == 0:
            return 1
        else:
            return self.data_length + 2

    def encode(self):
        # pack code
        self.buf += struct.pack('B', self.code)
        if self.code in (0, 255):
            return self
        # save buf
        save = self.buf
        self.buf = b''
        # pack data into the new buf
        if self.policy is not None:
            value = self.policy.get('encode', lambda x: x)(self.value)
            if self.policy['fmt'] == 'string':
                fmt = '%is' % len(value)
            else:
                fmt = self.policy['fmt']
            if sys.version_info[0] == 3 and isinstance(value, str):
                value = value.encode('utf-8')
            self.buf = struct.pack(fmt, value)
        else:
            msg.encode(self)
        # get the length
        data = self.buf
        self.buf = save
        self.buf += struct.pack('B', len(data))
        # attach the packed data
        self.buf += data
        return self

    def decode(self):
        if self.policy is not None:
            self.data_length = struct.unpack('B', self.buf[self.offset + 1:
                                                           self.offset + 2])[0]
            if self.policy['fmt'] == 'string':
                fmt = '%is' % self.data_length
            else:
                fmt = self.policy['fmt']
            value = struct.unpack(fmt, self.buf[self.offset + 2:
                                                self.offset + 2 +
                                                self.data_length])
            if len(value) == 1:
                value = value[0]
            value = self.policy.get('decode', lambda x: x)(value)
            if isinstance(value, basestring) and \
                    self.policy['fmt'] == 'string':
                value = value[:value.find('\x00')]
            self.value = value
        else:
            msg.decode(self)
        return self


class dhcpmsg(msg):
    options = ()
    l2addr = None
    _encode_map = {}
    _decode_map = {}

    def _register_options(self):
        for option in self.options:
            code, name, fmt = option[:3]
            self._decode_map[code] =\
                self._encode_map[name] = {'name': name,
                                          'code': code,
                                          'fmt': fmt}

    def decode(self):
        msg.decode(self)
        self._register_options()
        self['options'] = {}
        while self.offset < len(self.buf):
            code = struct.unpack('B', self.buf[self.offset:self.offset + 1])[0]
            if code == 0:
                self.offset += 1
                continue
            if code == 255:
                return self
            # code is unknown -- bypass it
            if code not in self._decode_map:
                length = struct.unpack('B', self.buf[self.offset + 1:
                                                     self.offset + 2])[0]
                self.offset += length + 2
                continue

            # code is known, work on it
            option_class = getattr(self, self._decode_map[code]['fmt'])
            option = option_class(buf=self.buf, offset=self.offset)
            option.decode()
            self.offset += option.length
            if option.value is not None:
                value = option.value
            else:
                value = option
            self['options'][self._decode_map[code]['name']] = value
        return self

    def encode(self):
        msg.encode(self)
        self._register_options()
        # put message type
        options = self['options'] or {'message_type': DHCPDISCOVER,
                                      'parameter_list': [1, 3, 6, 12, 15, 28]}

        self.buf += self.uint8(code=53,
                               value=options['message_type']).encode().buf
        self.buf += self.client_id({'type': 1,
                                    'key': self['chaddr']},
                                   code=61).encode().buf
        self.buf += self.string(code=60, value='pyroute2').encode().buf

        for (name, value) in options.items():
            if name in ('message_type', 'client_id', 'vendor_id'):
                continue
            fmt = self._encode_map.get(name, {'fmt': None})['fmt']
            if fmt is None:
                continue
            # name is known, ok
            option_class = getattr(self, fmt)
            if isinstance(value, dict):
                option = option_class(value,
                                      code=self._encode_map[name]['code'])
            else:
                option = option_class(code=self._encode_map[name]['code'],
                                      value=value)
            self.buf += option.encode().buf

        self.buf += self.none(code=255).encode().buf
        return self

    class none(option):
        pass

    class be16(option):
        policy = {'fmt': '>H'}

    class be32(option):
        policy = {'fmt': '>I'}

    class uint8(option):
        policy = {'fmt': 'B'}

    class string(option):
        policy = {'fmt': 'string'}

    class array8(option):
        policy = {'fmt': 'string',
                  'encode': lambda x: array('B', x).tobytes(),
                  'decode': lambda x: array('B', x).tolist()}

    class client_id(option):
        fields = (('type', 'uint8'),
                  ('key', 'l2addr'))

@@ -4497,25 +4497,29 @@
 tions = self
-%5B
+.get(
 'options'%5D o
@@ -4515,17 +4515,17 @@
 options'
-%5D
+)
  or %7B'me
@@ -4589,16 +4589,20 @@
         
+    
 'paramet
@@ -4620,16 +4620,77 @@
 1, 3, 6,
+%0A                                                            
  12, 15,

fix empty options