# pylint:disable=missing-class-docstring,too-many-boolean-expressions,unused-argument,no-self-use
from __future__ import annotations
from typing import cast, Any, TYPE_CHECKING

from collections.abc import Iterable
from collections.abc import Callable
from collections import defaultdict, Counter
import logging
import struct
import re

from angr.ailment import Block, Expr, Stmt, Tmp
from angr.ailment.constant import UNDETERMINED_SIZE
from angr.ailment.expression import StackBaseOffset, BinaryOp
from unique_log_filter import UniqueLogFilter

from angr.sim_type import (
    SimTypeLongLong,
    SimTypeInt,
    SimTypeShort,
    SimTypeChar,
    SimTypeWideChar,
    SimTypePointer,
    SimStruct,
    SimType,
    SimTypeBottom,
    SimTypeArray,
    SimTypeFunction,
    SimTypeFloat,
    SimTypeDouble,
    TypeRef,
    SimTypeNum,
    SimTypeFixedSizeArray,
    SimTypeLength,
    SimTypeReg,
    SimTypeInt128,
    SimTypeInt256,
    SimTypeInt512,
    SimCppClass,
)
from angr.knowledge_plugins.functions import Function
from angr.sim_variable import (
    SimVariable,
    SimTemporaryVariable,
    SimStackVariable,
    SimMemoryVariable,
    SimRegisterVariable,
)
from angr.utils.constants import is_alignment_mask
from angr.utils.library import get_cpp_function_name
from angr.utils.loader import is_in_readonly_segment, is_in_readonly_section
from angr.utils.types import unpack_typeref, unpack_pointer_and_array, dereference_simtype_by_lib
from angr.utils.strings import decode_utf16_string
from angr.analyses.decompiler.utils import structured_node_is_simple_return
from angr.analyses.decompiler.notes.deobfuscated_strings import DeobfuscatedStringsNote
from angr.errors import UnsupportedNodeTypeError, AngrRuntimeError
from angr.knowledge_plugins.cfg.memory_data import MemoryData, MemoryDataSort
from angr.analyses import Analysis, register_analysis
from angr.analyses.decompiler.region_identifier import MultiNode
from angr.analyses.decompiler.structuring.structurer_nodes import (
    SequenceNode,
    CodeNode,
    ConditionNode,
    ConditionalBreakNode,
    LoopNode,
    BreakNode,
    SwitchCaseNode,
    IncompleteSwitchCaseNode,
    ContinueNode,
    CascadingConditionNode,
)
from .base import (
    BaseStructuredCodeGenerator,
    InstructionMapping,
    PositionMapping,
    PositionMappingElement,
    IdentType,
    CConstantType,
)

if TYPE_CHECKING:
    import archinfo
    import angr
    from angr.knowledge_plugins.variables.variable_manager import VariableManagerInternal


l = logging.getLogger(name=__name__)
l.addFilter(UniqueLogFilter())


INDENT_DELTA = 4


def qualifies_for_simple_cast(ty1, ty2):
    # converting ty1 to ty2 - can this happen precisely?
    # used to decide whether to add explicit typecasts instead of doing *(int*)&v1
    return (
        ty1.size == ty2.size
        and isinstance(ty1, (SimTypeInt, SimTypeChar, SimTypeNum, SimTypePointer))
        and isinstance(ty2, (SimTypeInt, SimTypeChar, SimTypeNum, SimTypePointer))
    )


def qualifies_for_implicit_cast(ty1, ty2):
    # converting ty1 to ty2 - can this happen without a cast?
    # used to decide whether to omit typecasts from output during promotion
    # this function need to answer the question:
    # when does having a cast vs having an implicit promotion affect the result?
    # the answer: I DON'T KNOW
    if not isinstance(ty1, (SimTypeInt, SimTypeChar, SimTypeNum)) or not isinstance(
        ty2, (SimTypeInt, SimTypeChar, SimTypeNum)
    ):
        return False

    return ty1.size <= ty2.size if ty1.size is not None and ty2.size is not None else False


def extract_terms(expr: CExpression) -> tuple[int, list[tuple[int, CExpression]]]:
    # handle unnecessary type casts
    if isinstance(expr, CTypeCast):
        expr = MakeTypecastsImplicit.collapse(expr.dst_type, expr.expr)
    if (
        isinstance(expr, CTypeCast)
        and isinstance(expr.dst_type, SimTypeInt)
        and isinstance(expr.src_type, SimTypeInt)
        and expr.dst_type.size == expr.src_type.size
        and expr.dst_type.signed != expr.src_type.signed
    ):
        # (unsigned int)(a + 60)  ==>  a + 60, assuming a + 60 is an int
        expr = expr.expr

    if isinstance(expr, CConstant):
        return expr.value, []
    # elif isinstance(expr, CUnaryOp) and expr.op == 'Minus'
    if isinstance(expr, CBinaryOp) and expr.op == "Add":
        c1, t1 = extract_terms(expr.lhs)
        c2, t2 = extract_terms(expr.rhs)
        return c1 + c2, t1 + t2
    if isinstance(expr, CBinaryOp) and expr.op == "Sub":
        c1, t1 = extract_terms(expr.lhs)
        c2, t2 = extract_terms(expr.rhs)
        return c1 - c2, t1 + [(-c, t) for c, t in t2]
    if isinstance(expr, CBinaryOp) and expr.op == "Mul":
        if isinstance(expr.lhs, CConstant):
            c, t = extract_terms(expr.rhs)
            return c * expr.lhs.value, [(c1 * expr.lhs.value, t1) for c1, t1 in t]
        if isinstance(expr.rhs, CConstant):
            c, t = extract_terms(expr.lhs)
            return c * expr.rhs.value, [(c1 * expr.rhs.value, t1) for c1, t1 in t]
        return 0, [(1, expr)]
    if isinstance(expr, CBinaryOp) and expr.op == "Shl":
        if isinstance(expr.rhs, CConstant):
            c, t = extract_terms(expr.lhs)
            return c << expr.rhs.value, [(c1 << expr.rhs.value, t1) for c1, t1 in t]
        return 0, [(1, expr)]
    return 0, [(1, expr)]


def is_machine_word_size_type(type_: SimType, arch: archinfo.Arch) -> bool:
    return isinstance(type_, SimTypeReg) and type_.size == arch.bits


def guess_value_type(value: int, project: angr.Project) -> SimType | None:
    if project.kb.functions.contains_addr(value):
        # might be a function pointer
        return SimTypePointer(SimTypeBottom(label="void")).with_arch(project.arch)
    if value > 4096:
        sec = project.loader.find_section_containing(value)
        if sec is not None and sec.is_readable:
            return SimTypePointer(SimTypeBottom(label="void")).with_arch(project.arch)
        seg = project.loader.find_segment_containing(value)
        if seg is not None and seg.is_readable:
            return SimTypePointer(SimTypeBottom(label="void")).with_arch(project.arch)
    return None


def type_equals(t0: SimType, t1: SimType) -> bool:
    # special logic for C++ classes
    if isinstance(t0, SimCppClass) and isinstance(t1, SimCppClass):  # noqa: SIM102
        # TODO: Use the information (class names, etc.) in types_stl
        if {t1.name, t0.name} == {
            "std::string",
            "class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char>>",
        }:
            return True
    return t0 == t1


def type_to_c_repr_chunks(ty: SimType, name=None, name_type=None, full=False, indent_str=""):
    """
    Helper generator function to turn a SimType into generated tuples of (C-string, AST node).
    """
    if isinstance(ty, SimStruct):
        if full:
            # struct def preamble
            yield indent_str, None
            if isinstance(ty, SimCppClass):
                yield "class ", None
            else:
                yield "typedef struct ", None
            yield ty.name, ty
            yield " {\n", None

            # each of the fields
            # fields should be indented
            new_indent_str = (
                " " * 4
            ) + indent_str  # TODO: hardcoded as 4 character space indents, which is same as SimStruct.c_repr
            for k, v in ty.fields.items():
                yield new_indent_str, None
                yield from type_to_c_repr_chunks(v, name=k, name_type=CStructFieldNameDef(k), full=False, indent_str="")
                yield ";\n", None

            # struct def postamble
            yield "} ", None
            yield ty.name, ty
            yield ";\n\n", None

        else:
            assert name
            assert name_type
            yield indent_str, None
            yield ty.name, ty
            yield " ", None
            if name:
                yield name, name_type
    elif isinstance(ty, SimType):
        assert name
        assert name_type
        raw_type_str = ty.c_repr(name=name)
        assert name in raw_type_str

        type_pre, type_post = raw_type_str.split(name, 1)

        if type_pre.endswith(" "):
            type_pre_spaces = " " * (len(type_pre) - len(type_pre.rstrip(" ")))
            type_pre = type_pre.rstrip(" ")
        else:
            type_pre_spaces = ""

        yield indent_str, None
        yield type_pre, ty
        if type_pre_spaces:
            yield type_pre_spaces, None
        yield name, name_type
        yield type_post, CArrayTypeLength(type_post)
    # This case was used when generating externs, apparently there can be cases where the name is not known
    elif ty is None:
        assert name
        assert name_type
        yield "<missing-type> ", None
        yield name, name_type
    else:
        assert False


#
#   C Representation Classes
#


class CConstruct:
    """
    Represents a program construct in C.
    Acts as the base class for all other representation constructions.
    """

    __slots__ = ("codegen", "idx", "tags")

    def __init__(self, codegen, tags=None):
        self.tags = tags or {}
        self.codegen: StructuredCodeGenerator = codegen
        self.idx = codegen.next_idx(self.__class__.__name__)

    def c_repr(self, initial_pos=0, indent=0, pos_to_node=None, pos_to_addr=None, addr_to_pos=None):
        """
        Creates the C representation of the code and displays it by
        constructing a large string. This function is called by each program function that needs to be decompiled.
        The map_pos_to_node and map_pos_to_addr act as position maps for the location of each variable and statement to
        be tracked for later GUI operations. The map_pos_to_addr also contains expressions that are nested inside of
        statements.
        """

        pending_stmt_comments = dict(self.codegen.stmt_comments)
        pending_expr_comments = dict(self.codegen.expr_comments)

        def mapper(chunks):
            # start all positions at beginning of document
            pos = initial_pos

            last_insn_addr = None

            # track all variables so we can tell if this is a declaration or not
            used_vars = set()

            # get each string and object representation of the chunks
            for s, obj in chunks:
                # filter out anything that is not a statement or expression object
                if isinstance(obj, (CStatement, CExpression)):
                    # only add statements/expressions that can be address tracked into map_pos_to_addr
                    if hasattr(obj, "tags") and obj.tags is not None and "ins_addr" in obj.tags:
                        if isinstance(obj, CVariable) and obj not in used_vars:
                            used_vars.add(obj)
                        else:
                            last_insn_addr = obj.tags["ins_addr"]

                            # all valid statements and expressions should be added to map_pos_to_addr and
                            # tracked for instruction mapping from disassembly
                            if pos_to_addr is not None:
                                pos_to_addr.add_mapping(pos, len(s), obj)
                            if addr_to_pos is not None:
                                addr_to_pos.add_mapping(obj.tags["ins_addr"], pos)

                    # add all variables, constants, and function calls to map_pos_to_node for highlighting
                    # add ops to pos_to_node but NOT ast_to_pos
                    if (
                        isinstance(
                            obj,
                            (
                                CVariable,
                                CConstant,
                                CStructField,
                                CIndexedVariable,
                                CVariableField,
                                CBinaryOp,
                                CUnaryOp,
                                CAssignment,
                                CFunctionCall,
                                CLabel,
                            ),
                        )
                        and pos_to_node is not None
                    ):
                        pos_to_node.add_mapping(pos, len(s), obj)

                # add (), {}, [], and [20] to mapping for highlighting as well as the full functions name
                elif isinstance(obj, (CClosingObject, CFunction, CArrayTypeLength, CStructFieldNameDef)):
                    if s is None:
                        continue

                    if pos_to_node is not None:
                        pos_to_node.add_mapping(pos, len(s), obj)

                elif isinstance(obj, SimType):
                    if pos_to_node is not None:
                        if isinstance(obj, TypeRef):
                            pos_to_node.add_mapping(pos, len(s), obj.type)
                        else:
                            pos_to_node.add_mapping(pos, len(s), obj)

                if s.endswith("\n"):
                    text = pending_stmt_comments.pop(last_insn_addr, None) if isinstance(last_insn_addr, int) else None
                    if text is not None:
                        todo = "  // " + text
                        pos += len(s) - 1
                        yield s[:-1]
                        pos += len(todo)
                        yield todo
                        s = "\n"

                pos += len(s)
                yield s

                if isinstance(obj, CExpression):
                    text = pending_expr_comments.pop(last_insn_addr, None) if isinstance(last_insn_addr, int) else None
                    if text is not None:
                        todo = " /*" + text + "*/ "
                        pos += len(todo)
                        yield todo

            if pending_expr_comments or pending_stmt_comments:
                yield "// Orphaned comments\n"
                for text in pending_stmt_comments.values():
                    yield "// " + text + "\n"
                for text in pending_expr_comments.values():
                    yield "/* " + text + "*/\n"

        # A special note about this line:
        # Polymorphism allows that the c_repr_chunks() call will be called
        # by the CFunction class, which will then call each statement within it and construct
        # the chunks that get printed in qccode_edit in angr-management.
        return "".join(mapper(self.c_repr_chunks(indent)))

    def c_repr_chunks(self, indent=0, asexpr=False):
        raise NotImplementedError

    @staticmethod
    def indent_str(indent=0):
        return " " * indent


class CFunction(CConstruct):  # pylint:disable=abstract-method
    """
    Represents a function in C.
    """

    __slots__ = (
        "addr",
        "arg_list",
        "demangled_name",
        "functy",
        "name",
        "omit_header",
        "show_demangled_name",
        "statements",
        "unified_local_vars",
        "variable_manager",
        "variables_in_use",
    )

    def __init__(
        self,
        addr,
        name,
        functy: SimTypeFunction,
        arg_list: list[CVariable],
        statements,
        variables_in_use,
        variable_manager,
        demangled_name=None,
        show_demangled_name=True,
        omit_header=False,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.addr = addr
        self.name = name
        self.functy = functy
        self.arg_list = arg_list
        self.statements = statements
        self.variables_in_use = variables_in_use
        self.variable_manager: VariableManagerInternal = variable_manager
        self.demangled_name = demangled_name
        self.unified_local_vars: dict[SimVariable, set[tuple[CVariable, SimType]]] = {}
        self.show_demangled_name = show_demangled_name
        self.omit_header = omit_header

        self.refresh()

    def refresh(self):
        self.unified_local_vars = self.get_unified_local_vars()

    def get_unified_local_vars(self) -> dict[SimVariable, set[tuple[CVariable, SimType]]]:
        unified_to_var_and_types: dict[SimVariable, set[tuple[CVariable, SimType]]] = defaultdict(set)

        arg_set: set[SimVariable] = set()
        for arg in self.arg_list:
            # TODO: Handle CIndexedVariable
            if isinstance(arg, CVariable):
                if arg.unified_variable is not None:
                    arg_set.add(arg.unified_variable)
                else:
                    arg_set.add(arg.variable)

        # output each variable and its type
        for var, cvar in self.variables_in_use.items():
            if isinstance(var, SimMemoryVariable) and not isinstance(var, SimStackVariable):
                # Skip all global variables
                continue

            if var in arg_set or cvar.unified_variable in arg_set:
                continue

            unified_var = self.variable_manager.unified_variable(var)
            if unified_var is not None:
                key = unified_var
                var_type = self.variable_manager.get_variable_type(var)  # FIXME
            else:
                key = var
                var_type = self.variable_manager.get_variable_type(var)

            if var_type is None:
                var_type = SimTypeBottom().with_arch(self.codegen.project.arch)

            unified_to_var_and_types[key].add((cvar, var_type))

        return unified_to_var_and_types

    def variable_list_repr_chunks(self, indent=0):
        indent_str = self.indent_str(indent)

        for variable in self.sort_local_vars(self.unified_local_vars):
            cvar_and_vartypes = self.unified_local_vars[variable]

            yield indent_str, None

            # pick the first cvariable
            # picking any cvariable is enough since highlighting works on the unified variable
            try:
                cvariable = next(iter(cvar_and_vartypes))[0]
            except StopIteration:
                # this should never happen, but pylint complains
                continue

            if variable.name:
                name = variable.name
            elif isinstance(variable, SimTemporaryVariable):
                name = f"tmp_{variable.tmp_id}"
            else:
                name = str(variable)

            # sort by the following:
            #   * if it's a a non-basic type
            #   * the number of occurrences
            #   * the repr of the type itself
            # TODO: The type selection should actually happen during variable unification
            vartypes = [x[1] for x in cvar_and_vartypes]
            count = Counter(vartypes)
            vartypes = sorted(
                count.copy(),
                key=lambda x, ct=count: (isinstance(x, (SimTypeChar, SimTypeInt, SimTypeFloat)), ct[x], repr(x)),
            )

            for i, var_type in enumerate(vartypes):
                if i == 0:
                    yield from type_to_c_repr_chunks(var_type, name=name, name_type=cvariable)
                    yield ";  // ", None
                    yield variable.loc_repr(self.codegen.project.arch), None
                # multiple types
                else:
                    if i == 1:
                        yield ", Other Possible Types: ", None
                    else:
                        yield ", ", None
                    if isinstance(var_type, SimType):
                        yield var_type.c_repr(), var_type
                    else:
                        yield str(var_type), var_type
            yield "\n", None

        if self.unified_local_vars:
            yield "\n", None

    def c_repr_chunks(self, indent=0, asexpr=False):
        if self.omit_header:
            yield from self.headerless_c_repr_chunks(indent=indent)
        else:
            yield from self.full_c_repr_chunks(indent=indent, asexpr=asexpr)

    def headerless_c_repr_chunks(self, indent=0):
        yield from self.statements.c_repr_chunks(indent=indent)
        yield "\n", None

    def full_c_repr_chunks(self, indent=0, asexpr=False):
        indent_str = self.indent_str(indent)
        if self.codegen.show_local_types:
            local_types = [unpack_typeref(ty) for ty in self.variable_manager.types.iter_own()]
            name_to_structtypes = {}
            for ty in local_types:
                if isinstance(ty, SimStruct):
                    name_to_structtypes[ty.name] = ty
                    for field in ty.fields.values():
                        if isinstance(field, SimTypePointer):
                            if isinstance(field.pts_to, (SimTypeArray, SimTypeFixedSizeArray)):
                                field = field.pts_to.elem_type
                            else:
                                field = field.pts_to
                        if isinstance(field, SimStruct) and field not in local_types:
                            if field.name and not field.fields and field.name in name_to_structtypes:
                                # we use SimStruct types with empty fields to refer to already defined struct types
                                # for example, see how struct _IO_marker is defined in sim_type.py
                                continue
                            if field.name:
                                name_to_structtypes[field.name] = field
                            local_types.append(field)

                yield from type_to_c_repr_chunks(ty, full=True, indent_str=indent_str)

        if self.codegen.show_externs and self.codegen.cexterns:
            for v in sorted(self.codegen.cexterns, key=lambda v: str(v.variable.name)):
                if v.variable not in self.variables_in_use:
                    continue
                varname = v.c_repr() if v.type is None else v.variable.name
                yield "extern ", None
                if v.type is None:
                    yield "<unknown-type>", None
                else:
                    yield from type_to_c_repr_chunks(v.type, name=varname, name_type=v, full=False)
                yield ";\n", None
            yield "\n", None

        yield indent_str, None

        # header comments (if they exist)
        assert self.codegen.cfunc is not None and self.codegen.cfunc.addr is not None
        header_comments = self.codegen.kb.comments.get(self.codegen.cfunc.addr, [])
        if header_comments:
            header_cmt = self._line_wrap_comment("".join(header_comments))
            yield header_cmt, None

        if self.codegen._func.is_plt:
            yield "// attributes: PLT stub\n", None

        # return type
        assert self.functy.returnty is not None
        yield self.functy.returnty.c_repr(name="").strip(" "), self.functy.returnty
        yield " ", None
        # function name
        if self.demangled_name and self.show_demangled_name:
            normalized_name = get_cpp_function_name(self.demangled_name)
        else:
            normalized_name = self.name
        yield normalized_name, self
        # argument list
        paren = CClosingObject("(")
        brace = CClosingObject("{")
        yield "(", paren
        for i, (arg_type, cvariable) in enumerate(zip(self.functy.args, self.arg_list)):
            if i:
                yield ", ", None

            variable = cvariable.unified_variable or cvariable.variable
            yield from type_to_c_repr_chunks(arg_type, name=variable.name, name_type=cvariable, full=False)

        yield ")", paren
        # function body
        if self.codegen.braces_on_own_lines:
            yield "\n", None
            yield indent_str, None
        else:
            yield " ", None
        yield "{", brace
        yield "\n", None
        yield from self.variable_list_repr_chunks(indent=indent + INDENT_DELTA)
        yield from self.statements.c_repr_chunks(indent=indent + INDENT_DELTA)
        yield indent_str, None
        yield "}", brace
        yield "\n", None

    @staticmethod
    def _line_wrap_comment(comment: str, width=80) -> str:
        lines = comment.splitlines()
        wrapped_cmt = ""

        for line in lines:
            if len(line) < width:
                wrapped_cmt += line + "\n"
                continue

            for i, c in enumerate(line):
                if i % width == 0 and i != 0:
                    wrapped_cmt += "\n"
                wrapped_cmt += c

            wrapped_cmt += "\n"

        return "".join([f"// {line}\n" for line in wrapped_cmt.splitlines()])

    @staticmethod
    def sort_local_vars(local_vars: Iterable[SimVariable]) -> list[SimVariable]:
        # Order:
        # - SimRegisterVariable, ordered based on their identifiers
        # - SimStackVariables, ordered based on their stack offsets
        # - SimMemoryVariable (but not stack variables)  - we should not have global variables anyway
        reg_vars, stack_vars, mem_vars = [], [], []
        for var in local_vars:
            match var:
                case SimRegisterVariable():
                    reg_vars.append(var)
                case SimStackVariable():
                    stack_vars.append(var)
                case SimMemoryVariable():
                    mem_vars.append(var)
                case _:
                    pass

        reg_vars = sorted(reg_vars, key=lambda v: v.ident)
        stack_vars = sorted(stack_vars, key=lambda v: (v.offset, v.ident))
        mem_vars = sorted(mem_vars, key=lambda v: (v.addr if isinstance(v.addr, int) else -1, v.ident))
        return reg_vars + stack_vars + mem_vars


class CStatement(CConstruct):  # pylint:disable=abstract-method
    """
    Represents a statement in C.
    """

    def __init__(self, tags=None, codegen=None):
        super().__init__(codegen=codegen, tags=tags)


class CExpression(CConstruct):
    """
    Base class for C expressions.
    """

    __slots__ = ("_type", "collapsed")

    def __init__(self, collapsed=False, tags=None, codegen=None):
        super().__init__(codegen=codegen, tags=tags)
        self._type = None
        self.collapsed = collapsed

    @property
    def type(self):
        raise NotImplementedError(f"Class {type(self)} does not implement type().")

    def set_type(self, v):
        self._type = v

    @staticmethod
    def _try_c_repr_chunks(expr):
        if hasattr(expr, "c_repr_chunks"):
            yield from expr.c_repr_chunks()
        else:
            yield str(expr), expr


class CStatements(CStatement):
    """
    Represents a sequence of statements in C.
    """

    __slots__ = (
        "addr",
        "statements",
    )

    def __init__(self, statements, addr=None, **kwargs):
        super().__init__(**kwargs)

        self.statements = statements
        self.addr = addr

    def c_repr_chunks(self, indent=0, asexpr=False):
        indent_str = self.indent_str(indent)
        if self.codegen.display_block_addrs:
            yield indent_str, None
            yield f"/* Block {hex(self.addr) if self.addr is not None else 'unknown'} */", None
            yield "\n", None
        for stmt in self.statements:
            yield from stmt.c_repr_chunks(indent=indent, asexpr=asexpr)
            if asexpr:
                yield ", ", None


class CAILBlock(CStatement):
    """
    Represents a block of AIL statements.
    """

    __slots__ = ("block",)

    def __init__(self, block, **kwargs):
        super().__init__(**kwargs)

        self.block = block

    def c_repr_chunks(self, indent=0, asexpr=False):
        indent_str = self.indent_str(indent=indent)
        r = str(self.block)
        for stmt in r.split("\n"):
            yield indent_str, None
            yield stmt, None
            yield "\n", None


class CLoop(CStatement):  # pylint:disable=abstract-method
    """
    Represents a loop in C.
    """

    __slots__ = ()


class CWhileLoop(CLoop):
    """
    Represents a while loop in C.
    """

    __slots__ = (
        "body",
        "condition",
    )

    def __init__(self, condition, body, **kwargs):
        super().__init__(**kwargs)

        self.condition = condition
        self.body = body

    def c_repr_chunks(self, indent=0, asexpr=False):
        indent_str = self.indent_str(indent=indent)

        yield indent_str, None
        yield "while ", None
        paren = CClosingObject("(")
        brace = CClosingObject("{")
        yield "(", paren
        if self.condition is None:
            yield "true", self
        else:
            yield from self.condition.c_repr_chunks()
        yield ")", paren
        if self.codegen.braces_on_own_lines:
            yield "\n", None
            yield indent_str, None
        else:
            yield " ", None
        if self.body is None:
            yield ";", None
            yield "\n", None
        else:
            yield "{", brace
            yield "\n", None
            yield from self.body.c_repr_chunks(indent=indent + INDENT_DELTA)
            yield indent_str, None
            yield "}", brace
            yield "\n", None


class CDoWhileLoop(CLoop):
    """
    Represents a do-while loop in C.
    """

    __slots__ = (
        "body",
        "condition",
    )

    def __init__(self, condition, body, **kwargs):
        super().__init__(**kwargs)

        self.condition = condition
        self.body = body

    def c_repr_chunks(self, indent=0, asexpr=False):
        indent_str = self.indent_str(indent=indent)
        brace = CClosingObject("{")
        paren = CClosingObject("(")

        yield indent_str, None
        yield "do", self
        if self.codegen.braces_on_own_lines:
            yield "\n", None
            yield indent_str, None
        else:
            yield " ", None
        if self.body is not None:
            yield "{", brace
            yield "\n", None
            yield from self.body.c_repr_chunks(indent=indent + INDENT_DELTA)
            yield indent_str, None
            yield "}", brace
        else:
            yield "{", brace
            yield " ", None
            yield "}", brace
        yield " ", None
        yield "while ", self
        yield "(", paren
        if self.condition is None:
            yield "true", self
        else:
            yield from self.condition.c_repr_chunks()
        yield ")", paren
        yield ";\n", self


class CForLoop(CStatement):
    """
    Represents a for-loop in C.
    """

    __slots__ = (
        "body",
        "condition",
        "initializer",
        "iterator",
    )

    def __init__(self, initializer, condition, iterator, body, **kwargs):
        super().__init__(**kwargs)

        self.initializer = initializer
        self.condition = condition
        self.iterator = iterator
        self.body = body

    def c_repr_chunks(self, indent=0, asexpr=False):
        indent_str = self.indent_str(indent=indent)
        brace = CClosingObject("{")
        paren = CClosingObject("(")

        yield indent_str, None
        yield "for ", self
        yield "(", paren
        if self.initializer is not None:
            yield from self.initializer.c_repr_chunks(indent=0, asexpr=True)
        yield "; ", None
        if self.condition is not None:
            yield from self.condition.c_repr_chunks(indent=0)
        yield "; ", None
        if self.iterator is not None:
            yield from self.iterator.c_repr_chunks(indent=0, asexpr=True)
        yield ")", paren

        if self.body is not None:
            if self.codegen.braces_on_own_lines:
                yield "\n", None
                yield indent_str, None
            else:
                yield " ", None

            yield "{", brace
            yield "\n", None
            yield from self.body.c_repr_chunks(indent=indent + INDENT_DELTA)
            yield indent_str, None
            yield "}", brace
        else:
            yield ";", None
        yield "\n", None


class CIfElse(CStatement):
    """
    Represents an if-else construct in C.
    """

    __slots__ = (
        "condition_and_nodes",
        "cstyle_ifs",
        "else_node",
        "simplify_else_scope",
    )

    def __init__(
        self,
        condition_and_nodes: list[tuple[CExpression, CStatement | None]],
        else_node=None,
        simplify_else_scope=False,
        cstyle_ifs=True,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.condition_and_nodes = condition_and_nodes
        self.else_node = else_node
        self.simplify_else_scope = simplify_else_scope
        self.cstyle_ifs = cstyle_ifs

        if not self.condition_and_nodes:
            raise ValueError("You must specify at least one condition")

