# this should be invoked directly
from os import path
from typing import Dict, List, Tuple
import re

EnumDef = List[str]
EnumDict = Dict[str, EnumDef]
TypeDef = List[Tuple[str, str]]  # [(name, type)]
TypeDict = Dict[str, TypeDef]

output_dir = path.dirname(path.realpath(__file__))
title_case = re.compile(r'(?<!^)(?=[A-Z])')


def to_title_case(name: str) -> str:
    return title_case.sub('_', name).lower()


def generate_ast(base: str, imports: List[str], enums: EnumDict, types: Dict[str, List[Tuple[str, str]]]) -> List[str]:
    result = ['# autogenerated by {}'.format(path.basename(__file__)), "import abc"]

    if len(enums):
        result.append('from enum import auto, Enum')
    result.append("from typing import Optional")
    result.append("from parsing.source import SourceSpan")
    result.extend(imports)

    def one_blank():
        result.extend([''])

    def two_blank():
        result.extend(['', ''])

    def prop(name: str, type: str):
        result.append('    @property')
        result.append('    def {}(self) -> {}:'.format(name, type))
        result.append('        return self.__{}'.format(name))

    def props(name: str, type: str):
        result.append('    @property')
        result.append('    def {}(self) -> {}:'.format(name, type))
        result.append('        return self.__{}'.format(name))
        one_blank()
        result.append('    @{}.setter'.format(name))
        result.append('    def {}(self, value: {}):'.format(name, type))
        result.append('        self.__{} = value'.format(name))

    two_blank()
    result.append('class {}(abc.ABC):'.format(base))
    result.append('    __span: Optional[SourceSpan]')
    one_blank()
    result.append('    @abc.abstractmethod')
    result.append('    def accept(self, visitor: \'{}Visitor\'):'.format(base))
    result.append('        pass')
    one_blank()
    props('span', 'Optional[SourceSpan]')

    for name, items in enums.items():
        two_blank()
        result.append('class {}Type(Enum):'.format(name))
        for item in items:
            result.append('    {} = auto()'.format(item))

        two_blank()
        result.append('class {}:'.format(name))
        result.append('    def __init__(self, type: {}Type, span: SourceSpan) -> None:'.format(name))
        result.append('        self.__type = type')
        result.append('        self.__span = span')
        one_blank()
        result.append('    @property')
        result.append('    def type(self) -> {}Type:'.format(name))
        result.append('        return self.__type')
        prop('type', '{}Type'.format(name))
        one_blank()
        prop('span', 'Optional[SourceSpan]')

    for name, fields in types.items():
        two_blank()
        result.append('class {}({}):'.format(name, base))

        init_params = ''.join(map(lambda x: ', {}: {}'.format(x[0], x[1]), fields))
        result.append('    def __init__(self{}) -> None:'.format(init_params))

        for field in fields:
            result.append('        self.__{0} = {0}'.format(field[0]))

        one_blank()
        result.append('    def accept(self, visitor: \'ExprVisitor\'):')
        result.append('        return visitor.visit_{}(self)'.format(to_title_case(name)))

        for field in fields:
            one_blank()
            prop(field[0], field[1])

    two_blank()
    result.append('class {}Visitor:'.format(base))
    for name, _ in types.items():
        one_blank()
        result.append('    def visit_{}(self, node: {}):'.format(to_title_case(name), name))
        result.append('        pass')

    # blank line at end of file
    result.append('')
    return result


def define_ast(target: str, base: str, imports: List[str], enums: EnumDict, types: Dict[str, List[Tuple[str, str]]]):
    ast = generate_ast(base, imports, enums, types)
    with open(target, 'w') as f:
        f.write('\n'.join(ast))


def main():
    expr_base = "Expr"
    define_ast(path.join(output_dir, "expr.py"), expr_base, [
        "from typing import Any",
    ], {
        "BinaryOperator": ["MINUS", "PLUS", "MULTIPLY", "DIVIDE", "NOT_EQUAL", "EQUAL", "GREATER", "GREATER_EQUAL", "LESS", "LESS_EQUAL"],
        "UnaryOperator": ["NEGATE", "NOT"],
    }, {
        "Binary": [("left", expr_base), ("operator", "BinaryOperator"), ("right", expr_base)],
        "Grouping": [("expression", expr_base)],
        "Literal": [("value", "Any")],
        "Unary": [("operator", "UnaryOperator"), ("expression", expr_base)],
    })


if __name__ == '__main__':
    main()
