# ============================================================
# File: _solve_second_order_pde_canonical_reduction.py
# Topic: Canonical reduction of general 2nd-order PDE (2 variables)
#
# General linear 2nd-order PDE (focus on 2nd-order part):
#   A u_xx + B u_xy + C u_yy + (lower order terms) = G(x,y)
#
# Classification (local if A,B,C depend on x,y):
#   Δ = B^2 - 4 A C
#     Δ < 0  -> Elliptic
#     Δ = 0  -> Parabolic
#     Δ > 0  -> Hyperbolic
#
# Canonical reduction goal:
#   Find change of variables (ξ,η) to remove mixed term u_{ξη} (constant-coeff case),
#   or (for hyperbolic/parabolic) build characteristic variables:
#
# Characteristics from:
#   A (dy)^2 - B (dx)(dy) + C (dx)^2 = 0
# Let m = dy/dx (dx != 0):
#   A m^2 - B m + C = 0
#   m = (B ± sqrt(Δ)) / (2A)
#
# Constant-coefficient (A,B,C constants) rotation method:
#   tan(2θ) = B/(A-C)  (if A != C)
#   if A == C and B != 0 -> θ = π/4
# After rotation:
#   A' u_{ξξ} + C' u_{ηη} + ...  (no cross term)
#
# Canonical second-order structures:
#   Hyperbolic:   u_{ξη} + ... = ...
#   Parabolic:    u_{ξξ} + ... = ...
#   Elliptic:     u_{ξξ} + u_{ηη} + ... = ...
#
# Output:
#   - classification
#   - Δ, slopes m (if meaningful)
#   - suggested (ξ,η) for hyperbolic/parabolic constant-coeff via characteristics
#   - rotation angle θ and new coefficients A', C' for constant-coeff
#
# Notes:
#   - This is a reducer/classifier; it does not fully transform lower-order terms.
#   - We keep everything symbolic & display canonical mapping suggestions.
# ============================================================

from __future__ import annotations

from typing import Optional, Union, Dict, Any

import sympy as sp


def solve_second_order_pde_canonical_reduction(
    A_in: Union[int, float, str, sp.Expr],
    B_in: Union[int, float, str, sp.Expr],
    C_in: Union[int, float, str, sp.Expr],
    *,
    x_symbol: str = "x",
    y_symbol: str = "y",
    u_symbol: str = "u",
    locals_map: Optional[Dict[str, Any]] = None,
    simplify: bool = True,
) -> dict:
    """
    Classify and propose canonical reduction for the 2nd-order PDE operator:
        A u_xx + B u_xy + C u_yy

    Returns:
        { "problem_s": "...", "soln_s": "...", "answer_s": "..." }
    """
    x = sp.Symbol(x_symbol, real=True)
    y = sp.Symbol(y_symbol, real=True)
    u = sp.Function(u_symbol)

    base_locals = {
        x_symbol: x,
        y_symbol: y,
        u_symbol: u,
        "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,
        "atan": sp.atan,
        "Abs": sp.Abs,
    }
    if locals_map:
        base_locals.update(locals_map)

    def P(v: Union[int, float, str, sp.Expr]) -> sp.Expr:
        return sp.sympify(v, locals=base_locals)

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

    A = P(A_in)
    B = P(B_in)
    C = P(C_in)

    if simplify:
        A = sp.simplify(A)
        B = sp.simplify(B)
        C = sp.simplify(C)

    # Discriminant
    Delta = B**2 - 4 * A * C
    if simplify:
        Delta = sp.simplify(Delta)

    # Classification
    pde_type = "Inconclusive"
    # Try decide sign; if symbolic, leave conditional
    Delta_sign = sp.sign(Delta)
    if Delta_sign in (sp.Integer(-1), -1):
        pde_type = "Elliptic (Δ < 0)"
    elif Delta_sign in (sp.Integer(0), 0):
        pde_type = "Parabolic (Δ = 0)"
    elif Delta_sign in (sp.Integer(1), 1):
        pde_type = "Hyperbolic (Δ > 0)"
    else:
        pde_type = "Type depends on sign of Δ"

    # Characteristic slopes (if A != 0)
    m1 = m2 = None
    m_note = ""
    if simplify and sp.simplify(A) == 0:
        m_note = "A=0: the characteristic slope formula m=(B±√Δ)/(2A) does not apply (use alternate form)."
    else:
        root = sp.sqrt(Delta)
        m1 = (B + root) / (2 * A)
        m2 = (B - root) / (2 * A)
        if simplify:
            m1 = sp.simplify(m1)
            m2 = sp.simplify(m2)

    # Constant-coefficient detection (best-effort): A,B,C do not depend on x,y
    const_coeff = (A.free_symbols.isdisjoint({x, y}) and B.free_symbols.isdisjoint({x, y}) and C.free_symbols.isdisjoint({x, y}))

    # Rotation angle to kill cross term (constant coefficients)
    theta = None
    theta_rule = ""
    if const_coeff:
        if simplify and sp.simplify(A - C) == 0:
            # A == C
            if simplify and sp.simplify(B) == 0:
                theta = sp.Integer(0)
                theta_rule = "A=C and B=0: no cross term already; choose θ=0."
            else:
                theta = sp.pi / 4
                theta_rule = "A=C and B≠0: choose θ=π/4 (since tan(2θ) → ∞)."
        else:
            theta = sp.atan(B / (A - C)) / 2
            theta_rule = "Choose θ such that tan(2θ)=B/(A−C)."
        if simplify:
            theta = sp.simplify(theta)

    # New coefficients after rotation (constant coefficients)
    # Standard formulas for transformed second-order quadratic form:
    # A' = A cos^2θ + B cosθ sinθ + C sin^2θ
    # C' = A sin^2θ - B cosθ sinθ + C cos^2θ
    Apr = Cpr = Bpr = None
    if const_coeff and theta is not None:
        ct = sp.cos(theta)
        st = sp.sin(theta)
        Apr = A * ct**2 + B * ct * st + C * st**2
        Cpr = A * st**2 - B * ct * st + C * ct**2
        # B' should be zero by construction (up to simplification)
        Bpr = (C - A) * sp.sin(2 * theta) + B * sp.cos(2 * theta)

        if simplify:
            Apr = sp.simplify(Apr)
            Cpr = sp.simplify(Cpr)
            Bpr = sp.simplify(Bpr)

    # Suggested characteristic variables for hyperbolic/parabolic (constant coefficients)
    xi_suggest = eta_suggest = None
    char_suggest_note = ""
    if const_coeff and m1 is not None and m2 is not None:
        # For hyperbolic: two real distinct slopes -> choose invariants y - m x
        # For parabolic: repeated -> choose one invariant and a transversal variable
        if Delta_sign in (sp.Integer(1), 1):
            xi_suggest = y - m1 * x
            eta_suggest = y - m2 * x
            if simplify:
                xi_suggest = sp.simplify(xi_suggest)
                eta_suggest = sp.simplify(eta_suggest)
            char_suggest_note = "Hyperbolic: choose ξ and η constant along the two characteristic families."
        elif Delta_sign in (sp.Integer(0), 0):
            xi_suggest = y - m1 * x  # m1==m2
            eta_suggest = x
            if simplify:
                xi_suggest = sp.simplify(xi_suggest)
            char_suggest_note = "Parabolic: one characteristic family; choose ξ along characteristics and η transversal."

    # Canonical form statement (second-order structure only)
    canonical_form = ""
    if Delta_sign in (sp.Integer(1), 1):
        canonical_form = "Hyperbolic canonical structure: \\(u_{\\xi\\eta} + \\text{(lower-order)} = \\tilde G\\)."
    elif Delta_sign in (sp.Integer(0), 0):
        canonical_form = "Parabolic canonical structure: \\(u_{\\xi\\xi} + \\text{(lower-order)} = \\tilde G\\)."
    elif Delta_sign in (sp.Integer(-1), -1):
        canonical_form = "Elliptic canonical structure: \\(u_{\\xi\\xi}+u_{\\eta\\eta}+\\text{(lower-order)}=\\tilde G\\)."
    else:
        canonical_form = "Canonical structure depends on sign of \\(\\Delta\\)."

    # Build HTML (avoid f-string brace hazards)
    pde_ltx = Ltx(A) + "u_{xx} + " + Ltx(B) + "u_{xy} + " + Ltx(C) + "u_{yy}"

    slope_lines = ""
    if m_note:
        slope_lines = "& \\displaystyle \\Rightarrow \\text{" + m_note + "} \\\\"
    elif m1 is not None and m2 is not None:
        slope_lines = (
            "& \\displaystyle \\Rightarrow \\text{Characteristic slopes } m=\\frac{dy}{dx} \\text{ solve } A m^2 - B m + C = 0 \\\\"
            "& \\displaystyle \\Rightarrow m_{1,2}=\\frac{B\\pm\\sqrt{\\Delta}}{2A} = " + Ltx(m1) + ",\\; " + Ltx(m2) + " \\\\"
        )

    rot_lines = ""
    if const_coeff and theta is not None:
        rot_lines += (
            "& \\displaystyle \\Rightarrow \\text{Constant coefficients: use rotation to remove }u_{xy} \\\\"
            "& \\displaystyle \\Rightarrow " + theta_rule.replace("−", "-") + " \\\\"
            "& \\displaystyle \\Rightarrow \\theta = " + Ltx(theta) + " \\\\"
        )
        if Apr is not None and Cpr is not None and Bpr is not None:
            rot_lines += (
                "& \\displaystyle \\Rightarrow \\text{After rotation: } A' u_{\\xi\\xi} + B' u_{\\xi\\eta} + C' u_{\\eta\\eta} \\\\"
                "& \\displaystyle \\Rightarrow A'=" + Ltx(Apr) + ",\\; B'=" + Ltx(Bpr) + ",\\; C'=" + Ltx(Cpr) + " \\\\"
                "& \\displaystyle \\Rightarrow \\text{(Ideally }B'=0\\text{, so cross term is removed.)} \\\\[6pt]"
            )

    char_lines = ""
    if xi_suggest is not None:
        char_lines += (
            "& \\displaystyle \\Rightarrow \\text{" + char_suggest_note + "} \\\\"
            "& \\displaystyle \\Rightarrow \\xi = " + Ltx(xi_suggest) + ",\\quad \\eta = " + Ltx(eta_suggest) + " \\\\[6pt]"
        )

    soln_body = (
        "& \\displaystyle \\text{Second-order part: } " + pde_ltx + " \\\\[6pt]"
        "& \\displaystyle \\Rightarrow \\Delta = B^2 - 4AC = " + Ltx(Delta) + " \\\\[6pt]"
        "& \\displaystyle \\Rightarrow \\text{Classification: } " + pde_type + " \\\\[10pt]"
        + slope_lines
        + rot_lines
        + char_lines +
        "& \\displaystyle \\Rightarrow \\text{" + canonical_form + "} \\\\"
    )

    soln_s = (
        "<div class='wrap'><div class='card'>"
        "<div class='badge'>2nd-Order PDE — Classification & Canonical Reduction (cross term removal)<br/></div>"
        "\\(\\displaystyle \\begin{aligned}"
        + soln_body +
        "\\end{aligned}\\)"
        "<div class='note'>This reducer focuses on the 2nd-order operator. Lower-order terms transform via chain rule and are handled separately (or by routing to elliptic/parabolic/hyperbolic solvers after reduction).</div>"
        "</div></div>"
    )

    problem_s = "Classify and reduce the PDE (second-order part): \\( " + pde_ltx + " \\)."

    # Answer: classification + recommended change
    ans_parts = ["\\Delta=" + Ltx(Delta), "\\text{" + pde_type + "}"]
    if xi_suggest is not None:
        ans_parts.append("\\xi=" + Ltx(xi_suggest))
        ans_parts.append("\\eta=" + Ltx(eta_suggest))
    elif const_coeff and theta is not None:
        ans_parts.append("\\theta=" + Ltx(theta))
        if Apr is not None and Cpr is not None:
            ans_parts.append("A'=" + Ltx(Apr))
            ans_parts.append("C'=" + Ltx(Cpr))

    answer_s = "\\( " + ",\\; ".join(ans_parts) + " \\)"

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