"""
nf_tables expression netlink attributes

See EXPRESSIONS in nft(8).
"""

from socket import AF_INET, AF_INET6
from pyroute2.nftables.parser.parser import nfta_nla_parser, conv_map_tuple


class NFTReg(object):

    def __init__(self, num):
        self.num = num

    @classmethod
    def from_netlink(cls, nlval):
        # please, for more information read nf_tables.h.
        if nlval == 'NFT_REG_VERDICT':
            num = 0
        else:
            num = int(nlval.split('_')[-1].lower())
            if nlval.startswith('NFT_REG32_'):
                num += 8
        return cls(num=num)

    @staticmethod
    def to_netlink(reg):
        # please, for more information read nf_tables.h.
        if reg.num == 0:
            return 'NFT_REG_VERDICT'
        if reg.num < 8:
            return 'NFT_REG_{0}'.format(reg.num)
        return 'NFT_REG32_{0}'.format(reg.num)

    @classmethod
    def from_dict(cls, val):
        return cls(num=val)

    def to_dict(self):
        return self.num


class NFTVerdict(object):

    def __init__(self, verdict, chain):
        self.verdict = verdict
        self.chain = chain

    @classmethod
    def from_netlink(cls, ndmsg):
        if ndmsg.get_attr('NFTA_VERDICT_CODE') is not None:
            verdict = ndmsg.get_attr(
                'NFTA_VERDICT_CODE').split('_')[-1].lower()
        else:
            verdict = None
        chain = ndmsg.get_attr('NFTA_VERDICT_CHAIN')
        return cls(verdict=verdict, chain=chain)

    def to_netlink(self):
        attrs = [('NFTA_VERDICT_CODE', 'NF_' + self.verdict)]
        if self.chain is not None:
            attrs.append(('NFTA_VERDICT_CHAIN', self.chain))
        return attrs

    @classmethod
    def from_dict(cls, d):
        return cls(verdict=d['verdict'], chain=d.get('chain', None))

    def to_dict(self):
        d = {'verdict': self.verdict}
        if self.chain is not None:
            d['chain'] = self.chain
        return d


class NFTData(object):

    def __init__(self, data_type, data):
        self.type = data_type
        self.data = data

    def to_netlink(self):
        if self.type == 'value':
            return ('NFTA_DATA_VALUE', self.data)
        if self.type == 'verdict':
            return ('NFTA_DATA_VERDICT', self.data)
        raise NotImplementedError(self.type)

    @classmethod
    def from_netlink(cls, ndmsg):
        if ndmsg.get_attr('NFTA_DATA_VALUE') is not None:
            kwargs = {'data_type': 'value',
                      'data': ndmsg.get_attr('NFTA_DATA_VALUE')}
        elif ndmsg.get_attr('NFTA_DATA_VERDICT') is not None:
            kwargs = {'data_type': 'verdict',
                      'data': NFTVerdict.from_netlink(
                          ndmsg.get_attr('NFTA_DATA_VERDICT'))}
        else:
            raise NotImplementedError(ndmsg)
        return cls(**kwargs)

    @classmethod
    def from_dict(cls, data):
        def from_32hex(val):
            return bytes(bytearray.fromhex(val[2:]))[::-1]

        kwargs = {}
        data = data["reg"]
        if data['type'] == 'value':
            value = bytes()
            for i in range(0, data['len'], 4):
                value += from_32hex(data['data{0}'.format(i / 4)])
            kwargs['data'] = value
        elif data['type'] == 'verdict':
            kwargs['data'] = NFTVerdict.from_dict(data)
        else:
            raise NotImplementedError()
        kwargs['data_type'] = data['type']
        return cls(**kwargs)

    def to_dict(self):
        def to_32hex(s):
            res = ''.join('{:02x}'.format(c) for c in bytearray(s)[::-1])
            return '0x' + res.zfill(8)

        if self.type == 'value':
            len_data = len(self.data)
            d = {'type': 'value', 'len': len_data}
            for i in range(0, len_data, 4):
                d['data{0}'.format(i / 4)] = to_32hex(self.data[i:i + 4])
        elif self.type == 'verdict':
            d = self.data.to_dict()
            d['type'] = 'verdict'
        else:
            raise NotImplementedError()
        return {"reg": d}


class NFTRuleExpr(nfta_nla_parser):

    #######################################################################
    conv_maps = (
        conv_map_tuple('name', 'NFTA_EXPR_NAME', 'type', 'raw'),
    )
    #######################################################################

    @classmethod
    def from_netlink(cls, expr_type, ndmsg):
        inst = super(NFTRuleExpr, cls).from_netlink(ndmsg)
        inst.name = expr_type
        return inst

    cparser_reg = NFTReg
    cparser_data = NFTData

    class cparser_extract_str(object):
        STRVAL = None

        @classmethod
        def from_netlink(cls, val):
            magic = '{0}'
            left, right = cls.STRVAL.split(magic, 1)
            if right:
                val = val[len(left):-len(right)]
            else:
                val = val[len(left):]
            return val.lower()

        @classmethod
        def to_netlink(cls, val):
            return cls.STRVAL.format(val).upper()

        @staticmethod
        def from_dict(val):
            return val

        @staticmethod
        def to_dict(val):
            return val

    class cparser_inet_family(object):
        @staticmethod
        def from_netlink(val):
            if val == AF_INET:
                return 'ip'
            if val == AF_INET6:
                return 'ip6'
            return val

        @staticmethod
        def to_netlink(val):
            if val == 'ip':
                return AF_INET
            if val == 'ip6':
                return AF_INET6
            return val

        @staticmethod
        def from_dict(val):
            return val

        @staticmethod
        def to_dict(val):
            return val


class ExprMeta(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('key', 'NFTA_META_KEY', 'key', 'meta_key'),
        conv_map_tuple('dreg', 'NFTA_META_DREG', 'dreg', 'reg'),
    )

    class cparser_meta_key(NFTRuleExpr.cparser_extract_str):
        STRVAL = 'NFT_META_{0}'


class ExprCmp(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('sreg', 'NFTA_CMP_SREG', 'sreg', 'reg'),
        conv_map_tuple('op', 'NFTA_CMP_OP', 'op', 'cmp_op'),
        conv_map_tuple('data', 'NFTA_CMP_DATA', 'data', 'data'),
    )

    class cparser_cmp_op(NFTRuleExpr.cparser_extract_str):
        STRVAL = 'NFT_CMP_{0}'


class ExprImmediate(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('dreg', 'NFTA_IMMEDIATE_DREG', 'dreg', 'reg'),
        conv_map_tuple('data', 'NFTA_IMMEDIATE_DATA', 'data', 'data'),
    )


class ExprPayload(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('dreg', 'NFTA_PAYLOAD_DREG', 'dreg', 'reg'),
        conv_map_tuple('base', 'NFTA_PAYLOAD_BASE', 'base', 'payload_base'),
        conv_map_tuple('offset', 'NFTA_PAYLOAD_OFFSET', 'offset', 'raw'),
        conv_map_tuple('len', 'NFTA_PAYLOAD_LEN', 'len', 'raw'),
    )

    class cparser_payload_base(NFTRuleExpr.cparser_extract_str):
        STRVAL = 'NFT_PAYLOAD_{0}_HEADER'

        @classmethod
        def from_netlink(cls, ndmsg):
            val = super(
                ExprPayload.cparser_payload_base, cls).from_netlink(ndmsg)
            if val == 'll':
                return 'link'
            return val

        @classmethod
        def to_netlink(cls, val):
            if val == 'link':
                val = 'll'
            return super(ExprPayload.cparser_payload_base, cls).to_netlink(val)

        @staticmethod
        def from_dict(val):
            return val

        @staticmethod
        def to_dict(val):
            return val


class ExprLookup(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('setid', 'NFTA_LOOKUP_SET', 'set', 'raw'),
        conv_map_tuple('sreg', 'NFTA_LOOKUP_SREG', 'sreg', 'reg'),
        conv_map_tuple('flags', 'NFTA_LOOKUP_FLAGS', 'flags', 'raw'),
    )


class ExprNat(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('nat_type', 'NFTA_NAT_TYPE', 'nat_type', 'nat_type'),
        conv_map_tuple('family', 'NFTA_NAT_FAMILY', 'family', 'inet_family'),
        conv_map_tuple('sreg_addr_min', 'NFTA_NAT_REG_ADDR_MIN',
                       'sreg_addr_min', 'reg'),
        conv_map_tuple('sreg_addr_max', 'NFTA_NAT_REG_ADDR_MAX',
                       'sreg_addr_max', 'reg'),
    )

    class cparser_nat_type(NFTRuleExpr.cparser_extract_str):
        STRVAL = 'NFT_NAT_{0}'


class ExprBitwise(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('sreg', 'NFTA_BITWISE_SREG', 'sreg', 'reg'),
        conv_map_tuple('dreg', 'NFTA_BITWISE_DREG', 'dreg', 'reg'),
        conv_map_tuple('len', 'NFTA_BITWISE_LEN', 'len', 'raw'),
        conv_map_tuple('mask', 'NFTA_BITWISE_MASK', 'mask', 'data'),
        conv_map_tuple('xor', 'NFTA_BITWISE_XOR', 'xor', 'data'),
    )


class ExprCounter(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('bytes', 'NFTA_COUNTER_BYTES', 'bytes', 'raw'),
        conv_map_tuple('packets', 'NFTA_COUNTER_PACKETS', 'pkts', 'raw'),
    )


class ExprMatch(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('match_name', 'NFTA_MATCH_NAME', 'name', 'raw'),
    )


class ExprTarget(NFTRuleExpr):

    conv_maps = NFTRuleExpr.conv_maps + (
        conv_map_tuple('target_name', 'NFTA_TARGET_NAME', 'name', 'raw'),
    )


NFTA_EXPR_NAME_MAP = {
    'meta': ExprMeta,
    'cmp': ExprCmp,
    'immediate': ExprImmediate,
    'payload': ExprPayload,
    'lookup': ExprLookup,
    'nat': ExprNat,
    'bitwise': ExprBitwise,
    'counter': ExprCounter,
    'match': ExprMatch,
    'target': ExprTarget,
}


def get_expression_from_netlink(ndmsg):
    name = ndmsg.get_attr('NFTA_EXPR_NAME')
    try:
        expr_cls = NFTA_EXPR_NAME_MAP[name]
    except KeyError:
        raise NotImplementedError(
            "can't load rule expression {0} from netlink {1}".format(
                name, ndmsg))
    return expr_cls.from_netlink(name, ndmsg.get_attr('NFTA_EXPR_DATA'))


def get_expression_from_dict(d):
    name = d['type']
    if name in NFTA_EXPR_NAME_MAP:
        expr_cls = NFTA_EXPR_NAME_MAP[name]
    else:
        raise NotImplementedError(
            "can't load rule expression {0} from json {1}".format(name, d))
    return expr_cls.from_dict(d)
