# $Id: ppp.py 65 2010-03-26 02:53:51Z dugsong $
# -*- coding: utf-8 -*-
"""Point-to-Point Protocol."""
from __future__ import absolute_import

import struct

from . import dpkt

# XXX - finish later

# http://www.iana.org/assignments/ppp-numbers
PPP_IP = 0x21  # Internet Protocol
PPP_IP6 = 0x57  # Internet Protocol v6

# Protocol field compression
PFC_BIT = 0x01


class PPP(dpkt.Packet):
    # Note: This class is subclassed in PPPoE
    """Point-to-Point Protocol.

    TODO: Longer class information....

    Attributes:
        __hdr__: Header fields of PPP.
        TODO.
    """

    __hdr__ = (
        ('addr', 'B', 0xff),
        ('cntrl', 'B', 3),
        ('p', 'B', PPP_IP),
    )
    _protosw = {}

    @classmethod
    def set_p(cls, p, pktclass):
        cls._protosw[p] = pktclass

    @classmethod
    def get_p(cls, p):
        return cls._protosw[p]

    def unpack(self, buf):
        dpkt.Packet.unpack(self, buf)
        if self.p & PFC_BIT == 0:
            try:
                self.p = struct.unpack('>H', buf[2:4])[0]
            except struct.error:
                raise dpkt.NeedData
            self.data = self.data[1:]
        try:
            self.data = self._protosw[self.p](self.data)
            setattr(self, self.data.__class__.__name__.lower(), self.data)
        except (KeyError, struct.error, dpkt.UnpackError):
            pass

    def pack_hdr(self):
        try:
            if self.p > 0xff:
                return struct.pack('>BBH', self.addr, self.cntrl, self.p)
            return dpkt.Packet.pack_hdr(self)
        except struct.error as e:
            raise dpkt.PackError(str(e))


def __load_protos():
    g = globals()
    for k, v in g.items():
        if k.startswith('PPP_'):
            name = k[4:]
            modname = name.lower()
            try:
                mod = __import__(modname, g, level=1)
                PPP.set_p(v, getattr(mod, name))
            except (ImportError, AttributeError):
                continue


def _mod_init():
    """Post-initialization called when all dpkt modules are fully loaded"""
    if not PPP._protosw:
        __load_protos()


def test_ppp():
    # Test protocol compression
    s = b"\xff\x03\x21"
    p = PPP(s)
    assert p.p == 0x21

    s = b"\xff\x03\x00\x21"
    p = PPP(s)
    assert p.p == 0x21


def test_ppp_short():
    s = b"\xff\x03\x00"

    import pytest
    pytest.raises(dpkt.NeedData, PPP, s)


def test_packing():
    p = PPP()
    assert p.pack_hdr() == b"\xff\x03\x21"

    p.p = 0xc021  # LCP
    assert p.pack_hdr() == b"\xff\x03\xc0\x21"


def test_ppp_classmethods():
    import pytest

    class TestProto(dpkt.Packet):
        pass

    proto_number = 123

    # asserting that this proto is not currently added
    with pytest.raises(KeyError):
        PPP.get_p(proto_number)

    PPP.set_p(proto_number, TestProto)

    assert PPP.get_p(proto_number) == TestProto

    # we need to reset the class, or impact other tests
    del PPP._protosw[proto_number]


def test_unpacking_exceptions():
    from dpkt import ip

    from binascii import unhexlify
    buf_ppp = unhexlify(
        'ff'  # addr
        '03'  # cntrl
        '21'  # p (PPP_IP)
    )
    buf_ip = unhexlify(
        '45'    # _v_hl
        '00'    # tos
        '0014'  # len
        '0000'  # id
        '0000'  # off
        '80'    # ttl
        '06'    # p
        'd47e'  # sum
        '11111111'  # src
        '22222222'  # dst
    )

    buf = buf_ppp + buf_ip
    ppp = PPP(buf)
    assert hasattr(ppp, 'ip')
    assert isinstance(ppp.data, ip.IP)
    assert bytes(ppp) == buf


def test_ppp_packing_error():
    import pytest

    # addr is a 1-byte field, so this will overflow when packing
    ppp = PPP(p=257, addr=1234)
    with pytest.raises(dpkt.PackError):
        ppp.pack_hdr()


def test_proto_loading():
    # test that failure to load protocol handlers isn't catastrophic
    standard_protos = PPP._protosw
    # delete existing protos
    PPP._protosw = {}
    assert not PPP._protosw

    # inject a new global variable to be picked up
    globals()['PPP_NON_EXISTENT_PROTO'] = "FAIL"
    _mod_init()
    # we should get the same answer as if NON_EXISTENT_PROTO didn't exist
    assert PPP._protosw == standard_protos
