# ============================================================
# File: _solve_ode_first_order_clairaut.py
# Topic: First-Order Clairaut ODE
#
# Form:
#   y = x*y' + f(y')
# Let p = y'.
#
# Differentiate both sides:
#   y' = p = p + (x + f'(p)) p'
# => (x + f'(p)) p' = 0
#
# Hence:
#   (A) p' = 0  => p = C (constant)
#       General solution family:
#           y = C x + f(C)
#
#   (B) x + f'(p) = 0  => x = -f'(p)
#       Singular solution (parametric):
#           y = x p + f(p) = f(p) - p f'(p), with x = -f'(p)
#
# Optional IC: y(x0)=y0 -> solve y0 = C x0 + f(C) for C
#
# Notes:
#   - We always output the general family.
#   - Singular solution is shown parametrically; explicit y(x) attempted best-effort.
# ============================================================

from __future__ import annotations

from typing import Any, Dict, Optional, Union, List

import sympy as sp


def solve_ode_first_order_clairaut(
    f_of_p: Union[str, sp.Expr],
    *,
    x_symbol: str = "x",
    y_symbol: str = "y",
    p_symbol: str = "p",
    C_symbol: str = "C",
    x0: Optional[Union[int, float, str, sp.Expr]] = None,
    y0: Optional[Union[int, float, str, sp.Expr]] = None,
    locals_map: Optional[Dict[str, Any]] = None,
    simplify: bool = True,
) -> dict:
    """
    Solve Clairaut ODE:
        y = x*y' + f(y')

    Returns:
        { "problem_s": "...", "soln_s": "...", "answer_s": "..." }
    """

    # -----------------------------
    # 1) Symbols + parsing
    # -----------------------------
    x = sp.Symbol(x_symbol, real=True)
    p = sp.Symbol(p_symbol, real=True)

    y = sp.Function(y_symbol)
    yx = y(x)

    C = sp.Symbol(C_symbol, real=True)

    base_locals = {
        x_symbol: x,
        p_symbol: p,
        "pi": sp.pi,
        "E": sp.E,
        "exp": sp.exp,
        "log": sp.log,
        "ln": sp.log,
        "sqrt": sp.sqrt,
        "sin": sp.sin,
        "cos": sp.cos,
        "tan": sp.tan,
        "asin": sp.asin,
        "acos": sp.acos,
        "atan": sp.atan,
        "sinh": sp.sinh,
        "cosh": sp.cosh,
        "tanh": sp.tanh,
        "Abs": sp.Abs,
        "sec": sp.sec,
        "csc": sp.csc,
        "cot": sp.cot,
        "Piecewise": sp.Piecewise,
        "Heaviside": sp.Heaviside,
    }
    if locals_map:
        base_locals.update(locals_map)

    def L(expr: sp.Expr) -> str:
        return sp.latex(expr)

    try:
        f_raw = sp.sympify(f_of_p, locals=base_locals) if isinstance(f_of_p, str) else sp.sympify(f_of_p)
    except Exception as e:
        raise ValueError(f"Could not parse f(p)={f_of_p!r}. Error: {e}") from e

    f = sp.simplify(f_raw) if simplify else f_raw
    fp = sp.diff(f, p)
    fp_s = sp.simplify(fp) if simplify else fp

    use_ic = (x0 is not None) and (y0 is not None)
    parsed_x0 = parsed_y0 = None
    if use_ic:
        parsed_x0 = sp.sympify(x0, locals=base_locals)
        parsed_y0 = sp.sympify(y0, locals=base_locals)

    # -----------------------------
    # 2) General family: y = C x + f(C)
    # -----------------------------
    y_general = sp.simplify(C * x + f.subs(p, C)) if simplify else (C * x + f.subs(p, C))

    # -----------------------------
    # 3) Singular solution (parametric): x = -f'(p), y = f(p) - p f'(p)
    # -----------------------------
    x_param = sp.simplify(-fp_s) if simplify else (-fp_s)
    y_param = sp.simplify(f - p * fp_s) if simplify else (f - p * fp_s)

    # Try to eliminate p to get explicit y(x) best-effort
    y_singular_explicit = None
    try:
        sols_p = sp.solve(sp.Eq(x, x_param), p)
        if sols_p:
            p_expr = sols_p[0]
            y_singular_explicit = sp.simplify(y_param.subs(p, p_expr)) if simplify else y_param.subs(p, p_expr)
    except Exception:
        y_singular_explicit = None

    # -----------------------------
    # 4) Apply IC to pick C (best-effort)
    # -----------------------------
    C_value = None
    y_with_C = None
    if use_ic:
        try:
            eqC = sp.Eq(y_general.subs(x, parsed_x0), parsed_y0)
            solC = sp.solve(eqC, C)
            if solC:
                C_value = sp.simplify(solC[0]) if simplify else solC[0]
                y_with_C = sp.simplify(C_value * x + f.subs(p, C_value)) if simplify else (C_value * x + f.subs(p, C_value))
        except Exception:
            C_value = None
            y_with_C = None

    # -----------------------------
    # 5) Build HTML (match your style)
    # -----------------------------
    ode_ltx = f"{L(yx)} = {L(x)}\\,\\frac{{d{L(yx)}}}{{d{L(x)}}} + {L(f.subs(p, sp.Symbol('p')))}"
    # Use p in display for y'
    ode_ltx = ode_ltx.replace(L(sp.Symbol('p')), "p")

    diff_step_ltx = (
        f"\\frac{{d{L(yx)}}}{{d{L(x)}}} = p,\\quad "
        f"\\Rightarrow\\; \\frac{{d}}{{d{L(x)}}}\\Big({L(yx)}\\Big)"
        f"= \\frac{{d}}{{d{L(x)}}}\\Big({L(x)}p + f(p)\\Big)"
    )

    key_step_ltx = f"(x + f'(p))\\,\\frac{{dp}}{{d{L(x)}}} = 0"
    branch1_ltx = f"\\frac{{dp}}{{d{L(x)}}}=0 \\Rightarrow p=C \\Rightarrow {L(yx)} = {L(C)}{L(x)} + f({L(C)})"
    branch1_ltx = branch1_ltx.replace(f"f({L(C)})", L(f.subs(p, C)))

    branch2_ltx = f"x + f'(p)=0 \\Rightarrow x=-f'(p)=-{L(fp_s)}"
    sing_param_ltx = f"{L(yx)} = xp + f(p) = f(p) - p f'(p) = {L(y_param)}"
    sing_param_ltx = sing_param_ltx.replace(L(p), "p")

    note_parts: List[str] = []
    if y_singular_explicit is None:
        note_parts.append("Singular solution shown parametrically (elimination of \\(p\\) not simplified).")
    else:
        note_parts.append("A simplified explicit singular form was found (best-effort).")
    anti_note = " ".join(note_parts)

    soln_stmt = f"""
    <div class="wrap">
      <div class="card">
        <div class="badge">Note: First-Order Clairaut ODE<br/></div>

        \\(\\displaystyle
        \\begin{{aligned}}

        & \\displaystyle {ode_ltx} \\\\[8pt]

        & \\displaystyle \\text{{Let }} p=\\frac{{dy}}{{dx}}.\\; \\Rightarrow\\; {diff_step_ltx} \\\\[8pt]

        & \\displaystyle \\Rightarrow p = p + \\big(x + f'(p)\\big)\\,\\frac{{dp}}{{dx}} \\\\
        & \\displaystyle \\Rightarrow {key_step_ltx} \\\\[10pt]

        & \\displaystyle \\Rightarrow \\text{{Case 1: }} {branch1_ltx} \\\\[10pt]

        & \\displaystyle \\Rightarrow \\text{{Case 2: }} {branch2_ltx} \\\\
        & \\displaystyle \\phantom{{\\Rightarrow}} \\text{{and }} {sing_param_ltx} \\\\

        \\end{{aligned}}
        \\)
        <div class="note">{anti_note}</div>
      </div>
    </div>
    """.replace("\n", "")

    # IC block (if solved)
    if use_ic and (C_value is not None) and (y_with_C is not None):
        ic_ltx = f"{L(y)}({L(parsed_x0)})={L(parsed_y0)}"
        ic_block = f"""
        <br/>
        \\(\\displaystyle
        \\begin{{aligned}}
        & \\displaystyle \\text{{Apply IC }} {ic_ltx}: \\\\
        & \\displaystyle \\Rightarrow {L(C)} = {L(C_value)} \\\\
        & \\displaystyle \\Rightarrow {L(yx)} = {L(y_with_C)} \\\\
        \\end{{aligned}}
        \\)
        """.replace("\n", "")
        soln_s = soln_stmt + ic_block
        problem_s = f"Solve \\( {ode_ltx} \\) for \\( {ic_ltx} \\)"
        answer_s = f"\\( {L(yx)} = {L(y_with_C)} \\)"
    else:
        soln_s = soln_stmt
        problem_s = f"Solve \\( {ode_ltx} \\)"
        # Answer: show general family + singular
        if y_singular_explicit is not None:
            answer_s = (
                f"\\( {L(yx)} = {L(C)}{L(x)} + {L(f.subs(p, C))} \\)"
                f"<br/>"
                f"\\( \\text{{Singular: }} {L(yx)} = {L(y_singular_explicit)} \\)"
            )
        else:
            answer_s = (
                f"\\( {L(yx)} = {L(C)}{L(x)} + {L(f.subs(p, C))} \\)"
                f"<br/>"
                f"\\( \\text{{Singular (param): }} x=-{L(fp_s)},\\; y={L(y_param)} \\)"
            )

    return {
        "problem_s": problem_s.strip(),
        "soln_s": soln_s.strip(),
        "answer_s": answer_s.strip(),
    }


