#!/usr/bin/env python3
#
# SPDX-FileCopyrightText: 2017-2026 Sébastien Helleu <flashcode@flashtux.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# 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 <https://www.gnu.org/licenses/>.
#

"""Unparse AST tree to generate script.

All languages supported by WeeChat can be used: Python, Perl, Ruby, etc.
"""

# ruff: noqa: B905,T201,UP006,UP007,UP035

import argparse
import ast
import inspect
import io
import os
import select
import sys
from typing import Any, List, Union

sys.dont_write_bytecode = True


class Unparse:
    """Unparse AST to generate script code."""


class UnparsePython(Unparse):
    """Unparse AST to generate Python script code.

    This class is inspired from _Unparser class in cpython:
    https://github.com/python/cpython/blob/main/Lib/ast.py

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def __init__(self, output: io.TextIOBase = sys.stdout) -> None:
        """Initialize Python parser."""
        self.output = output
        self.indent_string = " " * 4
        self._indent_level = 0
        self._prefix = []  # not used in Python, only in other languages
        self.binop = {
            "Add": "+",
            "Sub": "-",
            "Mult": "*",
            "MatMult": "@",
            "Div": "/",
            "Mod": "%",
            "LShift": "<<",
            "RShift": ">>",
            "BitOr": "|",
            "BitXor": "^",
            "BitAnd": "&",
            "FloorDiv": "//",
            "Pow": "**",
        }
        self.unaryop = {
            "Invert": "~",
            "Not": "not ",
            "UAdd": "+",
            "USub": "-",
        }
        self.cmpop = {
            "Eq": "==",
            "NotEq": "!=",
            "Lt": "<",
            "LtE": "<=",
            "Gt": ">",
            "GtE": ">=",
            "Is": "is",
            "IsNot": "is not",
            "In": "in",
            "NotIn": "not in",
        }

    def fill(self, string: str = "") -> None:
        """Add a new line and an indented string."""
        self.add(f"\n{self.indent_string * self._indent_level}{string}")

    def indent(self) -> None:
        """Indent code."""
        self._indent_level += 1

    def unindent(self) -> None:
        """Unindent code."""
        self._indent_level -= 1

    def prefix(self, prefix: Union[str, None]) -> None:
        """Add or remove a prefix from list."""
        if prefix:
            self._prefix.append(prefix)
        else:
            self._prefix.pop()

    def add(self, *args) -> None:  # noqa: ANN002
        """Add string/node(s) to the output file."""
        for arg in args:
            if callable(arg):
                arg()
            elif isinstance(arg, tuple):
                arg[0](*arg[1:])
            elif isinstance(arg, list):
                for item in arg:
                    self.add(item)
            elif isinstance(arg, ast.AST):
                method = getattr(self, f"_ast_{arg.__class__.__name__.lower()}", None)
                if method is None:
                    raise NotImplementedError(arg)
                method(arg)
            elif isinstance(arg, str):
                self.output.write(arg)

    @staticmethod
    def make_list(values: List[Any], sep: str = ", ") -> list:
        """Add multiple values using a custom method and separator."""
        result = []
        for value in values:
            if result:
                result.append(sep)
            result.append(value)
        return result

    @staticmethod
    def is_bool(node: ast.AST) -> bool:
        """Check if the node is a boolean."""
        return isinstance(node, ast.Name) and node.id in ("False", "True")

    @staticmethod
    def is_number(node: ast.AST) -> bool:
        """Check if the node is a number."""
        return (isinstance(node, ast.Constant) and isinstance(node.value, int)) or (
            isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub))
        )

    def _ast_alias(self, node: ast.AST) -> None:
        """Add an AST alias in output."""
        # ignore alias
        pass  # noqa: PIE790

    def _ast_arg(self, node: ast.AST) -> None:
        """Add an AST arg in output."""
        prefix = self._prefix[-1] if self._prefix else ""
        self.add(f"{prefix}{node.arg}")

    def _ast_assign(self, node: ast.Assign) -> None:
        """Add an AST Assign in output."""
        self.add(
            self.fill,
            [[target, " = "] for target in node.targets],
            node.value,
        )

    def _ast_attribute(self, node: ast.Attribute) -> None:
        """Add an AST Attribute in output."""
        self.add(node.value, ".", node.attr)

    def _ast_binop(self, node: ast.BinOp) -> None:
        """Add an AST BinOp in output."""
        self.add(
            node.left,
            f" {self.binop[node.op.__class__.__name__]} ",
            node.right,
        )

    def _ast_call(self, node: ast.Call) -> None:
        """Add an AST Call in output."""
        self.add(
            node.func,
            "(",
            self.make_list(node.args),
            ")",
        )

    def _ast_compare(self, node: ast.Compare) -> None:
        """Add an AST Compare in output."""
        self.add(node.left)
        for operator, comparator in zip(node.ops, node.comparators):
            self.add(
                f" {self.cmpop[operator.__class__.__name__]} ",
                comparator,
            )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "{",
            self.make_list([[key, ": ", value] for key, value in zip(node.keys, node.values)]),
            "}",
        )

    def _ast_expr(self, node: ast.Expr) -> None:
        """Add an AST Expr in output."""
        if not isinstance(node.value, ast.Constant):  # ignore docstrings
            self.add(
                self.fill,
                node.value,
            )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            self.fill if self._indent_level == 0 else None,
            f"def {node.name}(",
            self.make_list(node.args.args),
            "):",
            self.indent,
            node.body,
            self.unindent,
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if ",
            node.test,
            ":",
            self.indent,
            node.body,
            self.unindent,
        )
        if node.orelse:
            self.add(
                self.fill,
                "else:",
                self.indent,
                node.orelse,
                self.unindent,
            )

    def _ast_index(self, node: ast.Index) -> None:
        """Add an AST Subscript in output."""
        # note: deprecated since Python 3.9
        self.add(node.value)

    def _ast_import(self, node: ast.AST) -> None:
        """Add an AST Import in output."""
        # ignore import
        pass  # noqa: PIE790

    def _ast_list(self, node: ast.AST) -> None:
        """Add an AST List in output."""
        self.add(
            "[",
            self.make_list(node.elts),
            "]",
        )

    def _ast_module(self, node: ast.Module) -> None:
        """Add an AST Module in output."""
        self.add(node.body)

    def _ast_name(self, node: ast.Name) -> None:
        """Add an AST Name in output."""
        prefix = self._prefix[-1] if self._prefix else ""
        self.add(f"{prefix}{node.id}")

    def _ast_num(self, node: ast.Num) -> None:
        """Add an AST Num in output."""
        self.add(repr(node.n))

    def _ast_pass(self, node: ast.Pass) -> None:  # noqa: ARG002
        """Add an AST Pass in output."""
        self.fill("pass")

    def _ast_return(self, node: ast.Return) -> None:
        """Add an AST Return in output."""
        self.fill("return")
        if node.value:
            self.add(" ", node.value)

    def _ast_str(self, node: ast.Str) -> None:
        """Add an AST Str in output."""
        self._ast_constant(node)

    def _ast_subscript(self, node: ast.Subscript) -> None:
        """Add an AST Subscript in output."""
        self.add(
            node.value,
            "[",
            node.slice,
            "]",
        )

    def _ast_tuple(self, node: ast.Tuple) -> None:
        """Add an AST Tuple in output."""
        self.add(
            "(",
            self.make_list(node.elts),
            "," if len(node.elts) == 1 else None,
            ")",
        )

    def _ast_unaryop(self, node: ast.UnaryOp) -> None:
        """Add an AST UnaryOp in output."""
        self.add(
            self.unaryop[node.op.__class__.__name__],
            node.operand,
        )


class UnparsePerl(UnparsePython):
    """Unparse AST to generate Perl script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def _ast_assign(self, node: ast.Assign) -> None:
        """Add an AST Assign in output."""
        self.add(
            self.fill,
            (self.prefix, "$"),
            [[target, " = "] for target in node.targets],
            (self.prefix, None),
            node.value,
            ";",
        )

    def _ast_attribute(self, node: ast.Attribute) -> None:
        """Add an AST Attribute in output."""
        saved_prefix = self._prefix
        self._prefix = []
        self.add(node.value, "::", node.attr)
        self._prefix = saved_prefix

    def _ast_binop(self, node: ast.BinOp) -> None:
        """Add an AST BinOp in output."""
        if isinstance(node.op, ast.Add) and (not self.is_number(node.left) or not self.is_number(node.right)):
            str_op = "."
        else:
            str_op = self.binop[node.op.__class__.__name__]
        self.add(
            (self.prefix, "$"),
            node.left,
            f" {str_op} ",
            node.right,
            (self.prefix, None),
        )

    def _ast_call(self, node: ast.Call) -> None:
        """Add an AST Call in output."""
        self.add(
            node.func,
            "(",
            (self.prefix, "$"),
            self.make_list(node.args),
            (self.prefix, None),
            ")",
        )

    def _ast_compare(self, node: ast.Compare) -> None:
        """Add an AST Compare in output."""
        self.add(node.left)
        for operator, comparator in zip(node.ops, node.comparators):
            if (
                isinstance(operator, (ast.Eq, ast.NotEq))
                and not self.is_number(node.left)
                and not self.is_bool(node.left)
                and not self.is_number(comparator)
                and not self.is_bool(comparator)
            ):
                custom_cmpop = {
                    "Eq": "eq",
                    "NotEq": "ne",
                }
            else:
                custom_cmpop = self.cmpop
            self.add(
                f" {custom_cmpop[operator.__class__.__name__]} ",
                comparator,
            )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if isinstance(node.value, str):
            str_node = node.value.replace("$", "\\$").replace("@", "\\@")
            self.add(f'"{str_node}"')
        elif node.value is None:
            self.add("undef")
        else:
            self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "{",
            self.make_list([[key, " => ", value] for key, value in zip(node.keys, node.values)]),
            "}",
        )

    def _ast_expr(self, node: ast.Expr) -> None:
        """Add an AST Expr in output."""
        if not isinstance(node.value, ast.Constant):  # ignore docstrings
            self.add(
                self.fill,
                node.value,
                ";",
            )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"sub {node.name}",
            self.fill,
            "{",
            self.indent,
        )
        if node.args.args:
            self.add(
                self.fill,
                "my (",
                (self.prefix, "$"),
                self.make_list(node.args.args),
                (self.prefix, None),
                ") = @_;",
            )
        self.add(
            node.body,
            self.unindent,
            self.fill,
            "}",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if (",
            (self.prefix, "$"),
            node.test,
            (self.prefix, None),
            ")",
            self.fill,
            "{",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )
        if node.orelse:
            self.add(
                self.fill,
                "else",
                self.fill,
                "{",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                "}",
            )

    def _ast_list(self, node: ast.List) -> None:
        """Add an AST List in output."""
        self.add(
            "(",
            self.make_list(node.elts),
            ")",
        )

    def _ast_pass(self, node: ast.Pass) -> None:
        """Add an AST Pass in output."""
        pass  # noqa: PIE790

    def _ast_return(self, node: ast.Return) -> None:
        """Add an AST Return in output."""
        self.fill("return")
        if node.value:
            self.add(
                " ",
                (self.prefix, "%" if isinstance(node.value, ast.Dict) else "$"),
                node.value,
                (self.prefix, None),
                ";",
            )

    def _ast_str(self, node: ast.Str) -> None:
        """Add an AST Str in output."""
        self._ast_constant(node)

    def _ast_subscript(self, node: ast.Subscript) -> None:
        """Add an AST Subscript in output."""
        self.add(
            (self.prefix, "$"),
            node.value,
            (self.prefix, None),
            "->{",
            node.slice,
            "}",
        )


class UnparseRuby(UnparsePython):
    """Unparse AST to generate Ruby script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def _ast_attribute(self, node: ast.Attribute) -> None:
        """Add an AST Attribute in output."""
        self.add(
            node.value,
            "::" if node.attr.startswith("WEECHAT_") else ".",
            node.attr,
        )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if isinstance(node.value, str):
            str_node = node.value.replace("#{", "\\#{")
            self.add(f'"{str_node}"')
        elif node.value is None:
            self.add("nil")
        else:
            self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "Hash[",
            self.make_list([[key, " => ", value] for key, value in zip(node.keys, node.values)]),
            "]",
        )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"def {node.name}",
        )
        if node.args.args:
            self.add(
                "(",
                self.make_list(node.args.args),
                ")",
            )
        self.add(
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "end",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if ",
            node.test,
            self.indent,
            node.body,
            self.unindent,
        )
        if node.orelse:
            self.add(
                self.fill,
                "else",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                "end",
            )

    def _ast_list(self, node: ast.List) -> None:
        """Add an AST List in output."""
        self.add(
            "Array[",
            self.make_list(node.elts),
            "]",
        )

    def _ast_pass(self, node: ast.Pass) -> None:
        """Add an AST Pass in output."""
        pass  # noqa: PIE790

    def _ast_str(self, node: ast.Str) -> None:
        """Add an AST Str in output."""
        self._ast_constant(node)


class UnparseLua(UnparsePython):
    """Unparse AST to generate Lua script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def __init__(self, *args, **kwargs) -> None:  # noqa: ANN002,ANN003
        """Initialize Lua parser."""
        super().__init__(*args, **kwargs)
        self.cmpop = {
            "Eq": "==",
            "NotEq": "~=",
            "Lt": "<",
            "LtE": "<=",
            "Gt": ">",
            "GtE": ">=",
        }

    def _ast_binop(self, node: ast.BinOp) -> None:
        """Add an AST BinOp in output."""
        if isinstance(node.op, ast.Add) and (not self.is_number(node.left) or not self.is_number(node.right)):
            str_op = ".."
        else:
            str_op = self.binop[node.op.__class__.__name__]
        self.add(
            node.left,
            f" {str_op} ",
            node.right,
        )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if node.value is None:
            self.add("nil")
        else:
            self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "{",
            self.make_list([["[", key, "]", "=", value] for key, value in zip(node.keys, node.values)]),
            "}",
        )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"function {node.name}",
        )
        self.add(
            "(",
            self.make_list(node.args.args),
            ")",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "end",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if ",
            node.test,
            " then",
            self.indent,
            node.body,
            self.unindent,
        )
        if node.orelse:
            self.add(
                self.fill,
                "else",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                "end",
            )

    def _ast_list(self, node: ast.List) -> None:
        """Add an AST List in output."""
        self.add(
            "{",
            self.make_list(node.elts),
            "}",
        )

    def _ast_pass(self, node: ast.Pass) -> None:
        """Add an AST Pass in output."""
        pass  # noqa: PIE790


class UnparseTcl(UnparsePython):
    """Unparse AST to generate Tcl script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def __init__(self, *args, **kwargs) -> None:  # noqa: ANN002,ANN003
        """Initialize Tcl parser."""
        super().__init__(*args, **kwargs)
        self._call = 0

    def _ast_assign(self, node: ast.Assign) -> None:
        """Add an AST Assign in output."""
        exclude_types = (ast.Dict, ast.List, ast.Constant, ast.Subscript)
        self.add(
            self.fill,
            "set ",
            node.targets[0],
            " ",
            "[" if not isinstance(node.value, exclude_types) else "",
            node.value,
            "]" if not isinstance(node.value, exclude_types) else "",
        )

    def _ast_attribute(self, node: ast.Attribute) -> None:
        """Add an AST Attribute in output."""
        saved_prefix = self._prefix
        self._prefix = []
        if node.attr.startswith("WEECHAT_"):
            self.add("$::")
        self.add(node.value, "::", node.attr)
        self._prefix = saved_prefix

    def _ast_binop(self, node: ast.BinOp) -> None:
        """Add an AST BinOp in output."""
        self.add(
            "[join [list ",
            (self.prefix, "$"),
            node.left,
            " ",
            node.right,
            (self.prefix, None),
            '] ""]',
        )

    def _ast_call(self, node: ast.Call) -> None:
        """Add an AST Call in output."""
        if self._call:
            self.add("[")
        self._call += 1
        self.add(
            node.func,
            " " if node.args else None,
            (self.prefix, "$"),
            self.make_list(node.args, sep=" "),
            (self.prefix, None),
        )
        self._call -= 1
        if self._call:
            self.add("]")

    def _ast_compare(self, node: ast.Compare) -> None:
        """Add an AST Compare in output."""
        self.prefix("$")
        if self._call:
            self.add("[expr {")
        self.add(node.left)
        for operator, comparator in zip(node.ops, node.comparators):
            if (
                isinstance(operator, (ast.Eq, ast.NotEq))
                and not self.is_number(node.left)
                and not self.is_bool(node.left)
                and not self.is_number(comparator)
                and not self.is_bool(comparator)
            ):
                custom_cmpop = {
                    "Eq": "eq",
                    "NotEq": "ne",
                }
            else:
                custom_cmpop = self.cmpop
            self.add(
                f" {custom_cmpop[operator.__class__.__name__]} ",
                comparator,
            )
        if self._call:
            self.add("}]")
        self.prefix(None)

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if isinstance(node.value, str):
            str_node = node.value.replace("$", "\\$")
            self.add(f'"{str_node}"')
        elif node.value is None:
            self.add("$::weechat::WEECHAT_NULL")
        else:
            self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "[dict create ",
            self.make_list([[key, " ", value] for key, value in zip(node.keys, node.values)], sep=" "),
            "]",
        )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"proc {node.name} {{",
            (self.make_list(node.args.args, sep=" ") if node.args.args else None),
            "} {",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if {",
            (self.prefix, "$"),
            node.test,
            (self.prefix, None),
            "} {",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )
        if node.orelse:
            self.add(
                " else {",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                "}",
            )

    def _ast_list(self, node: ast.List) -> None:
        """Add an AST List in output."""
        self.add(
            "[",
            self.make_list(node.elts, sep=" "),
            "]",
        )

    def _ast_pass(self, node: ast.Pass) -> None:
        """Add an AST Pass in output."""
        pass  # noqa: PIE790

    def _ast_return(self, node: ast.Return) -> None:
        """Add an AST Return in output."""
        self.fill("return")
        if node.value:
            self.add(
                " ",
                (self.prefix, "$"),
                node.value,
                (self.prefix, None),
            )

    def _ast_str(self, node: ast.Str) -> None:
        """Add an AST Str in output."""
        self._ast_constant(node)

    def _ast_subscript(self, node: ast.Subscript) -> None:
        """Add an AST Subscript in output."""
        self.add(
            "[dict get ",
            (self.prefix, "$"),
            node.value,
            (self.prefix, None),
            " ",
            node.slice,
            "]",
        )


class UnparseGuile(UnparsePython):
    """Unparse AST to generate Guile script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def __init__(self, *args, **kwargs) -> None:  # noqa: ANN002,ANN003
        """Initialize Guile parser."""
        super().__init__(*args, **kwargs)
        self.cmpop = {
            "Eq": "=",
            "NotEq": "<>",
            "Lt": "<",
            "LtE": "<=",
            "Gt": ">",
            "GtE": ">=",
        }
        self._call = 0
        self._let = 0

    def _ast_assign(self, node: ast.Assign) -> None:
        """Add an AST Assign in output."""
        self.add(
            self.fill,
            "(let ((",
            node.targets[0],
            " ",
            node.value,
            "))",
            self.indent,
            self.fill,
            "(begin",
            self.indent,
        )
        self._let += 1

    def _ast_attribute(self, node: ast.Attribute) -> None:
        """Add an AST Attribute in output."""
        self.add(node.value, ":", node.attr)

    def _ast_binop(self, node: ast.BinOp) -> None:
        """Add an AST BinOp in output."""
        if isinstance(node.op, ast.Add) and (
            isinstance(node.left, (ast.Name, ast.Constant)) or isinstance(node.right, (ast.Name, ast.Constant))
        ):
            self.add(
                "(string-append ",
                node.left,
                " ",
                node.right,
                ")",
            )
        else:
            self.add(
                node.left,
                f" {self.binop[node.op.__class__.__name__]} ",
                node.right,
            )

    def _ast_call(self, node: ast.Call) -> None:
        """Add an AST Call in output."""
        self._call += 1
        self.add(
            "(",
            node.func,
            " " if node.args else None,
            self.make_list(node.args, sep=" "),
            ")",
        )
        self._call -= 1

    def _ast_compare(self, node: ast.Compare) -> None:
        """Add an AST Compare in output."""
        for operator, comparator in zip(node.ops, node.comparators):
            if (
                isinstance(operator, (ast.Eq, ast.NotEq))
                and not self.is_number(node.left)
                and not self.is_bool(node.left)
                and not self.is_number(comparator)
                and not self.is_bool(comparator)
            ):
                prefix = "string"
            else:
                prefix = ""
            self.add(
                f"({prefix}{self.cmpop[operator.__class__.__name__]} ",
                node.left,
                " ",
                comparator,
                ")",
            )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if isinstance(node.value, str):
            self.add(f'"{node.value}"')
        elif node.value is None:
            self.add("#nil")
        else:
            self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "'(",
            self.make_list(
                [["(", key, " ", value, ")"] for key, value in zip(node.keys, node.values)], sep=" ",
            ),
            ")",
        )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"(define ({node.name}",
            " " if node.args.args else None,
            (self.make_list(node.args.args, sep=" ") if node.args.args else None),
            ")",
            self.indent,
            node.body,
        )
        while self._let > 0:
            self.add(
                self.unindent,
                self.fill,
                ")",
                self.unindent,
                self.fill,
                ")",
            )
            self._let -= 1
        self.add(
            self.unindent,
            self.fill,
            ")",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "(if " if isinstance(node.test, ast.Name) else "(",
            node.test,
            "" if isinstance(node.test, ast.Name) else ")",
            self.indent,
            self.fill,
            "(begin",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            ")",
            self.unindent,
        )
        if node.orelse:
            self.add(
                self.indent,
                self.fill,
                "(begin",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                ")",
                self.unindent,
            )
        self.add(self.fill, ")")

    def _ast_list(self, node: ast.List) -> None:
        """Add an AST List in output."""
        self.add(
            "'(",
            self.make_list(node.elts, sep=" "),
            ")",
        )

    def _ast_pass(self, node: ast.Pass) -> None:  # noqa: ARG002
        """Add an AST Pass in output."""
        self.add("#t")

    def _ast_return(self, node: ast.Return) -> None:
        """Add an AST Return in output."""
        if node.value:
            self.add(self.fill, node.value)

    def _ast_str(self, node: ast.Str) -> None:
        """Add an AST Str in output."""
        self._ast_constant(node)

    def _ast_subscript(self, node: ast.Subscript) -> None:
        """Add an AST Subscript in output."""
        self.add(
            "(assoc-ref ",
            node.value,
            " ",
            node.slice,
            ")",
        )


class UnparseJavaScript(UnparsePython):
    """Unparse AST to generate JavaScript script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "{",
            self.make_list([[key, ": ", value] for key, value in zip(node.keys, node.values)]),
            "}",
        )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if node.value is None:
            self.add("null")
        else:
            self.add(repr(node.value))

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"function {node.name}(",
            self.make_list(node.args.args),
            ") {",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if (",
            node.test,
            ") {",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )
        if node.orelse:
            self.add(
                " else {",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                "}",
            )

    def _ast_pass(self, node: ast.Pass) -> None:
        """Add an AST Pass in output."""
        pass  # noqa: PIE790


class UnparsePhp(UnparsePython):
    """Unparse AST to generate PHP script code.

    Note: only part of AST types are supported (just the types used by
    the script to test WeeChat scripting API).
    """

    __lineno__ = inspect.currentframe().f_lineno

    def _ast_assign(self, node: ast.Assign) -> None:
        """Add an AST Assign in output."""
        self.add(
            self.fill,
            (self.prefix, "$"),
            [[target, " = "] for target in node.targets],
            (self.prefix, None),
            node.value,
            ";",
        )

    def _ast_attribute(self, node: ast.Attribute) -> None:
        """Add an AST Attribute in output."""
        saved_prefix = self._prefix
        self._prefix = []
        if not node.attr.startswith("WEECHAT_"):
            self.add(node.value, "_")
        self.add(node.attr)
        self._prefix = saved_prefix

    def _ast_binop(self, node: ast.BinOp) -> None:
        """Add an AST BinOp in output."""
        if isinstance(node.op, ast.Add) and (
            isinstance(node.left, (ast.Name, ast.Constant)) or isinstance(node.right, (ast.Name, ast.Constant))
        ):
            str_op = "."
        else:
            str_op = self.binop[node.op.__class__.__name__]
        self.add(
            (self.prefix, "$"),
            node.left,
            f" {str_op} ",
            node.right,
            (self.prefix, None),
        )

    def _ast_call(self, node: ast.Call) -> None:
        """Add an AST Call in output."""
        self.add(
            node.func,
            "(",
            (self.prefix, "$"),
            self.make_list(node.args),
            (self.prefix, None),
            ")",
        )

    def _ast_constant(self, node: ast.Constant) -> None:
        """Add an AST Constant in output."""
        if isinstance(node.value, str):
            str_node = node.value.replace("$", "\\$")
            self.add(f'"{str_node}"')
        elif node.value is None:
            self.add("NULL")
        else:
            self.add(repr(node.value))

    def _ast_dict(self, node: ast.Dict) -> None:
        """Add an AST Dict in output."""
        self.add(
            "array(",
            self.make_list([[key, " => ", value] for key, value in zip(node.keys, node.values)]),
            ")",
        )

    def _ast_expr(self, node: ast.Expr) -> None:
        """Add an AST Expr in output."""
        if not isinstance(node.value, ast.Constant):  # ignore docstrings
            self.add(
                self.fill,
                node.value,
                ";",
            )

    def _ast_functiondef(self, node: ast.FunctionDef) -> None:
        """Add an AST FunctionDef in output."""
        self.add(
            self.fill,
            self.fill,
            f"function {node.name}(",
            (self.prefix, "$"),
            self.make_list(node.args.args),
            (self.prefix, None),
            ")",
            self.fill,
            "{",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )

    def _ast_list(self, node: ast.List) -> None:
        """Add an AST List in output."""
        self.add(
            "array(",
            self.make_list(node.elts),
            ")",
        )

    def _ast_if(self, node: ast.If) -> None:
        """Add an AST If in output."""
        self.add(
            self.fill,
            "if (",
            (self.prefix, "$"),
            node.test,
            (self.prefix, None),
            ")",
            self.fill,
            "{",
            self.indent,
            node.body,
            self.unindent,
            self.fill,
            "}",
        )
        if node.orelse:
            self.add(
                self.fill,
                "else",
                self.fill,
                "{",
                self.indent,
                node.orelse,
                self.unindent,
                self.fill,
                "}",
            )

    def _ast_pass(self, node: ast.Pass) -> None:
        """Add an AST Pass in output."""
        pass  # noqa: PIE790

    def _ast_return(self, node: ast.Return) -> None:
        """Add an AST Return in output."""
        self.fill("return")
        if node.value:
            self.add(
                " ",
                (self.prefix, "$"),
                node.value,
                (self.prefix, None),
                ";",
            )

    def _ast_str(self, node: ast.Str) -> None:
        """Add an AST Str in output."""
        self._ast_constant(node)

    def _ast_subscript(self, node: ast.Subscript) -> None:
        """Add an AST Subscript in output."""
        self.add(
            "$",
            node.value,
            "[",
            node.slice,
            "]",
        )


def get_languages() -> List[str]:
    """Return a list of supported languages: ['python', 'perl', ...]."""
    members = [
        member
        for member in inspect.getmembers(sys.modules[__name__], predicate=inspect.isclass)
        if inspect.isclass(member[1]) and member[0].startswith("Unparse") and member[0] != "Unparse"
    ]
    return [name[7:].lower() for name, _ in sorted(members, key=lambda member: member[1].__lineno__)]


LANGUAGES = get_languages()


def get_parser() -> argparse.ArgumentParser:
    """Get parser arguments."""
    all_languages = [*LANGUAGES, "all"]
    default_language = LANGUAGES[0]
    parser = argparse.ArgumentParser(
        description=(
            "Unparse Python code from stdin and generate code in "
            "another language (to stdout).\n\n"
            "The code is read from stdin and generated code is "
            "written on stdout."
        ),
    )
    parser.add_argument(
        "-l",
        "--language",
        default=default_language,
        choices=all_languages,
        help="output language (default: {default_language})",
    )
    return parser


def get_stdin() -> str:
    """Return data from standard input.

    If there is nothing in stdin, wait for data until ctrl-d (EOF)
    is received.
    """
    data = ""
    inr = select.select([sys.stdin], [], [], 0)[0]
    if not inr:
        print("Enter the code to convert (Enter + ctrl-d to end)")
    while True:
        inr = select.select([sys.stdin], [], [], 0.1)[0]
        if not inr:
            continue
        new_data = os.read(sys.stdin.fileno(), 4096)
        if not new_data:  # EOF?
            break
        data += new_data.decode("utf-8")
    return data


def convert_to_language(code: str, language: str, prefix: str = "") -> None:
    """Convert Python code to a language."""
    class_name = f"Unparse{language.capitalize()}"
    unparse_class = getattr(sys.modules[__name__], class_name)
    if prefix:
        print(prefix)
    output = io.StringIO()
    unparse_class(output=output).add(ast.parse(code))
    print(output.getvalue().lstrip())


def convert(code: str, language: str) -> None:
    """Convert Python code to one or all languages."""
    if language == "all":
        for lang in LANGUAGES:
            convert_to_language(code, lang, "\n{lang}:")
    else:
        convert_to_language(code, language)


def main() -> None:
    """Generate scripts."""
    parser = get_parser()
    args = parser.parse_args()

    code = get_stdin()
    if not code:
        print("ERROR: missing input")
        print()
        parser.print_help()
        sys.exit(1)

    convert(code, args.language)

    sys.exit(0)


if __name__ == "__main__":
    main()
