# coding=utf8
from __future__ import unicode_literals, absolute_import, print_function


import ast


class Position(object):

    def __init__(self, start, end=None):
        self.start = start
        self.end = end


class Node(object):

    param_keys = []
    children_keys = []

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

    @property
    def node_name(self):
        return self.__class__.__name__

    @classmethod
    def create(cls, **kwargs):
        return create(cls, **kwargs)

    def accept(self, visitor):
        return visitor.dispatch_visit(self)


class Type(Node):

    pass


class Symbol(Node):

    param_keys = ['name']

    def __init__(self, name, pos):
        super(Symbol, self).__init__(pos)
        assert isinstance(name, basestring)
        self.name = name


class TypeName(Node):

    param_keys = ['name']

    def __init__(self, name, pos):
        super(TypeName, self).__init__(pos)
        assert isinstance(name, basestring)
        self.name = name


class LineComment(Node):

    param_keys = ['text']

    def __init__(self, text, pos):
        super(LineComment, self).__init__(pos)
        assert text is None or isinstance(text, basestring)
        self.text = text


class BlockComment(Node):

    param_keys = ['text']

    def __init__(self, text, pos):
        super(BlockComment, self).__init__(pos)
        assert text is None or isinstance(text, basestring)
        self.text = text


class Pragma(Node):

    param_keys = ['text']

    def __init__(self, text, pos):
        super(Pragma, self).__init__(pos)
        assert text is None or isinstance(text, basestring)
        self.text = text


class NamedType(Type):

    children_keys = ['type_name']

    def __init__(self, type_name, pos):
        super(NamedType, self).__init__(pos)
        assert isinstance(type_name, TypeName)
        self.type_name = type_name


class ArrayType(Type):

    children_keys = ['element_type']

    def __init__(self, element_type, pos):
        super(ArrayType, self).__init__(pos)
        assert isinstance(element_type, Type)
        self.element_type = element_type


class GenericInstanceType(Type):

    children_keys = ['type_name', 'type_arguments']

    def __init__(self, type_name, type_arguments, pos):
        super(GenericInstanceType, self).__init__(pos)
        assert isinstance(type_name, TypeName)
        assert isinstance(type_arguments, list)
        for t in type_arguments:
            assert isinstance(t, Type)
        self.type_name = type_name
        self.type_arguments = type_arguments


class RecordType(Type):

    children_keys = ['entries']

    def __init__(self, entries, pos):
        super(RecordType, self).__init__(pos)
        entries = list(entries)
        for entry in entries:
            assert isinstance(entry, RecordTypeEntry)
        self.entries = entries


class RecordTypeEntry(Node):

    children_keys = ['symbol', 'element_type', 'block_comment', 'line_comment']

    def __init__(self, symbol, element_type, block_comment, line_comment, pos):
        super(RecordTypeEntry, self).__init__(pos)
        assert isinstance(symbol, Symbol)
        assert isinstance(element_type, Type)
        assert isinstance(block_comment, BlockComment)
        assert isinstance(line_comment, LineComment)
        self.symbol = symbol
        self.element_type = element_type
        self.block_comment = block_comment
        self.line_comment = line_comment


class UnionType(Type):

    children_keys = ['choices']

    def __init__(self, choices, pos):
        super(UnionType, self).__init__(pos)
        choices = list(choices)
        for choice in choices:
            assert isinstance(choice, Type)
        self.choices = choices


class TypeDef(Node):

    children_keys = ['type_name', 'type_', 'block_comment', 'line_comment']

    def __init__(self, type_name, type_, block_comment, line_comment, pos):
        super(TypeDef, self).__init__(pos)
        assert isinstance(type_name, TypeName)
        assert isinstance(type_, Type)
        assert isinstance(block_comment, BlockComment)
        assert isinstance(line_comment, LineComment)
        self.type_name = type_name
        self.type_ = type_
        self.block_comment = block_comment
        self.line_comment = line_comment


class Code(Node):

    children_keys = ['members']

    def __init__(self, members, pos):
        super(Code, self).__init__(pos)
        members = list(members)
        for m in members:
            assert isinstance(m, Node)
        self.members = members


node_list = [
    Symbol,
    TypeName,
    LineComment,
    BlockComment,
    Pragma,
    NamedType,
    ArrayType,
    GenericInstanceType,
    RecordType,
    RecordTypeEntry,
    UnionType,
    TypeDef,
    Code,
]


def create(node_type, **kwargs):
    assert node_type in node_list
    pos = kwargs.pop('pos', None)
    kwargs_keys = set(kwargs.keys())
    expected_keys = set(node_type.children_keys + node_type.param_keys)
    if kwargs_keys != expected_keys:
        absent_args = expected_keys - kwargs_keys
        unexpected_args = kwargs_keys - expected_keys
        raise Exception("absent args: {}, unexpected args: {}".format(list(absent_args), list(unexpected_args)))
    kwargs.setdefault('pos', pos)
    return node_type(**kwargs)


class Visitor(object):

    def __init__(self, debug=False):
        self._debug = debug

    def dispatch_visit(self, node):
        func_pre_name = 'visit__pre_' + node.node_name
        func_pre = getattr(self, func_pre_name, self.visit__pre__default)

        func_child_name = 'visit__child_' + node.node_name
        func_child = getattr(self, func_child_name, self.visit__child__default)

        func_name = 'visit_' + node.node_name
        func = getattr(self, func_name, self.visit__default)

        d = {}
        for ck in node.children_keys:
            d[ck] = getattr(node, ck)

        if self._debug: print("VISIT : {}".format(func_pre_name))
        d = func_pre(node, **d)

        if self._debug: print("VISIT : {}".format(func_child_name))
        d = func_child(node, **d)

        if self._debug: print("VISIT : {}".format(func_name))
        r = func(node, **d)

        return r

    def visit__pre__default(self, node, **kwargs):
        return kwargs

    def visit__child__default(self, node, **kwargs):
        d = {}
        for ck, cv in kwargs.items():
            if isinstance(cv, list):
                cr = [v.accept(self) for v in cv]
            else:
                cr = cv.accept(self)
            d[ck] = cr
        return d

    def visit__default(self, node, **kwargs):
        raise NotImplementedError("visit_" + node.node_name)


def dump_visitor_template(namespace=None):

    def ns(attr):
        if namespace:
            s = namespace + '.' + attr
        else:
            s = attr
        sp = s.split('.')
        first, rest = sp[0], sp[1:]
        expr = ast.Name(id=first, ctx=ast.Load)
        for a in rest:
            expr = ast.Attribute(
                value=expr,
                attr=a,
                ctx=ast.Load,
            )
        return expr

    body = []
    for node_class in node_list:
        node_name = node_class.__name__
        visitor = ast.FunctionDef(
            name='visit_'+node_name,
            args=ast.arguments(
                args=[
                    ast.Name(id=arg_name, ctx=ast.Param)
                    for arg_name in ['self', 'node'] + node_class.children_keys
                ],
                vararg=[],
                kwarg=[],
                defaults=[],
            ),
            body=[
                ast.Assert(
                    test=ast.Call(
                        func=ast.Name(id='isinstance', ctx=ast.Load),
                        args=[
                            ast.Name(id='node', ctx=ast.Param),
                            ns(node_name),
                        ],
                        keywords=[],
                        starargs=[],
                        kwargs=[],
                    ),
                    msg=None,
                ),
                ast.Raise(
                    type=ast.Name(id="NotImplementedError", ctx=ast.Load),
                    inst=None,
                    tback=None,
                )
            ],
            decorator_list=[]
        )
        body.append(visitor)
    class_def = ast.ClassDef(
        name='VisitorTemplate',
        bases=[ns('Visitor')],
        body=body,
        decorator_list=[],
        )
    return class_def


if __name__ == '__main__':
    import sys
    import astunparse
    if len(sys.argv) == 1:
        ns = None
    elif len(sys.argv) == 2:
        ns = sys.argv[1]
    else:
        raise Exception("unexpected sys.argv")

    tree = dump_visitor_template(ns)
    code = astunparse.unparse(tree)
    print(code)
