# coding: utf-8
"""
AST to code converter base class
"""
import ast


class AstToCode(object):
    """ converts an AST to code """

    property_accessor_op = '.'
    """ Default property accessor operator for OOP """

    constant_accessor_op = '.'
    """ Default constant accessor operator for OOP """

    instance_var = 'self'
    """ Name of the implicit instance variable (if any) """

    add_op = '+'
    """ Addition operator (usually a "+") """

    unary_add_op = '+'
    """ Unary addition operator as in "+x" (usually a "+") """

    sub_op = '-'
    """ Subtraction operator (usually a "-") """

    unary_sub_op = '-'
    """ Unary subtraction operator as in "-x" (usually a "-") """

    mult_op = '*'
    """ Multiplication operator (usually a "*") """

    div_op = '/'
    """ Division operator (usually a "/") """

    list_subscription_parens = ('[', ']')
    """ Parenthesis to access a list/array member (subscription) """

    callable_exec_parens = ('(', ')')
    """ Parenthesis that denote the execution of a callable, as in "foo()" """

    call_args_delim = ', '
    """ Delimiter for function/method/callable arguments, usually a ", " """

    list_members_delim = ', '
    """ Delimiter for list/array members, usually a ", " """

    list_const_parens = ('[', ']')
    """ List/array constructor parenthesis, usually [], note that in case of PHP it's array() """

    bool_and = '&&'
    """ Boolean AND operator """

    bool_or = '||'
    """ Boolean OR operator """

    cmp_lt_op = '<'
    cmp_lte_op = '<='
    cmp_gt_op = '>'
    cmp_gte_op = '>='
    cmp_eq_op = '=='
    cmp_neq_op = '!='
    """ Comparison operators """

    bd_class = 'BigDecimal'
    """ BigDecimal class or its alias """

    bd_class_constructor = 'new BigDecimal'
    """ BigDecimal constructor statement, as in "new BigDecimal()" """

    allow_constants = True
    """ Allow usage of class constants (orf final static fields) """

    def __init__(self, parser, class_name=None):
        self.parser = parser
        self.class_name = class_name if class_name else self.parser.internal_name

    @property
    def inst_prefix(self):
        return '{}{}'.format(
            self.instance_var,
            self.property_accessor_op
        ) if self.instance_var else ''


    def _conv_attribute(self, node):
        return (
            self.to_code(node.value) +
            [self.property_accessor_op, node.attr]
        )

    def _conv_binop(self, node):
        return (
            self.to_code(node.left) +
            self.to_code(node.op) +
            self.to_code(node.right)
        )

    def _conv_name(self, node):
        if node.id == 'BigDecimal':
            return [self.bd_class]
        elif node.id == 'BigDecimalConstructor':
            return [self.bd_class_constructor]
        if self.allow_constants and node.id in self.parser.constant_names:
            return ['{}{}{}'.format(
                self.class_name,
                self.constant_accessor_op,
                node.id
            )]
        return ['{}{}'.format(self.inst_prefix, node.id)]

    def _conv_number(self, node):
        return [str(node.n)]

    def _conv_bool_op(self, node):
        vals = []
        op = self.bool_and
        if isinstance(node.op, ast.Or):
            op = self.bool_or
        op = ' {} '.format(op)
        for (idx, val) in enumerate(node.values):
            vals += self.to_code(val)
            if idx != len(node.values) - 1:
                vals.append(op)
        return vals

    def _conv_unary_op(self, node):
        op = self.unary_sub_op if isinstance(node.op, ast.USub) else self.unary_add_op
        return [op] + self.to_code(node.operand)

    def _conv_list_subscript(self, node):
        try:
            idx = node.slice.value
        except AttributeError:
            # python3.9 deprecated the ast.Index class for slices
            idx = node.slice
        return (
            self.to_code(node.value) +
            [self.list_subscription_parens[0]] +
            self.to_code(idx) +
            [self.list_subscription_parens[1]]
        )

    def _conv_call(self, node):
        args = []
        caller = self.to_code(node.func)
        for (idx, arg) in enumerate(node.args):
            args += self.to_code(arg)
            if idx != len(node.args) - 1:
                args.append(self.call_args_delim)
        return (
            caller +
            [self.callable_exec_parens[0]] +
            args +
            [self.callable_exec_parens[1]]
        )

    def _conv_comp(self, node):
        return (
            self.to_code(node.left) +
            self.to_code(node.ops[0]) +
            self.to_code(node.comparators[0])
        )

    def _conv_list(self, node):
        ret = [self.list_const_parens[0]]
        for (idx, elt) in enumerate(node.elts):
            ret += self.to_code(elt)
            if idx != len(node.elts) - 1:
                ret.append(self.list_members_delim)
        ret.append(self.list_const_parens[1])
        return ret

    def _conv_cmp_lt(self, node):
        return [' {} '.format(self.cmp_lt_op)]

    def _conv_cmp_lte(self, node):
        return [' {} '.format(self.cmp_lte_op)]

    def _conv_cmp_eq(self, node):
        return [' {} '.format(self.cmp_eq_op)]

    def _conv_cmp_gt(self, node):
        return [' {} '.format(self.cmp_gt_op)]

    def _conv_cmp_gte(self, node):
        return [' {} '.format(self.cmp_gte_op)]

    def _conv_cmp_neq(self, node):
        return [' {} '.format(self.cmp_neq_op)]

    def _conv_add_op(self, node):
        return [' {} '.format(self.add_op)]

    def _conv_sub_op(self, node):
        return [' {} '.format(self.sub_op)]

    def _conv_mult_op(self, node):
        return [' {} '.format(self.mult_op)]

    def _conv_div_op(self, node):
        return [' {} '.format(self.div_op)]

    def to_code(self, node):
        """ convert an AST node to its specific language representation """
        if isinstance(node, ast.Attribute):
            return self._conv_attribute(node)

        if isinstance(node, ast.BinOp):
            return self._conv_binop(node)

        if isinstance(node, ast.Name):
            return self._conv_name(node)

        if isinstance(node, ast.Num):
            return self._conv_number(node)

        if isinstance(node, ast.BoolOp):
            return self._conv_bool_op(node)

        if isinstance(node, ast.UnaryOp):
            return self._conv_unary_op(node)

        if isinstance(node, ast.Subscript):
            return self._conv_list_subscript(node)

        if isinstance(node, ast.Call):
            return self._conv_call(node)

        if isinstance(node, ast.List):
            return self._conv_list(node)

        if isinstance(node, ast.Compare):
            return self._conv_comp(node)

        if isinstance(node, ast.Lt):
            return self._conv_cmp_lt(node)

        if isinstance(node, ast.LtE):
            return self._conv_cmp_lte(node)

        if isinstance(node, ast.Eq):
            return self._conv_cmp_eq(node)

        if isinstance(node, ast.Gt):
            return self._conv_cmp_gt(node)

        if isinstance(node, ast.GtE):
            return self._conv_cmp_gte(node)

        if isinstance(node, ast.NotEq):
            return self._conv_cmp_neq(node)

        if isinstance(node, ast.Add):
            return self._conv_add_op(node)

        if isinstance(node, ast.Sub):
            return self._conv_sub_op(node)

        if isinstance(node, ast.Mult):
            return self._conv_mult_op(node)

        if isinstance(node, ast.Div):
            return self._conv_div_op(node)

        raise ValueError(u'Unknown AST element: {}'.format(node))
