# encoding: utf-8
"""
code.py

Created by Thomas Mangin on 2020-09-01.
Copyright (c) 2020 Exa Networks. All rights reserved.
"""

# Used https://github.com/asottile/astpretty
# to understand how the python AST works
# Python 3.9 will have ast.unparse but until then
# https://github.com/simonpercivall/astunparse
# is used to generate code from the AST created

from ast import Module, Import, FunctionDef, arguments, arg, alias
from ast import Load, Call, Return, Name, Attribute, Constant, Param
from ast import Add, If, Compare, Gt, Lt, GtE, LtE, And, Or
from ast import BoolOp, UnaryOp, Not, USub

import astunparse

from exabgp.conf.yang.datatypes import kw
from exabgp.conf.yang.datatypes import ranges


class Code(object):
    def __init__(self, tree):
        # the modules (import) required within the generated function
        self.imported = set()
        # type/function referenced in other types (union, ...)
        # which should therefore also be generated
        self.referenced = set()
        # the parsed yang as a tree
        self.tree = tree
        # the main yang namespace/module
        self.module = tree[kw['loaded']][0]
        # the namespace we are working in
        self.ns = self.module

    @staticmethod
    def _missing(**kargs):
        print(' '.join(f'{k}={v}' for k, v in kargs.items()))

        # this code path was not handled
        breakpoint()

    @staticmethod
    def _unique(name, counter={}):
        unique = counter.get(name, 0)
        unique += 1
        counter[name] = unique
        return f'{name}_{unique}'

    @staticmethod
    def _return_boolean(value):
        return [
            Return(
                value=Constant(value=value, kind=None),
            )
        ]

    def _python_name(self, name):
        if self.ns != self.tree[kw['loaded']][0] and ':' not in name:
            # XXX: could this lead to function shadowing?
            name = f'{self.ns}:{name}'
        # XXX: could this lead to function shadowing?
        return name.replace(':', '__').replace('-', '_')

    def _if_pattern(self, pattern):
        self.imported.add('re')
        # fix known ietf regex use
        pattern = pattern.replace('\\p{N}\\p{L}', '\\w')
        return [
            If(
                test=UnaryOp(
                    op=Not(),
                    operand=Call(
                        func=Attribute(
                            value=Name(id='re', ctx=Load()),
                            attr='match',
                            ctx=Load(),
                        ),
                        args=[
                            Constant(value=pattern, kind=None),
                            Name(id='value', ctx=Load()),
                            Attribute(
                                value=Name(id='re', ctx=Load()),
                                attr='UNICODE',
                                ctx=Load(),
                            ),
                        ],
                        keywords=[],
                    ),
                ),
                body=[
                    Return(
                        value=Constant(value=False, kind=None),
                    ),
                ],
                orelse=[],
            ),
        ]

    def _if_length(self, minimum, maximum):
        return [
            If(
                test=Compare(
                    left=Constant(value=int(minimum), kind=None),
                    ops=[
                        Gt(),
                        Gt(),
                    ],
                    comparators=[
                        Call(
                            func=Name(id='len', ctx=Load()),
                            args=[Name(id='value', ctx=Load())],
                            keywords=[],
                        ),
                        Constant(value=int(maximum), kind=None),
                    ],
                ),
                body=[
                    Return(
                        value=Constant(value=False, kind=None),
                    ),
                ],
                orelse=[],
            ),
        ]

    def _iter_if_string(self, node):
        for what, sub in node.items():
            if what == kw['pattern']:
                yield self._if_pattern(sub)
                continue

            if what == kw['match']:
                self._missing(if_type=what, node=node)
                continue

            if what == kw['length']:
                yield self._if_length(*sub)
                continue

            self._missing(if_type=what, node=node)

    @staticmethod
    def _if_digit():
        return [
            If(
                test=UnaryOp(
                    op=Not(),
                    operand=Call(
                        func=Attribute(
                            value=Name(id='value', ctx=Load()),
                            attr='isdigit',
                            ctx=Load(),
                        ),
                        args=[],
                        keywords=[],
                    ),
                ),
                body=[
                    Return(
                        value=Constant(value=False, kind=None),
                    ),
                ],
                orelse=[],
            )
        ]

    @staticmethod
    def _if_lt(value):
        if value >= 0:
            comparators = [Constant(value=value, kind=None)]
        else:
            comparators = [
                UnaryOp(
                    op=USub(),
                    operand=Constant(value=abs(value), kind=None),
                ),
            ]

        return [
            If(
                test=Compare(
                    left=Call(
                        func=Name(id='int', ctx=Load()),
                        args=[Name(id='value', ctx=Load())],
                        keywords=[],
                    ),
                    ops=[Lt()],
                    comparators=comparators,
                    keywords=[],
                ),
                body=[
                    Return(
                        value=Constant(value=False, kind=None),
                    ),
                ],
                orelse=[],
            )
        ]

    @staticmethod
    def _if_gt(value):
        if value >= 0:
            comparators = [Constant(value=value, kind=None)]
        else:
            comparators = [
                UnaryOp(
                    op=USub(),
                    operand=Constant(value=abs(value), kind=None),
                ),
            ]
        return [
            If(
                test=Compare(
                    left=Call(
                        func=Name(id='int', ctx=Load()),
                        args=[Name(id='value', ctx=Load())],
                        keywords=[],
                    ),
                    ops=[Gt()],
                    comparators=comparators,
                ),
                body=[
                    Return(
                        value=Constant(value=False, kind=None),
                    ),
                ],
                orelse=[],
            )
        ]

    def _if_range(self, minimum, maximum):
        return self._if_digit() + self._if_lt(minimum) + self._if_gt(maximum)

    def _union(self, node):
        values = []
        generated = []

        for union in node:
            for what, sub in union.items():
                if ':' in what:
                    if what in generated:
                        # only generate any imported function once
                        continue
                    generated.append(what)
                    name = what
                    yield self._type(what, name, sub)
                else:
                    # this is a build_in type (and my have been refined)
                    # therefore generate one function per type
                    name = self._unique(what)
                    yield self._function(name, self._type(what, what, sub))

                values += [
                    UnaryOp(
                        op=Not(),
                        operand=Call(
                            func=Name(id=self._python_name(name), ctx=Load()),
                            args=[Name(id='value', ctx=Load())],
                            keywords=[],
                        ),
                    ),
                ]

        yield [
            If(
                test=BoolOp(
                    op=And(),
                    values=values,
                ),
                body=[
                    Return(
                        value=Constant(value=False, kind=None),
                    ),
                ],
                orelse=[],
            ),
        ]

    def _imported(self):
        for imported in self.imported:
            yield Import(names=[alias(name=imported, asname=None)])

    def _type(self, what, name, node):
        if what == 'union':
            return list(self._union(node))

        if what in ('int8', 'int16', 'int16', 'int32', 'uint8', 'uint16', 'uint16', 'uint32'):
            # not dealing with refine
            minimum, maximum = ranges[what]
            return self._if_range(minimum, maximum)

        if what == 'string':
            return list(self._iter_if_string(node))

        if ':' in what:
            ns, name = what.split(':', 1)
            backup_ns, self.ns = self.ns, ns
            answer = list(self._typedef(ns, name))
            self.ns = backup_ns
            return answer

        self._missing(what=what, name=name, node=node)

    def _iter(self, node):
        for keyword, content in node.items():
            yield self._type(keyword, keyword, content)

    def _function(self, name, body):
        # XXX: could this lead to function shadowing?
        return [
            FunctionDef(
                name=self._python_name(name),
                args=arguments(
                    posonlyargs=[],
                    args=[arg(arg='value', annotation=None, type_comment=None)],
                    vararg=None,
                    kwonlyargs=[],
                    kw_defaults=[],
                    kwarg=None,
                    defaults=[],
                ),
                body=body + self._return_boolean(True),
                decorator_list=[],
                returns=None,
                type_comment=None,
            )
        ]

    def _typedef(self, module, only):
        td = self.tree[module][kw['typedef']]

        for name in td:
            if only and only != name:
                continue
            body = list(self._iter(td[name][kw['type']]))
            yield self._function(name, body)

    def _module(self, module, only=''):
        generated = list(self._typedef(module, only))
        # while self.referenced:
        #     module, check = self.referenced.pop(0)
        #     generated += list(self._typedef(module, check))
        return generated

    def generate(self, module):
        # this must be run first so that the imported module can be generated
        body = list(self._module(module))
        ast = Module(
            body=list(self._imported()) + body,
        )
        return ast
