import abc
from collections.abc import Callable
import inspect
import math
import re
from typing import Any

from esphome.core import (
    CORE,
    ID,
    Define,
    EnumValue,
    HexInt,
    Lambda,
    Library,
    TimePeriodMicroseconds,
    TimePeriodMilliseconds,
    TimePeriodMinutes,
    TimePeriodNanoseconds,
    TimePeriodSeconds,
)
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
from esphome.types import Expression, SafeExpType, TemplateArgsType
from esphome.util import OrderedDict
from esphome.yaml_util import ESPHomeDataBase


class RawExpression(Expression):
    __slots__ = ("text",)

    def __init__(self, text: str):
        self.text = text

    def __str__(self):
        return self.text


class AssignmentExpression(Expression):
    __slots__ = ("type", "modifier", "name", "rhs")

    def __init__(self, type_, modifier, name, rhs):
        self.type = type_
        self.modifier = modifier
        self.name = name
        self.rhs = safe_exp(rhs)

    def __str__(self):
        if self.type is None:
            return f"{self.name} = {self.rhs}"
        return f"{self.type} {self.modifier}{self.name} = {self.rhs}"


class VariableDeclarationExpression(Expression):
    __slots__ = ("type", "modifier", "name", "static")

    def __init__(
        self, type_: "MockObj", modifier: str, name: ID, *, static: bool = False
    ) -> None:
        self.type = type_
        self.modifier = modifier
        self.name = name
        self.static = static

    def __str__(self) -> str:
        prefix = "static " if self.static else ""
        return f"{prefix}{self.type} {self.modifier}{self.name}"


class ExpressionList(Expression):
    __slots__ = ("args",)

    def __init__(self, *args: SafeExpType | None):
        # Remove every None on end
        args = list(args)
        while args and args[-1] is None:
            args.pop()
        self.args = [safe_exp(arg) for arg in args]

    def __str__(self):
        text = ", ".join(str(x) for x in self.args)
        return indent_all_but_first_and_last(text)

    def __iter__(self):
        return iter(self.args)


class TemplateArguments(Expression):
    __slots__ = ("args",)

    def __init__(self, *args: SafeExpType):
        self.args = ExpressionList(*args)

    def __str__(self):
        return f"<{self.args}>"

    def __iter__(self):
        return iter(self.args)


class CallExpression(Expression):
    __slots__ = ("base", "template_args", "args")

    def __init__(self, base: Expression, *args: SafeExpType):
        self.base = base
        if args and isinstance(args[0], TemplateArguments):
            self.template_args = args[0]
            args = args[1:]
        else:
            self.template_args = None
        self.args = ExpressionList(*args)

    def __str__(self):
        if self.template_args is not None:
            return f"{self.base}{self.template_args}({self.args})"
        return f"{self.base}({self.args})"


class StructInitializer(Expression):
    __slots__ = ("base", "args")

    def __init__(self, base: Expression, *args: tuple[str, SafeExpType | None]):
        self.base = base
        # TODO: args is always a Tuple, is this check required?
        if not isinstance(args, OrderedDict):
            args = OrderedDict(args)
        self.args = OrderedDict()
        for key, value in args.items():
            if value is None:
                continue
            exp = safe_exp(value)
            self.args[key] = exp

    def __str__(self):
        cpp = f"{self.base}{{\n"
        for key, value in self.args.items():
            cpp += f"  .{key} = {value},\n"
        cpp += "}"
        return cpp


class ArrayInitializer(Expression):
    __slots__ = ("multiline", "args")

    def __init__(self, *args: Any, multiline: bool = False):
        self.multiline = multiline
        self.args = []
        for arg in args:
            if arg is None:
                continue
            exp = safe_exp(arg)
            self.args.append(exp)

    def __str__(self):
        if not self.args:
            return "{}"
        if self.multiline:
            cpp = "{\n  "
            cpp += ",\n  ".join(str(arg) for arg in self.args)
            cpp += ",\n}"
        else:
            cpp = f"{{{', '.join(str(arg) for arg in self.args)}}}"
        return cpp


class ParameterExpression(Expression):
    __slots__ = ("type", "id")

    def __init__(self, type_, id_):
        self.type = safe_exp(type_)
        self.id = id_

    def __str__(self):
        return f"{self.type} {self.id}"


class ParameterListExpression(Expression):
    __slots__ = ("parameters",)

    def __init__(self, *parameters: ParameterExpression | tuple[SafeExpType, str]):
        self.parameters = []
        for parameter in parameters:
            if not isinstance(parameter, ParameterExpression):
                parameter = ParameterExpression(*parameter)
            self.parameters.append(parameter)

    def __str__(self):
        return ", ".join(str(x) for x in self.parameters)


class LambdaExpression(Expression):
    __slots__ = ("parts", "parameters", "capture", "return_type", "source")

    def __init__(
        self, parts, parameters, capture: str = "=", return_type=None, source=None
    ):
        self.parts = parts
        if not isinstance(parameters, ParameterListExpression):
            parameters = ParameterListExpression(*parameters)
        self.parameters = parameters
        self.source = source
        self.capture = capture
        self.return_type = safe_exp(return_type) if return_type is not None else None

    def __str__(self):
        # Stateless lambdas (empty capture) implicitly convert to function pointers
        # when assigned to function pointer types - no unary + needed
        cpp = f"[{self.capture}]({self.parameters})"
        if self.return_type is not None:
            cpp += f" -> {self.return_type}"
        cpp += " {\n"
        if self.source is not None:
            cpp += f"{self.source.as_line_directive}\n"
        cpp += f"{self.content}\n}}"
        return indent_all_but_first_and_last(cpp)

    @property
    def content(self):
        return "".join(str(part) for part in self.parts)


# pylint: disable=abstract-method
class Literal(Expression, metaclass=abc.ABCMeta):
    __slots__ = ()


class StringLiteral(Literal):
    __slots__ = ("string",)

    def __init__(self, string: str):
        super().__init__()
        self.string = string

    def __str__(self):
        return cpp_string_escape(self.string)


class LogStringLiteral(Literal):
    """A string literal that uses LOG_STR() macro for flash storage on ESP8266."""

    __slots__ = ("string",)

    def __init__(self, string: str) -> None:
        super().__init__()
        self.string = string

    def __str__(self) -> str:
        return f"LOG_STR({cpp_string_escape(self.string)})"


class FlashStringLiteral(Literal):
    """A string literal wrapped in ESPHOME_F() for PROGMEM storage on ESP8266.

    On ESP8266, ESPHOME_F(s) expands to F(s) which stores the string in flash (PROGMEM).
    On other platforms, ESPHOME_F(s) expands to plain s (no-op).
    """

    __slots__ = ("string",)

    def __init__(self, string: str) -> None:
        super().__init__()
        self.string = string

    def __str__(self) -> str:
        return f"ESPHOME_F({cpp_string_escape(self.string)})"


class IntLiteral(Literal):
    __slots__ = ("i",)

    def __init__(self, i: int):
        super().__init__()
        self.i = i

    def __str__(self):
        if self.i > 4294967295:
            return f"{self.i}ULL"
        if self.i > 2147483647:
            return f"{self.i}UL"
        if self.i < -2147483648:
            return f"{self.i}LL"
        return str(self.i)


class BoolLiteral(Literal):
    __slots__ = ("binary",)

    def __init__(self, binary: bool):
        super().__init__()
        self.binary = binary

    def __str__(self):
        return "true" if self.binary else "false"


class HexIntLiteral(Literal):
    __slots__ = ("i",)

    def __init__(self, i: int):
        super().__init__()
        self.i = HexInt(i)

    def __str__(self):
        return str(self.i)


class FloatLiteral(Literal):
    __slots__ = ("f",)

    def __init__(self, value: float):
        super().__init__()
        self.f = value

    def __str__(self):
        if math.isnan(self.f):
            return "NAN"
        return f"{self.f}f"


class BinOpExpression(Expression):
    __slots__ = ("op", "lhs", "rhs")

    def __init__(self, lhs: SafeExpType, op: str, rhs: SafeExpType):
        self.lhs = safe_exp(lhs)
        self.op = op
        self.rhs = safe_exp(rhs)

    def __str__(self):
        # Surround with parentheses to ensure generated code has same
        # order as python one
        return f"({self.lhs} {self.op} {self.rhs})"


class UnaryOpExpression(Expression):
    __slots__ = ("op", "exp")

    def __init__(self, op: str, exp: SafeExpType):
        self.op = op
        self.exp = safe_exp(exp)

    def __str__(self):
        return f"({self.op}{self.exp})"


def safe_exp(obj: SafeExpType) -> Expression:
    """Try to convert obj to an expression by automatically converting native python types to
    expressions/literals.
    """
    from esphome.cpp_types import bool_, float_, int32

    if isinstance(obj, Expression):
        return obj
    if isinstance(obj, EnumValue):
        return safe_exp(obj.enum_value)
    if isinstance(obj, bool):
        return BoolLiteral(obj)
    if isinstance(obj, str):
        return StringLiteral(obj)
    if isinstance(obj, HexInt):
        return HexIntLiteral(obj)
    if isinstance(obj, int):
        return IntLiteral(obj)
    if isinstance(obj, float):
        return FloatLiteral(obj)
    if isinstance(obj, TimePeriodNanoseconds):
        return IntLiteral(int(obj.total_nanoseconds))
    if isinstance(obj, TimePeriodMicroseconds):
        return IntLiteral(int(obj.total_microseconds))
    if isinstance(obj, TimePeriodMilliseconds):
        return IntLiteral(int(obj.total_milliseconds))
    if isinstance(obj, TimePeriodSeconds):
        return IntLiteral(int(obj.total_seconds))
    if isinstance(obj, TimePeriodMinutes):
        return IntLiteral(int(obj.total_minutes))
    if isinstance(obj, (tuple, list)):
        return ArrayInitializer(*[safe_exp(o) for o in obj])
    if obj is bool:
        return bool_
    if obj is int:
        return int32
    if obj is float:
        return float_
    if isinstance(obj, ID):
        raise ValueError(
            f"Object {obj} is an ID. Did you forget to register the variable?"
        )
    if inspect.isgenerator(obj):
        raise ValueError(
            f"Object {obj} is a coroutine. Did you forget to await the expression with 'await'?"
        )
    raise ValueError("Object is not an expression", obj)


class Statement(abc.ABC):
    __slots__ = ()

    @abc.abstractmethod
    def __str__(self):
        """
        Convert statement into C++ code
        """


class RawStatement(Statement):
    __slots__ = ("text",)

    def __init__(self, text: str):
        self.text = text

    def __str__(self):
        return self.text


class ExpressionStatement(Statement):
    __slots__ = ("expression",)

    def __init__(self, expression):
        self.expression = safe_exp(expression)

    def __str__(self):
        return f"{self.expression};"


class LineComment(Statement):
    __slots__ = ("value",)

    def __init__(self, value: str):
        self.value = value

    def __str__(self):
        parts = re.sub(r"\\\s*\n", r"<cont>\n", self.value, flags=re.MULTILINE).split(
            "\n"
        )
        parts = [f"// {x}" for x in parts]
        return "\n".join(parts)


class ProgmemAssignmentExpression(AssignmentExpression):
    __slots__ = ()

    def __init__(self, type_, name, rhs):
        super().__init__(type_, "", name, rhs)

    def __str__(self):
        return f"static constexpr {self.type} {self.name}[] PROGMEM = {self.rhs}"


class StaticConstAssignmentExpression(AssignmentExpression):
    __slots__ = ()

    def __init__(self, type_, name, rhs):
        super().__init__(type_, "", name, rhs)

    def __str__(self):
        return f"static const {self.type} {self.name}[] = {self.rhs}"


def progmem_array(id_, rhs) -> "MockObj":
    rhs = safe_exp(rhs)
    obj = MockObj(id_, ".")
    assignment = ProgmemAssignmentExpression(id_.type, id_, rhs)
    CORE.add(assignment)
    CORE.register_variable(id_, obj)
    return obj


def static_const_array(id_, rhs) -> "MockObj":
    rhs = safe_exp(rhs)
    obj = MockObj(id_, ".")
    assignment = StaticConstAssignmentExpression(id_.type, id_, rhs)
    CORE.add(assignment)
    CORE.register_variable(id_, obj)
    return obj


def statement(expression: Expression | Statement) -> Statement:
    """Convert expression into a statement unless is already a statement."""
    if isinstance(expression, Statement):
        return expression
    return ExpressionStatement(expression)


def literal(name: str) -> "MockObj":
    """Create a literal name that will appear in the generated code
    not surrounded by quotes.

    :param name: The name of the literal.
    :return: The literal as a MockObj.
    """
    return MockObj(name, "")


def variable(
    id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True
) -> "MockObj":
    """Declare a new variable, not pointer type, in the code generation.

    :param id_: The ID used to declare the variable.
    :param rhs: The expression to place on the right hand side of the assignment.
    :param type_: Manually define a type for the variable, only use this when it's not possible
      to do so during config validation phase (for example because of template arguments).
    :param register: If true register the variable with the core

    :return: The new variable as a MockObj.
    """
    assert isinstance(id_, ID)
    rhs = safe_exp(rhs)
    obj = MockObj(id_, ".")
    if type_ is not None:
        id_.type = type_
    assignment = AssignmentExpression(id_.type, "", id_, rhs)
    CORE.add(assignment)
    if register:
        CORE.register_variable(id_, obj)
    return obj


def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) -> None:
    """Declare a new variable, not pointer type, in the code generation, within a scoped block
    The variable is only usable within the callback
    The callback cannot be async.

    :param id_: The ID used to declare the variable.
    :param rhs: The expression to place on the right hand side of the assignment.
    :param callback: The function to invoke that will receive the temporary variable
    :param args: args to pass to the callback in addition to the temporary variable

    """

    # throw if the callback is async:
    assert not inspect.iscoroutinefunction(callback), (
        "with_local_variable() callback cannot be async!"
    )

    CORE.add(RawStatement("{"))  # output opening curly brace
    obj = variable(id_, rhs, None, True)
    # invoke user-provided callback to generate code with this local variable
    callback(obj, *args)
    CORE.add(RawStatement("}"))  # output closing curly brace


def new_variable(
    id_: ID, rhs: SafeExpType, type_: "MockObj" = None, *, static: bool = True
) -> "MockObj":
    """Declare and define a new variable, not pointer type, in the code generation.

    :param id_: The ID used to declare the variable.
    :param rhs: The expression to place on the right hand side of the assignment.
    :param type_: Manually define a type for the variable, only use this when it's not possible
      to do so during config validation phase (for example because of template arguments).
    :param static: If True (default), declare with static storage class for optimization.
      Set to False when the variable must have external linkage (e.g., to match library declarations).

    :return: The new variable as a MockObj.
    """
    assert isinstance(id_, ID)
    rhs = safe_exp(rhs)
    obj = MockObj(id_, ".")
    if type_ is not None:
        id_.type = type_
    decl = VariableDeclarationExpression(id_.type, "", id_, static=static)
    CORE.add_global(decl)
    assignment = AssignmentExpression(None, "", id_, rhs)
    CORE.add(assignment)
    CORE.register_variable(id_, obj)
    return obj


def _extract_component_ns(type_str: str) -> str:
    """Extract the component namespace from a fully-qualified C++ type string.

    Strips leading ``esphome::`` and template arguments, then returns
    the first namespace segment.  Falls back to ``"esphome"`` when the
    type has no namespace qualifier (after stripping templates).

    Examples::

        esphome::dsmr::Dsmr                                        -> dsmr
        esphome::logger::Logger                                     -> logger
        esphome::Automation<std::optional<bool>, std::optional<bool>> -> esphome
        Logger                                                      -> esphome
    """
    bare = type_str.removeprefix("esphome::")
    # Strip template arguments before namespace extraction to avoid
    # matching :: inside template params (e.g. Automation<std::optional<bool>>)
    bare_no_template = bare.split("<", maxsplit=1)[0]
    if "::" in bare_no_template:
        return bare_no_template.split("::", maxsplit=1)[0].rstrip("_")
    return "esphome"


def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
    """Declare a new pointer variable in the code generation.

    :param id_: The ID used to declare the variable.
    :param rhs: The expression to place on the right hand side of the assignment.
    :param type_: Manually define a type for the variable, only use this when it's not possible
      to do so during config validation phase (for example because of template arguments).

    :return: The new variable as a MockObj.
    """
    rhs = safe_exp(rhs)
    obj = MockObj(id_, "->")
    if type_ is not None:
        id_.type = type_

    if isinstance(rhs, MockObj) and rhs.is_new_expr:
        # For 'new' allocations, use placement new into static storage
        # to avoid heap fragmentation on embedded devices.
        #
        # Storage must be sized and aligned for the actual instantiated class,
        # which may be a subclass of id_.type (e.g. `cv.declare_id(BaseClass)`
        # combined with `SubClass.new()` — used by ili9xxx, waveshare_epaper,
        # etc. to select a model-specific constructor). Using id_.type would
        # run the base-class default constructor instead, silently losing any
        # subclass initialization. Template args live on the CallExpression
        # and are re-emitted below.
        call_expr = rhs.base
        assert isinstance(call_expr, CallExpression), (
            f"Expected CallExpression for placement new, got {type(call_expr)}"
        )
        actual_type = rhs.new_type if rhs.new_type is not None else id_.type
        if call_expr.template_args is not None:
            actual_type = f"{actual_type}{call_expr.template_args}"
        pointer_type = id_.type
        # Extract component namespace from type for memory analysis attribution
        component_ns = _extract_component_ns(str(actual_type))
        storage_name = f"{component_ns}__{id_.id}__pstorage"

        # Declare aligned byte array for the object storage
        CORE.add_global(
            RawStatement(
                f"alignas({actual_type}) static unsigned char {storage_name}[sizeof({actual_type})];"
            )
        )
        # Pointer declaration uses id_.type to preserve the declared base-class
        # pointer type for downstream callers (polymorphism through base ptr).
        CORE.add_global(
            AssignmentExpression(
                f"static {pointer_type}",
                "*const ",
                id_,
                MockObj(f"reinterpret_cast<{pointer_type} *>({storage_name})"),
            )
        )
        placement_new = CallExpression(f"new({id_.id}) {actual_type}", *call_expr.args)
        CORE.add(ExpressionStatement(placement_new))
    else:
        decl = VariableDeclarationExpression(id_.type, "*", id_, static=True)
        CORE.add_global(decl)
        CORE.add(AssignmentExpression(None, None, id_, rhs))

    CORE.register_variable(id_, obj)
    return obj


def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
    """Declare a new pointer variable in the code generation by calling it's constructor
    with the given arguments.

    :param id_: The ID used to declare the variable (also specifies the type).
    :param args: The values to pass to the constructor.

    :return: The new variable as a MockObj.
    """
    if args and isinstance(args[0], TemplateArguments):
        id_ = id_.copy()
        id_.type = id_.type.template(args[0])
        args = args[1:]
    rhs = id_.type.new(*args)
    return Pvariable(id_, rhs)


def add(expression: Expression | Statement, prepend: bool = False):
    """Add an expression to the codegen section.

    After this is called, the given given expression will
    show up in the setup() function after this has been called.
    """
    CORE.add(expression, prepend)


def add_global(expression: SafeExpType | Statement, prepend: bool = False):
    """Add an expression to the codegen global storage (above setup())."""
    CORE.add_global(expression, prepend)


def add_library(name: str, version: str | None, repository: str | None = None):
    """Add a library to the codegen library storage.

    :param name: The name of the library (for example 'AsyncTCP')
    :param version: The version of the library, may be None.
    :param repository: The repository for the library
    """
    CORE.add_library(Library(name, version, repository))


def add_build_flag(build_flag: str):
    """Add a global build flag to the compiler flags."""
    CORE.add_build_flag(build_flag)


def add_build_unflag(build_unflag: str) -> None:
    """Add a global build unflag to the compiler flags."""
    CORE.add_build_unflag(build_unflag)


def set_cpp_standard(standard: str) -> None:
    """Set C++ standard with compiler flag `-std={standard}`."""
    CORE.add_build_unflag("-std=gnu++11")
    CORE.add_build_unflag("-std=gnu++14")
    CORE.add_build_unflag("-std=gnu++17")
    CORE.add_build_unflag("-std=gnu++23")
    CORE.add_build_unflag("-std=gnu++2a")
    CORE.add_build_unflag("-std=gnu++2b")
    CORE.add_build_unflag("-std=gnu++2c")
    CORE.add_build_flag(f"-std={standard}")


def add_define(name: str, value: SafeExpType = None):
    """Add a global define to the auto-generated defines.h file.

    Optionally define a value to set this define to.
    """
    if value is None:
        CORE.add_define(Define(name))
    else:
        CORE.add_define(Define(name, safe_exp(value)))


def add_platformio_option(key: str, value: str | list[str]):
    CORE.add_platformio_option(key, value)


async def get_variable(id_: ID) -> "MockObj":
    """
    Wait for the given ID to be defined in the code generation and
    return it as a MockObj.

    This is a coroutine, you need to await it with an 'await' expression!

    :param id_: The ID to retrieve
    :return: The variable as a MockObj.
    """
    return await CORE.get_variable(id_)


async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
    """
    Wait for the given ID to be defined in the code generation and
    return it as a MockObj.

    This is a coroutine, you need to await it with an 'await' expression!

    :param id_: The ID to retrieve
    :return: The variable as a MockObj.
    """
    return await CORE.get_variable_with_full_id(id_)


async def process_lambda(
    value: Lambda | Expression,
    parameters: TemplateArgsType,
    capture: str = "",
    return_type: SafeExpType = None,
) -> LambdaExpression | None:
    """Process the given lambda value into a LambdaExpression.

    This is a coroutine because lambdas can depend on other IDs,
    you need to await it with 'await'!

    :param value: The lambda to process.
    :param parameters: The parameters to pass to the Lambda, list of tuples
    :param capture: The capture expression for the lambda, usually ''.
    :param return_type: The return type of the lambda.
    :return: The generated lambda expression.
    """
    from esphome.components.globals import (
        GlobalsComponent,
        RestoringGlobalsComponent,
        RestoringGlobalStringComponent,
    )

    if value is None:
        return None
    # Inadvertently passing a malformed parameters value will lead to the build process mysteriously hanging at the
    # "Generating C++ source..." stage, so check here to save the developer's hair.
    assert isinstance(parameters, list) and all(
        isinstance(p, tuple) and len(p) == 2 for p in parameters
    )
    if isinstance(value, Expression):
        value = Lambda(value)

    parts = value.parts[:]
    for i, id in enumerate(value.requires_ids):
        full_id, var = await get_variable_with_full_id(id)
        if (
            full_id is not None
            and isinstance(full_id.type, MockObjClass)
            and (
                full_id.type.inherits_from(GlobalsComponent)
                or full_id.type.inherits_from(RestoringGlobalsComponent)
                or full_id.type.inherits_from(RestoringGlobalStringComponent)
            )
        ):
            parts[i * 3 + 1] = var.value()
            continue

        if parts[i * 3 + 2] == ".":
            parts[i * 3 + 1] = var._
        else:
            parts[i * 3 + 1] = var
        parts[i * 3 + 2] = ""

    if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
        location = value.esp_range.start_mark
        location.line += value.content_offset
    else:
        location = None
    return LambdaExpression(parts, parameters, capture, return_type, location)


def is_template(value):
    """Return if value is a lambda expression."""
    return isinstance(value, Lambda)


async def templatable(
    value: Any,
    args: list[tuple[SafeExpType, str]],
    output_type: SafeExpType | None,
    to_exp: Callable | dict = None,
    *,
    wrap_constant: bool = False,
):
    """Generate code for a templatable config option.

    If `value` is a templated value, the lambda expression is returned.
    For std::string output, constants are returned as-is (with PROGMEM wrapping),
    using the std::string-specific TemplatableValue specialization.
    For all other output types, constants are wrapped in stateless lambdas
    so that TemplatableFn-backed macro-generated fields can store them as
    function pointers.

    :param value: The value to process.
    :param args: The arguments for the lambda expression.
    :param output_type: The output type of the lambda expression.
    :param to_exp: An optional callable to use for converting non-templated values.
    :return: The potentially templated value.
    """
    if is_template(value):
        return await process_lambda(value, args, return_type=output_type)
    # Late import to avoid circular dependency (cpp_generator <-> cpp_types).
    from esphome.cpp_types import std_string

    if to_exp is not None:
        value = to_exp[value] if isinstance(to_exp, dict) else to_exp(value)
    elif (
        isinstance(value, str) and output_type is not None and output_type is std_string
    ):
        # Automatically wrap static strings in ESPHOME_F() for PROGMEM storage on ESP8266.
        # On other platforms ESPHOME_F() is a no-op returning const char*.
        return FlashStringLiteral(value)
    # Wrap non-string constants in stateless lambdas so that TemplatableFn
    # (used by TEMPLATABLE_VALUE macro) stores them as function pointers.
    # wrap_constant=True forces wrapping even with output_type=None (compiler deduces type).
    if (output_type is not None or wrap_constant) and output_type is not std_string:
        return LambdaExpression(
            f"return {safe_exp(value)};",
            args,
            capture="",
            return_type=output_type,
        )
    return value


class MockObj(Expression):
    """A general expression that can be used to represent any value.

    Mostly consists of magic methods that allow ESPHome's codegen syntax.
    """

    __slots__ = ("base", "op", "is_new_expr", "new_type")

    def __init__(self, base, op=".", is_new_expr=False, new_type=None) -> None:
        self.base = base
        self.op = op
        self.is_new_expr = is_new_expr
        # For `is_new_expr=True` objects, `new_type` holds the class name being
        # constructed (e.g. "ili9xxx::ILI9XXXST7789V"). Needed by Pvariable so
        # placement new uses the actual subclass rather than id_.type.
        self.new_type = new_type

    def __getattr__(self, attr: str) -> "MockObj":
        # prevent python dunder methods being replaced by mock objects
        if attr.startswith("__"):
            raise AttributeError()
        next_op = "."
        if attr.startswith("P") and self.op not in ["::", ""]:
            attr = attr[1:]
            next_op = "->"
        attr = attr.removeprefix("_")
        return MockObj(f"{self.base}{self.op}{attr}", next_op)

    def __call__(self, *args: SafeExpType) -> "MockObj":
        call = CallExpression(self.base, *args)
        return MockObj(
            call, self.op, is_new_expr=self.is_new_expr, new_type=self.new_type
        )

    def __str__(self):
        return str(self.base)

    def __repr__(self):
        return f"MockObj<{str(self.base)}>"

    @property
    def _(self) -> "MockObj":
        return MockObj(f"{self.base}{self.op}")

    @property
    def new(self) -> "MockObj":
        return MockObj(f"new {self.base}", "->", is_new_expr=True, new_type=self.base)

    def template(self, *args: SafeExpType) -> "MockObj":
        """Apply template parameters to this object."""
        if len(args) != 1 or not isinstance(args[0], TemplateArguments):
            args = TemplateArguments(*args)
        else:
            args = args[0]
        return MockObj(f"{self.base}{args}")

    def namespace(self, name: str) -> "MockObj":
        return MockObj(f"{self._}{name}", "::")

    def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass":
        op = "" if self.op == "" else "::"
        result = MockObjClass(f"{self.base}{op}{name}", ".", parents=parents)
        CORE.id_classes[str(result)] = result
        return result

    def struct(self, name: str) -> "MockObjClass":
        return self.class_(name)

    def enum(self, name: str, is_class: bool = False) -> "MockObj":
        result = MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op)
        CORE.id_classes[str(result)] = result
        return result

    def operator(self, name: str) -> "MockObj":
        """Various other operations.

        Named operator because it's a C++ keyword and can't occur in valid code.
        """
        if name == "ref":
            return MockObj(f"{self.base} &", "")
        if name == "ptr":
            return MockObj(f"{self.base} *", "")
        if name == "const_ptr":
            return MockObj(f"{self.base} *const", "")
        if name == "const":
            return MockObj(f"const {self.base}", "")
        raise ValueError("Expected one of ref, ptr, const_ptr, const.")

    @property
    def using(self) -> "MockObj":
        assert self.op == "::"
        return MockObj(f"using namespace {self.base}")

    def __getitem__(self, item: str | Expression) -> "MockObj":
        next_op = "."
        if isinstance(item, str) and item.startswith("P"):
            item = item[1:]
            next_op = "->"
        return MockObj(f"{self.base}[{item}]", next_op)

    def __lt__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "<", other)
        return MockObj(op)

    def __le__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "<=", other)
        return MockObj(op)

    def __eq__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "==", other)
        return MockObj(op)

    def __ne__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "!=", other)
        return MockObj(op)

    def __gt__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, ">", other)
        return MockObj(op)

    def __ge__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, ">=", other)
        return MockObj(op)

    def __add__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "+", other)
        return MockObj(op)

    def __sub__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "-", other)
        return MockObj(op)

    def __mul__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "*", other)
        return MockObj(op)

    def __truediv__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "/", other)
        return MockObj(op)

    def __mod__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "%", other)
        return MockObj(op)

    def __lshift__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "<<", other)
        return MockObj(op)

    def __rshift__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, ">>", other)
        return MockObj(op)

    def __and__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "&", other)
        return MockObj(op)

    def __xor__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "^", other)
        return MockObj(op)

    def __or__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "|", other)
        return MockObj(op)

    def __radd__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "+", self)
        return MockObj(op)

    def __rsub__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "-", self)
        return MockObj(op)

    def __rmul__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "*", self)
        return MockObj(op)

    def __rtruediv__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "/", self)
        return MockObj(op)

    def __rmod__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "%", self)
        return MockObj(op)

    def __rlshift__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "<<", self)
        return MockObj(op)

    def __rrshift__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, ">>", self)
        return MockObj(op)

    def __rand__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "&", self)
        return MockObj(op)

    def __rxor__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "^", self)
        return MockObj(op)

    def __ror__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(other, "|", self)
        return MockObj(op)

    def __iadd__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "+=", other)
        return MockObj(op)

    def __isub__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "-=", other)
        return MockObj(op)

    def __imul__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "*=", other)
        return MockObj(op)

    def __itruediv__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "/=", other)
        return MockObj(op)

    def __imod__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "%=", other)
        return MockObj(op)

    def __ilshift__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "<<=", other)
        return MockObj(op)

    def __irshift__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, ">>=", other)
        return MockObj(op)

    def __iand__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "&=", other)
        return MockObj(op)

    def __ixor__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "^=", other)
        return MockObj(op)

    def __ior__(self, other: SafeExpType) -> "MockObj":
        op = BinOpExpression(self, "|=", other)
        return MockObj(op)

    def __neg__(self) -> "MockObj":
        op = UnaryOpExpression("-", self)
        return MockObj(op)

    def __pos__(self) -> "MockObj":
        op = UnaryOpExpression("+", self)
        return MockObj(op)

    def __invert__(self) -> "MockObj":
        op = UnaryOpExpression("~", self)
        return MockObj(op)


class MockObjEnum(MockObj):
    def __init__(self, *args, **kwargs):
        self._enum = kwargs.pop("enum")
        self._is_class = kwargs.pop("is_class")
        base = kwargs.pop("base")
        if self._is_class:
            base = f"{base}::{self._enum}"
        kwargs["op"] = "::"
        kwargs["base"] = base
        MockObj.__init__(self, *args, **kwargs)

    def __str__(self):
        if self._is_class:
            return super().__str__()
        return f"{self.base}{self.op}{self._enum}"

    def __repr__(self):
        return f"MockObj<{str(self.base)}>"


class MockObjClass(MockObj):
    def __init__(self, *args, **kwargs):
        parens = kwargs.pop("parents")
        MockObj.__init__(self, *args, **kwargs)
        self._parents = []
        for paren in parens:
            if not isinstance(paren, MockObjClass):
                raise ValueError
            self._parents.append(paren)
            # pylint: disable=protected-access
            self._parents += paren._parents

    def inherits_from(self, other: "MockObjClass") -> bool:
        if str(self) == str(other):
            return True
        return any(str(parent) == str(other) for parent in self._parents)

    def template(self, *args: SafeExpType) -> "MockObjClass":
        if len(args) != 1 or not isinstance(args[0], TemplateArguments):
            args = TemplateArguments(*args)
        else:
            args = args[0]
        new_parents = self._parents[:]
        new_parents.append(self)
        return MockObjClass(f"{self.base}{args}", parents=new_parents)

    def __repr__(self):
        return f"MockObjClass<{str(self.base)}, parents={self._parents}>"
