#  Copyright (c) 2022-2024 by Rocky Bernstein
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Custom Nonterminal action functions. See NonterminalActions docstring.
"""

from uncompyle6.parsers.treenode import SyntaxTree
from uncompyle6.scanners.tok import Token
from uncompyle6.semantics.consts import (
    INDENT_PER_LEVEL,
    NO_PARENTHESIS_EVER,
    NONE,
    PARENTHESIS_ALWAYS,
    PRECEDENCE,
    minint,
)
from uncompyle6.semantics.helper import find_code_node, flatten_list, print_docstring
from uncompyle6.util import better_repr, get_code_name


class NonterminalActions:
    """
    Methods here all start with n_ and the remaining portion should be a nonterminal
    name defined by some rule.

    These methods take priority over names defined in table constants.
    All of the methods should have the same signature: (self, node) and return None.

    node is the subtree of the parse tree the that nonterminal name as the root.
    """

    def __init__(self):
        # Precedence is used to determine when an expression needs
        # parenthesis surrounding it. A high value indicates no
        # parenthesis are needed.
        self.prec = 1000
        self.in_format_string = False

    def n_alias(self, node: SyntaxTree):
        if self.version <= (2, 1):
            if len(node) == 2:
                store = node[1]
                assert store == "store"
                if store[0].pattr == node[0].pattr:
                    self.write("import %s\n" % node[0].pattr)
                else:
                    self.write("import %s as %s\n" % (node[0].pattr, store[0].pattr))
                    pass
                pass
            self.prune()  # stop recursing

        store_node = node[-1][-1]
        assert store_node.kind.startswith("STORE_")
        iname = node[0].pattr  # import name
        sname = store_node.pattr  # store_name
        if iname and iname == sname or iname.startswith(sname + "."):
            self.write(iname)
        else:
            self.write(iname, " as ", sname)
        self.prune()  # stop recursing

    n_alias37 = n_alias

    def n_assign(self, node: SyntaxTree):
        # A horrible hack for Python 3.0 .. 3.2
        if (3, 0) <= self.version <= (3, 2) and len(node) == 2:
            if (
                node[0][0] == "LOAD_FAST"
                and node[0][0].pattr == "__locals__"
                and node[1][0].kind == "STORE_LOCALS"
            ):
                self.prune()
        self.default(node)

    def n_assign2(self, node: SyntaxTree):
        for n in node[-2:]:
            if n[0] == "unpack":
                n[0].kind = "unpack_w_parens"
        self.default(node)

    def n_assign3(self, node: SyntaxTree):
        for n in node[-3:]:
            if n[0] == "unpack":
                n[0].kind = "unpack_w_parens"
        self.default(node)

    def n_attribute(self, node: SyntaxTree):
        if node[0] == "LOAD_CONST" or node[0] == "expr" and node[0][0] == "LOAD_CONST":
            # FIXME: I didn't record which constants parenthesis is
            # necessary. However, I suspect that we could further
            # refine this by looking at operator precedence and
            # eval'ing the constant value (pattr) and comparing with
            # the type of the constant.
            node.kind = "attribute_w_parens"
        self.default(node)

    def n_bin_op(self, node: SyntaxTree):
        """bin_op (formerly "binary_expr") is the Python AST BinOp"""
        self.preorder(node[0])
        self.write(" ")
        self.preorder(node[-1])
        self.write(" ")
        # Try to avoid a trailing parentheses by lowering the priority a little
        self.prec -= 1
        self.preorder(node[1])
        self.prec += 1
        self.prune()

    def n_build_slice2(self, node: SyntaxTree):
        p = self.prec
        self.prec = NO_PARENTHESIS_EVER
        if not node[0].isNone():
            self.preorder(node[0])
        self.write(":")
        if not node[1].isNone():
            self.preorder(node[1])
        self.prec = p
        self.prune()  # stop recursing

    def n_build_slice3(self, node: SyntaxTree):
        p = self.prec
        self.prec = NO_PARENTHESIS_EVER
        if not node[0].isNone():
            self.preorder(node[0])
        self.write(":")
        if not node[1].isNone():
            self.preorder(node[1])
        self.write(":")
        if not node[2].isNone():
            self.preorder(node[2])
        self.prec = p
        self.prune()  # stop recursing

    def n_classdef(self, node: SyntaxTree):
        if self.version >= (3, 6):
            self.n_classdef36(node)
        elif self.version >= (3, 0):
            self.n_classdef3(node)

        # class definition ('class X(A,B,C):')
        cclass = self.currentclass

        # Pick out various needed bits of information
        # * class_name - the name of the class
        # * subclass_info - the parameters to the class  e.g.
        #      class Foo(bar, baz)
        #            ------------
        # * subclass_code - the code for the subclass body

        if node == "classdefdeco2":
            build_class = node
        else:
            build_class = node[0]
        build_list = build_class[1][0]
        if hasattr(build_class[-3][0], "attr"):
            subclass_code = build_class[-3][0].attr
            class_name = build_class[0].pattr
        elif (
            build_class[-3] == "mkfunc"
            and node == "classdefdeco2"
            and build_class[-3][0] == "load_closure"
        ):
            subclass_code = build_class[-3][1].attr
            class_name = build_class[-3][0][0].pattr
        elif hasattr(node[0][0], "pattr"):
            subclass_code = build_class[-3][1].attr
            class_name = node[0][0].pattr
        else:
            raise RuntimeError("Internal Error n_classdef: cannot find class name")

        if node == "classdefdeco2":
            self.write("\n")
        else:
            self.write("\n\n")

        self.currentclass = str(class_name)
        self.write(self.indent, "class ", self.currentclass)

        self.print_super_classes(build_list)
        self.println(":")

        # class body
        self.indent_more()
        self.build_class(subclass_code)
        self.indent_less()

        self.currentclass = cclass
        if len(self.param_stack) > 1:
            self.write("\n\n")
        else:
            self.write("\n\n\n")

        self.prune()

    n_classdefdeco2 = n_classdef

    def n_const_list(self, node: SyntaxTree):
        """
        prettyprint a constant dict, list, set or tuple.
        """
        p = self.prec

        lastnodetype = node[2].kind
        flat_elems = node[1]
        is_dict = lastnodetype.endswith("DICT")

        if lastnodetype.endswith("LIST"):
            self.write("[")
            endchar = "]"
        elif lastnodetype.endswith("SET") or is_dict:
            self.write("{")
            endchar = "}"
        else:
            # from trepan.api import debug; debug()
            raise TypeError(
                (
                    "Internal Error: n_const_list expects dict, list set, or set; got "
                    f"{lastnodetype}"
                )
            )

        self.indent_more(INDENT_PER_LEVEL)
        sep = ""
        line_len = len(self.indent)
        if is_dict:
            keys = flat_elems[-1].attr
            assert isinstance(keys, tuple)
            assert len(keys) == len(flat_elems) - 1
            for i, elem in enumerate(flat_elems[:-1]):
                assert elem.kind == "ADD_VALUE"
                if elem.optype in ("local", "name"):
                    value = elem.attr
                elif elem.optype == "const" and not isinstance(elem.attr, str):
                    value = elem.attr
                else:
                    try:
                        value = "%r" % elem.pattr
                    except Exception:
                        value = elem.pattr
                if elem.linestart is not None:
                    if elem.linestart != self.line_number:
                        next_indent = self.indent + INDENT_PER_LEVEL[:-1]
                        line_len = len(next_indent)
                        sep += "\n" + next_indent
                        self.line_number = elem.linestart
                    else:
                        if sep != "":
                            sep += ", "
                elif line_len > 80:
                    next_indent = self.indent + INDENT_PER_LEVEL[:-1]
                    line_len = len(next_indent)
                    sep += "\n" + next_indent

                sep_key_value = f"{sep}{repr(keys[i])}: {value}"
                line_len += len(sep_key_value)
                self.write(sep_key_value)
                sep = ", "
        else:
            for elem in flat_elems:
                if elem == "add_value":
                    elem = elem[0]

                if elem == "ADD_VALUE":
                    if elem.optype in ("local", "name"):
                        value = elem.attr
                    elif elem.optype == "const" and not isinstance(elem.attr, str):
                        value = elem.attr
                    else:
                        value = "%s" % repr(elem.attr)
                else:
                    assert elem.kind == "ADD_VALUE_VAR"
                    value = "%s" % elem.pattr

                if elem.linestart is not None:
                    if elem.linestart != self.line_number:
                        next_indent = self.indent + INDENT_PER_LEVEL[:-1]
                        line_len += len(next_indent)
                        sep += "\n" + next_indent
                        self.line_number = elem.linestart
                    else:
                        if sep != "":
                            sep += " "
                        line_len += len(sep)
                elif line_len > 80:
                    next_indent = self.indent + INDENT_PER_LEVEL[:-1]
                    line_len = len(next_indent)
                    sep += "\n" + next_indent

                line_len += len(sep) + len(str(value)) + 1
                self.write(sep, value)
                sep = ", "
        self.write(endchar)
        self.indent_less(INDENT_PER_LEVEL)

        self.prec = p
        self.prune()
        return

    def n_delete_subscript(self, node: SyntaxTree):
        if node[-2][0] == "build_list" and node[-2][0][-1].kind.startswith(
            "BUILD_TUPLE"
        ):
            if node[-2][0][-1] != "BUILD_TUPLE_0":
                node[-2][0].kind = "build_tuple2"
        self.default(node)

    n_store_subscript = n_subscript = n_delete_subscript

    def n_dict(self, node: SyntaxTree):
        """
        Prettyprint a dict.
        'dict' is something like k = {'a': 1, 'b': 42}"
        We will use source-code line breaks to guide us when to break.
        """
        if len(node) == 1 and node[0] == "const_list":
            self.preorder(node[0])
            self.prune()
            return

        p = self.prec
        self.prec = PRECEDENCE["dict"]

        self.indent_more(INDENT_PER_LEVEL)
        sep = INDENT_PER_LEVEL[:-1]
        if node[0] != "dict_entry":
            self.write("{")
        line_number = self.line_number

        if self.version >= (3, 0) and not self.is_pypy:
            if node[0].kind.startswith("kvlist"):
                # Python 3.5+ style key/value list in dict
                kv_node = node[0]
                ll = list(kv_node)
                length = len(ll)
                if kv_node[-1].kind.startswith("BUILD_MAP"):
                    length -= 1
                i = 0

                # Respect line breaks from source
                while i < length:
                    self.write(sep)
                    name = self.traverse(ll[i], indent="")
                    if i > 0:
                        line_number = self.indent_if_source_nl(
                            line_number, self.indent + INDENT_PER_LEVEL[:-1]
                        )
                    line_number = self.line_number
                    self.write(name, ": ")
                    value = self.traverse(
                        ll[i + 1], indent=self.indent + (len(name) + 2) * " "
                    )
                    self.write(value)
                    sep = ", "
                    if line_number != self.line_number:
                        sep += "\n" + self.indent + INDENT_PER_LEVEL[:-1]
                        line_number = self.line_number
                    i += 2
                    pass
                pass
            elif len(node) > 1 and node[1].kind.startswith("kvlist"):
                # Python 3.0..3.4 style key/value list in dict
                kv_node = node[1]
                ll = list(kv_node)
                if len(ll) > 0 and ll[0].kind == "kv3":
                    # Python 3.2 does this
                    kv_node = node[1][0]
                    ll = list(kv_node)
                i = 0
                while i < len(ll):
                    self.write(sep)
                    name = self.traverse(ll[i + 1], indent="")
                    if i > 0:
                        line_number = self.indent_if_source_nl(
                            line_number, self.indent + INDENT_PER_LEVEL[:-1]
                        )
                        pass
                    line_number = self.line_number
                    self.write(name, ": ")
                    value = self.traverse(
                        ll[i], indent=self.indent + (len(name) + 2) * " "
                    )
                    self.write(value)
                    sep = ", "
                    if line_number != self.line_number:
                        sep += "\n" + self.indent + INDENT_PER_LEVEL[:-1]
                        line_number = self.line_number
                    else:
                        sep += " "
                    i += 3
                    pass
                pass
            elif node[-1].kind.startswith("BUILD_CONST_KEY_MAP"):
                # Python 3.6+ style const map
                keys = node[-2].pattr
                values = node[:-2]
                # FIXME: Line numbers?
                for key, value in zip(keys, values):
                    self.write(sep)
                    self.write(repr(key))
                    line_number = self.line_number
                    self.write(":")
                    self.write(self.traverse(value[0]))
                    sep = ", "
                    if line_number != self.line_number:
                        sep += "\n" + self.indent + INDENT_PER_LEVEL[:-1]
                        line_number = self.line_number
                    else:
                        sep += " "
                        pass
                    pass
                if sep.startswith(",\n"):
                    self.write(sep[1:])
                pass
            elif node[0].kind.startswith("dict_entry"):
                assert self.version >= (3, 5)
                template = ("%C", (0, len(node[0]), ", **"))
                self.template_engine(template, node[0])
                sep = ""
            elif node[-1].kind.startswith("BUILD_MAP_UNPACK") or node[
                -1
            ].kind.startswith("dict_entry"):
                assert self.version >= (3, 5)
                # FIXME: I think we can intermingle dict_comp's with other
                # dictionary kinds of things. The most common though is
                # a sequence of dict_comp's
                kwargs = node[-1].attr
                template = ("**%C", (0, kwargs, ", **"))
                self.template_engine(template, node)
                sep = ""
            if node[0].kind == "COLLECTION_START":
                key_value_pairs = node[1]
                for key_value_pair in key_value_pairs:
                    key, value = key_value_pair
                    if key.linestart is not None:
                        line_number = key.linestart
                    if line_number != self.line_number:
                        sep += "\n" + self.indent + INDENT_PER_LEVEL[:-1]
                        self.line_number = line_number
                    self.write(sep)
                    self.write(key.pattr)
                    self.write(": ")
                    if value.linestart is not None:
                        line_number = value.linestart
                    if line_number != self.line_number:
                        sep += "\n" + self.indent + INDENT_PER_LEVEL[:-1]
                        self.line_number = line_number
                    else:
                        sep += " "
                        pass
                    self.write(value.pattr)
                    sep = ", "
                    pass
                if sep.startswith(",\n"):
                    self.write(sep[1:])
                pass

            pass
        else:
            # Python 2 style kvlist. Find beginning of kvlist.
            indent = self.indent + "  "
            line_number = self.line_number
            if node[0].kind.startswith("BUILD_MAP"):
                if len(node) > 1 and node[1].kind in ("kvlist", "kvlist_n"):
                    kv_node = node[1]
                else:
                    kv_node = node[1:]
                self.kv_map(kv_node, sep, line_number, indent)
            else:
                sep = ""
                opname = node[-1].kind
                if self.is_pypy and self.version >= (3, 5):
                    if opname.startswith("BUILD_CONST_KEY_MAP"):
                        keys = node[-2].attr
                        # FIXME: DRY this and the above
                        for i in range(len(keys)):
                            key = keys[i]
                            value = self.traverse(node[i], indent="")
                            self.write(sep, key, ": ", value)
                            sep = ", "
                            if line_number != self.line_number:
                                sep += "\n" + self.indent + "  "
                                line_number = self.line_number
                                pass
                            pass
                        pass
                    else:
                        if opname.startswith("kvlist"):
                            list_node = node[0]
                        else:
                            list_node = node

                        assert list_node[-1].kind.startswith("BUILD_MAP")
                        for i in range(0, len(list_node) - 1, 2):
                            key = self.traverse(list_node[i], indent="")
                            value = self.traverse(list_node[i + 1], indent="")
                            self.write(sep, key, ": ", value)
                            sep = ", "
                            if line_number != self.line_number:
                                sep += "\n" + self.indent + "  "
                                line_number = self.line_number
                                pass
                            pass
                        pass
                elif opname.startswith("kvlist"):
                    kv_node = node[-1]
                    self.kv_map(node[-1], sep, line_number, indent)

                pass
            pass
        if sep.startswith(",\n"):
            self.write(sep[1:])
        if node[0] != "dict_entry":
            self.write("}")
        self.indent_less(INDENT_PER_LEVEL)
        self.prec = p
        self.prune()

    def n_docstring(self, node):
        indent = self.indent
        doc_node = node[0]
        if doc_node.attr:
            docstring = doc_node.attr
            if not isinstance(docstring, str):
                # FIXME: we have mistakenly tagged something as a doc
                # string in transform when it isn't one.
                # The rule in n_mkfunc is pretty flaky.
                self.prune()
                return
        else:
            docstring = node[0].pattr

        print_docstring(self, indent, docstring)
        self.prune()

    def n_elifelsestmtr(self, node: SyntaxTree):
        if node[2] == "COME_FROM":
            return_stmts_node = node[3]
            node.kind = "elifelsestmtr2"
        else:
            return_stmts_node = node[2]

        if len(return_stmts_node) != 2:
            self.default(node)

        for n in return_stmts_node[0]:
            if not (n[0] == "ifstmt" and n[0][1][0] == "return_if_stmts"):
                self.default(node)
                return

        self.write(self.indent, "elif ")
        self.preorder(node[0])
        self.println(":")
        self.indent_more()
        self.preorder(node[1])
        self.indent_less()

        for n in return_stmts_node[0]:
            n[0].kind = "elifstmt"
            self.preorder(n)
        self.println(self.indent, "else:")
        self.indent_more()
        self.preorder(return_stmts_node[1])
        self.indent_less()
        self.prune()

    def n_except_cond2(self, node: SyntaxTree):
        if node[-1] == "come_from_opt":
            unpack_node = -3
        else:
            unpack_node = -2

        if node[unpack_node][0] == "unpack":
            node[unpack_node][0].kind = "unpack_w_parens"
        self.default(node)

    # Note: this node is only in Python 2.x
    # FIXME: figure out how to get this into customization
    # put so that we can get access via super from
    # the fragments routine.
    def n_exec_stmt(self, node: SyntaxTree):
        """
        exec_stmt ::= expr exprlist DUP_TOP EXEC_STMT
        exec_stmt ::= expr exprlist EXEC_STMT
        """
        self.write(self.indent, "exec ")
        self.preorder(node[0])
        if not node[1][0].isNone():
            sep = " in "
            for subnode in node[1]:
                self.write(sep)
                sep = ", "
                self.preorder(subnode)
        self.println()
        self.prune()  # stop recursing

    def n_expr(self, node):
        first_child = node[0]
        if first_child == "_lambda_body" and self.in_format_string:
            p = -2
        else:
            p = self.prec

        if first_child.kind.startswith("bin_op"):
            n = node[0][-1][0]
        else:
            n = node[0]

        # if (hasattr(n, 'linestart') and n.linestart and
        #     hasattr(self, 'current_line_number')):
        #     self.source_linemap[self.current_line_number] = n.linestart

        if n.kind != "expr":
            self.prec = PRECEDENCE.get(n.kind, PARENTHESIS_ALWAYS)

        if n == "LOAD_CONST" and repr(n.pattr)[0] == "-":
            self.prec = 6

        # print("XXX", n.kind, p, "<", self.prec)
        # print(self.f.getvalue())

        if p < self.prec:
            # print(f"PREC {p}, {node[0].kind}")
            self.write("(")
            self.preorder(node[0])
            self.write(")")
        else:
            self.preorder(node[0])
        self.prec = p
        self.prune()

    n_return_expr_or_cond = n_expr

    def n_generator_exp(self, node):
        self.write("(")
        iter_index = 3
        if self.version > (3, 2):
            if self.version >= (3, 3):
                if node[0].kind in (
                    "load_closure",
                    "load_genexpr",
                ) and self.version >= (3, 8):
                    code_index = -6
                    is_lambda = self.is_lambda
                    if node[0].kind == "load_genexpr":
                        self.is_lambda = False
                    self.closure_walk(node, collection_index=4)
                    self.is_lambda = is_lambda
                else:
                    # Python 3.7+ adds optional "come_froms" at node[0] so count from
                    # the end.
                    if node == "generator_exp_async" and self.version[:2] == (3, 6):
                        code_index = 0
                    else:
                        code_index = -6
                    iter_index = (
                        4
                        if self.version < (3, 8) and not isinstance(node[4], Token)
                        else 3
                    )
                    self.comprehension_walk(
                        node, iter_index=iter_index, code_index=code_index
                    )
                    pass
                pass
        else:
            code_index = -5
            self.comprehension_walk(node, iter_index=iter_index, code_index=code_index)
        self.write(")")
        self.prune()

    n_genexpr_func = n_generator_exp_async = n_generator_exp

    def n_ifelsestmtr(self, node):
        if node[2] == "COME_FROM":
            return_stmts_node = node[3]
            node.kind = "ifelsestmtr2"
        else:
            return_stmts_node = node[2]
        if len(return_stmts_node) != 2:
            self.default(node)

        if not (
            return_stmts_node[0][0][0] == "ifstmt"
            and return_stmts_node[0][0][0][1][0] == "return_if_stmts"
        ) and not (
            return_stmts_node[0][-1][0] == "ifstmt"
            and return_stmts_node[0][-1][0][1][0] == "return_if_stmts"
        ):
            self.default(node)
            return

        self.write(self.indent, "if ")
        self.preorder(node[0])
        self.println(":")
        self.indent_more()
        self.preorder(node[1])
        self.indent_less()

        if_ret_at_end = False
        if len(return_stmts_node[0]) >= 3:
            if (
                return_stmts_node[0][-1][0] == "ifstmt"
                and return_stmts_node[0][-1][0][1][0] == "return_if_stmts"
            ):
                if_ret_at_end = True

        past_else = False
        prev_stmt_is_if_ret = True
        for n in return_stmts_node[0]:
            if n[0] == "ifstmt" and n[0][1][0] == "return_if_stmts":
                if prev_stmt_is_if_ret:
                    n[0].kind = "elifstmt"
                prev_stmt_is_if_ret = True
            else:
                prev_stmt_is_if_ret = False
                if not past_else and not if_ret_at_end:
                    self.println(self.indent, "else:")
                    self.indent_more()
                    past_else = True
            self.preorder(n)
        if not past_else or if_ret_at_end:
            self.println(self.indent, "else:")
            self.indent_more()
        self.preorder(return_stmts_node[1])
        self.indent_less()
        self.prune()

    n_ifelsestmtr2 = n_ifelsestmtr

    def n_import_from(self, node):
        relative_path_index = 0
        if self.version >= (2, 5):
            if node[relative_path_index].pattr > 0:
                node[2].pattr = ("." * node[relative_path_index].attr) + node[2].pattr
            if self.version > (2, 7):
                if isinstance(node[1].pattr, tuple):
                    imports = node[1].pattr
                    for pattr in imports:
                        node[1].pattr = pattr
                        self.default(node)
                    return
                pass
        self.default(node)

    n_import_from_star = n_import_from

    def n_lambda_body(self, node):
        self.make_function(node, is_lambda=True, code_node=node[-2])
        self.prune()  # stop recursing

    def n_list(self, node: SyntaxTree):
        """
        prettyprint a dict, list, set or tuple.
        """
        if len(node) == 1 and node[0] == "const_list":
            self.preorder(node[0])
            self.prune()
            return

        p = self.prec
        self.prec = PRECEDENCE["yield"] - 1
        lastnode = node[-1]
        node = node[:-1]
        lastnodetype = lastnode.kind

        # If this build list is inside a CALL_FUNCTION_VAR,
        # then the first * has already been printed.
        # Until I have a better way to check for CALL_FUNCTION_VAR,
        # will assume that if the text ends in *.
        last_was_star = self.f.getvalue().endswith("*")

        if lastnodetype.endswith("UNPACK"):
            # FIXME: need to handle range of BUILD_LIST_UNPACK
            have_star = True
            # endchar = ''
        else:
            have_star = False

        if lastnodetype.startswith("BUILD_LIST"):
            self.write("[")
            endchar = "]"

        elif lastnodetype.startswith("BUILD_MAP_UNPACK"):
            self.write("{*")
            endchar = "}"

        elif lastnodetype.startswith("BUILD_SET"):
            self.write("{")
            endchar = "}"

        elif lastnodetype.startswith("BUILD_TUPLE") or node == "tuple":
            # Tuples can appear places that can NOT
            # have parenthesis around them, like array
            # subscripts. We check for that by seeing
            # if a tuple item is some sort of slice.
            no_parens = False
            for n in node:
                if n == "expr" and n[0].kind.startswith("build_slice"):
                    no_parens = True
                    break
                pass
            if no_parens:
                endchar = ""
            else:
                self.write("(")
                endchar = ")"
                pass

        elif lastnodetype.startswith("ROT_TWO"):
            self.write("(")
            endchar = ")"

        else:
            raise TypeError(
                "Internal Error: n_build_list expects list, tuple, set, or unpack"
            )

        flat_elems = flatten_list(node)

        self.indent_more(INDENT_PER_LEVEL)
        sep = ""
        for elem in flat_elems:
            if elem in ("ROT_THREE", "EXTENDED_ARG"):
                continue
            assert elem == "expr"
            line_number = self.line_number
            value = self.traverse(elem)
            if line_number != self.line_number:
                sep += "\n" + self.indent + INDENT_PER_LEVEL[:-1]
            else:
                if sep != "":
                    sep += " "
            if not last_was_star:
                if have_star:
                    sep += "*"
                    pass
                pass
            else:
                last_was_star = False
            self.write(sep, value)
            sep = ","
        if lastnode.attr == 1 and lastnodetype.startswith("BUILD_TUPLE"):
            self.write(",")
        self.write(endchar)
        self.indent_less(INDENT_PER_LEVEL)

        self.prec = p
        self.prune()
        return

    n_set = n_build_set = n_tuple = n_list

    def n_list_comp(self, node):
        self.write("[")
        if node[0].kind == "load_closure":
            assert self.version >= (3, 0)
            self.listcomp_closure3(node)
        else:
            if node == "listcomp_async":
                list_iter_index = 5
            else:
                list_iter_index = 1
            self.comprehension_walk_newer(node, list_iter_index, 0)
        self.write("]")
        self.prune()

    def n_list_comp_pypy27(self, node):
        """List comprehensions in PYPY."""
        p = self.prec
        self.prec = 27
        if node[-1].kind == "list_iter":
            n = node[-1]
        elif self.is_pypy and node[-1] == "JUMP_BACK":
            n = node[-2]
        list_expr = node[1]

        if len(node) >= 3:
            store = node[3]
        elif self.is_pypy and n[0] == "list_for":
            store = n[0][2]

        assert n == "list_iter"
        assert store == "store"

        # Find the list comprehension body. It is the inner-most
        # node.
        # FIXME: DRY with other use
        while n == "list_iter":
            n = n[0]  # iterate one nesting deeper
            if n == "list_for":
                n = n[3]
            elif n == "list_if":
                n = n[2]
            elif n == "list_if_not":
                n = n[2]
        assert n == "lc_body"
        self.write("[ ")

        expr = n[0]
        if self.is_pypy and node[-1] == "JUMP_BACK":
            list_iter = node[-2]
        else:
            list_iter = node[-1]

        assert expr == "expr"
        assert list_iter == "list_iter"

        # FIXME: use source line numbers for directing line breaks

        self.preorder(expr)
        self.preorder(list_expr)
        self.write(" ]")
        self.prec = p
        self.prune()  # stop recursing

    def n_mkfunc(self, node):
        code_node = find_code_node(node, -2)
        code = code_node.attr
        self.write(get_code_name(code))
        self.indent_more()

        self.make_function(node, is_lambda=False, code_node=code_node)

        if len(self.param_stack) > 1:
            self.write("\n\n")
        else:
            self.write("\n\n\n")
        self.indent_less()
        self.prune()  # stop recursing

    def n_return(self, node):
        if self.params["is_lambda"]:
            self.preorder(node[0])
            self.prune()
        else:
            # One reason we worry over whether we use "return None" or "return"
            # is that inside a generator, "return None" is illegal.
            # Thank you, Python!
            if self.return_none or not self.is_return_none(node):
                self.default(node)
            else:
                self.template_engine(("%|return\n",), node)

            self.prune()  # stop recursing

    def n_return_expr(self, node):
        if len(node) == 1 and node[0] == "expr":
            # If expr is yield we want parens.
            self.prec = PRECEDENCE["yield"] - 1
            self.n_expr(node[0])
        else:
            self.n_expr(node)

    # Python 3.x can have dead code as a result of its optimization?
    # So we'll add a # at the end of the return lambda so the rest is ignored
    def n_return_expr_lambda(self, node):
        if 1 <= len(node) <= 2:
            self.preorder(node[0])
            self.write(" # Avoid dead code: ")
            self.prune()
        else:
            # We can't comment out like above because there may be a trailing ')'
            # that needs to be written
            assert len(node) == 3 and node[2] in (
                "RETURN_VALUE_LAMBDA",
                "LAMBDA_MARKER",
            )
            self.preorder(node[0])
            self.prune()

    def n_return_if_stmt(self, node):
        if self.params["is_lambda"]:
            self.write(" return ")
            self.preorder(node[0])
            self.prune()
        else:
            self.write(self.indent, "return")
            if self.return_none or not self.is_return_none(node):
                self.write(" ")
                self.preorder(node[0])
            self.println()
            self.prune()  # stop recursing

    def n_set_comp(self, node):
        self.write("{")
        if node[0] in ["LOAD_SETCOMP", "LOAD_DICTCOMP"]:
            if self.version == (3, 0):
                if len(node) >= 6:
                    iter_index = 6
                else:
                    assert node[1].kind.startswith("MAKE_FUNCTION")
                    iter_index = 2
                    pass
            else:
                iter_index = 1
            self.comprehension_walk_newer(node, iter_index=iter_index, code_index=0)
        elif node[0].kind == "load_closure" and self.version >= (3, 0):
            self.closure_walk(node, collection_index=4)
        else:
            self.comprehension_walk(node, iter_index=4)
        self.write("}")
        self.prune()

    n_dict_comp = n_set_comp

    # In the old days this node would never get called because
    # it was embedded inside some sort of comprehension
    # Nowadays, we allow starting any code object, not just
    # a top-level module. In doing so we can
    # now encounter this outside of the embedding of
    # a comprehension.
    def n_set_comp_async(self, node):
        self.write("{")
        if node[0] in ["BUILD_SET_0", "BUILD_MAP_0"]:
            self.comprehension_walk_newer(node[1], 3, 0, collection_node=node[1])
        if node[0] in ["LOAD_SETCOMP", "LOAD_DICTCOMP"]:
            get_aiter = node[3]
            assert get_aiter == "get_aiter", node.kind
            self.comprehension_walk_newer(node, 1, 0, collection_node=get_aiter[0])
        self.write("}")
        self.prune()

    n_dict_comp_async = n_set_comp_async

    def n_str(self, node):
        self.write(node[0].pattr)
        self.prune()

    def n_store(self, node):
        expr = node[0]
        if expr == "expr" and expr[0] == "LOAD_CONST" and node[1] == "STORE_ATTR":
            # FIXME: I didn't record which constants parenthesis is
            # necessary. However, I suspect that we could further
            # refine this by looking at operator precedence and
            # eval'ing the constant value (pattr) and comparing with
            # the type of the constant.
            node.kind = "store_w_parens"
        self.default(node)

    def n_unpack(self, node):
        if node[0].kind.startswith("UNPACK_EX"):
            # Python 3+
            before_count, after_count = node[0].attr
            for i in range(before_count + 1):
                self.preorder(node[i])
                if i != 0:
                    self.write(", ")
            self.write("*")
            for i in range(1, after_count + 2):
                self.preorder(node[before_count + i])
                if i != after_count + 1:
                    self.write(", ")
            self.prune()
            return

        if node[0] == "UNPACK_SEQUENCE_0":
            self.write("[]")
            self.prune()
            return

        for n in node[1:]:
            if n[0].kind == "unpack":
                n[0].kind = "unpack_w_parens"

        # In Python 2.4, unpack is used in (a, b, c) of:
        #   except RuntimeError, (a, b, c):
        if self.version < (2, 7):
            node.kind = "unpack_w_parens"
        self.default(node)

    n_unpack_w_parens = n_unpack

    def n_yield(self, node):
        if node != SyntaxTree("yield", [NONE, Token("YIELD_VALUE")]):
            self.template_engine(("yield %c", 0), node)
        elif self.version <= (2, 4):
            # Early versions of Python don't allow a plain "yield"
            self.write("yield None")
        else:
            self.write("yield")

        self.prune()  # stop recursing

    def n_LOAD_CONST(self, node):
        attr = node.attr
        data = node.pattr
        datatype = type(data)
        if isinstance(data, float):
            self.write(better_repr(data, self.version))
        elif isinstance(data, complex):
            self.write(better_repr(data, self.version))
        elif isinstance(datatype, int) and data == minint:
            # convert to hex, since decimal representation
            # would result in 'LOAD_CONST; UNARY_NEGATIVE'
            # change:hG/2002-02-07: this was done for all negative integers
            # todo: check whether this is necessary in Python 2.1
            self.write(hex(data))
        elif datatype is type(Ellipsis):
            self.write("...")
        elif attr is None:
            # LOAD_CONST 'None' only occurs, when None is
            # implicit e.g. in 'return' w/o params
            # pass
            self.write("None")
        elif isinstance(data, tuple):
            self.pp_tuple(data)
        elif isinstance(attr, bool):
            self.write(repr(attr))
        elif self.FUTURE_UNICODE_LITERALS:
            # The FUTURE_UNICODE_LITERALS compiler flag
            # in 2.6 on change the way
            # strings are interpreted:
            #    u'xxx' -> 'xxx'
            #    xxx'   -> b'xxx'
            if isinstance(data, str):
                self.write("b" + repr(data))
            else:
                self.write(repr(data))
        else:
            self.write(repr(data))
        # LOAD_CONST is a terminal, so stop processing/recursing early
        self.prune()
