from dash import Dash, dash, html, dcc, Input, Output, State, no_update, ctx
import plotly.graph_objects as go
import numpy as np
import dash_bootstrap_components as dbc

import builtins
from dash.exceptions import PreventUpdate
import traceback

import webbrowser
from threading import Timer

from ellipsoide import EllipsoidTriaxial
import winkelumrechnungen as wu
import ausgaben as aus
from GHA_triaxial.utils import alpha_ell2para, alpha_para2ell

from GHA_triaxial.gha1_ana import gha1_ana
from GHA_triaxial.gha1_num import gha1_num
from GHA_triaxial.gha1_ES import gha1_ES
from GHA_triaxial.gha1_approx import gha1_approx

from GHA_triaxial.gha2_num import gha2_num
from GHA_triaxial.gha2_ES import gha2_ES
from GHA_triaxial.gha2_approx import gha2_approx


# Prints von importierten Funktionen unterdücken
def _no_print(*args, **kwargs):
    pass

#builtins.print = _no_print


# Bootstrap (CSS) einbindung
app = Dash(__name__, suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.BOOTSTRAP])
# App-Name im Tab
app.title = "Geodätische Hauptaufgaben"


# Erzeugen der Eingabefelder
def inputfeld(left_text, input_id, right_text="", width=200, min=None, max=None):
    return html.Div(
        children=[
            html.Span(f"{left_text} =", style={"minWidth": 36, "textAlign": "right", "marginRight": 5}),
            dcc.Input(
                id=input_id,
                type="number",
                placeholder=f"{left_text}...[{right_text}]",
                min=min,
                max=max,
                style={"width": width, "display": "block"},
                persistence=True,
                persistence_type="memory",   # oder "session"/"local"
            ),
            html.Span(right_text, style={"marginLeft": 5}) if right_text else html.Span()
        ],
        style={"display": "flex", "alignItems": "center", "marginBottom": "10px"},
    )

# Erzeugen der Checklisten inkl. Eingabefelder
def method_row(label, cb_id, input_id=None, value="", info=""):
    base_row_style = {
        "display": "flex",
        "alignItems": "center",
        "gap": "4px",
        "marginLeft": "10px",
        "marginBottom": "6px",
    }

    checkbox = dcc.Checklist(
        id=cb_id,
        options=[{"label": "", "value": "on"}],
        value=[],
        style={"margin": "0"},
        persistence=True,
        persistence_type="memory",
    )

    label_span = html.Span(
        label,
        style={
            "marginLeft": "4px",
            "minWidth": "130px",
        },
    )

    if not info:
        return html.Div([checkbox, label_span], style=base_row_style)

    input_box = dcc.Input(
        id=input_id,
        type="number",
        value=value,
        #placeholder=placeholder,
        style={
            "width": "90px",
            "marginLeft": "2px",
        },
        disabled=True,
        persistence=True,
        persistence_type="memory",
    )

    info_text = html.Span(
        info,
        style={
            "marginLeft": "6px",
            "fontSize": "12px",
            "color": "#6c757d",
            "lineHeight": "1.1",
            "whiteSpace": "nowrap",
        },
    )
    children = [checkbox, label_span, input_box]
    if info:
        children.append(info_text)

    return html.Div(children, style=base_row_style)

def method_failed(method_label: str, exc: Exception):
    return html.Div([
        html.Strong(f"{method_label}: "),
        html.Span("konnte nicht berechnet werden. ", style={"color": "red"}),
        #html.Span(f"({type(exc).__name__}: {exc})", style={"color": "#b02a37"}),

        html.Details([
            html.Summary("Details"),
            html.Pre(traceback.format_exc(), style={
                "whiteSpace": "pre-wrap",
                "fontSize": "12px",
                "color": "#6c757d",
                "marginTop": "6px"
            })
        ], style={"marginTop": "6px"})
    ])


def ellipsoid_figure(ell: EllipsoidTriaxial, title="Dreiachsiges Ellipsoid"):
    fig = go.Figure()

    # Darstellung
    rx, ry, rz = 1.05*ell.ax, 1.05*ell.ay, 1.05*ell.b
    fig.update_layout(
        title=title,
        scene=dict(
            xaxis=dict(range=[-rx, rx], title="X [m]"),
            yaxis=dict(range=[-ry, ry], title="Y [m]"),
            zaxis=dict(range=[-rz, rz], title="Z [m]"),
            aspectmode="data"
        ),
        margin=dict(l=0, r=0, t=10, b=0),
    )

    # Ellipsoid
    u = np.linspace(-np.pi/2, np.pi/2, 80)
    v = np.linspace(-np.pi,     np.pi,   160)
    U, V = np.meshgrid(u, v)
    X, Y, Z = ell.para2cart(U, V)
    fig.add_trace(go.Surface(
        x=X, y=Y, z=Z, showscale=False, opacity=0.7,
        surfacecolor=np.zeros_like(X),
        colorscale=[[0, "rgb(200,220,255)"], [1, "rgb(200,220,255)"]],
        name="Ellipsoid"
    ))

    return fig

def figure_constant_lines(fig, ell: EllipsoidTriaxial, coordsystem: str = "para"):
    if coordsystem == "para":
        constants_u = wu.deg2rad(np.arange(-90, 91, 15))
        all_v = wu.deg2rad(np.arange(-180, 180, 1))
        for u in constants_u:
            xm, ym, zm = ell.para2cart(u, all_v)
            fig.add_trace(go.Scatter3d(
                x=xm, y=ym, z=zm, mode="lines",
                line=dict(width=1, color="black"),
                showlegend=False
            ))

        all_u = wu.deg2rad(np.arange(-90, 91, 1))
        constants_v = wu.deg2rad(np.arange(-180, 180, 15))
        for v in constants_v:
            x, y, z = ell.para2cart(all_u, v)
            fig.add_trace(go.Scatter3d(
                x=x, y=y, z=z, mode="lines",
                line=dict(width=1, color="black"),
                showlegend=False
            ))

    elif coordsystem == "ell":
        constants_beta = wu.deg2rad(np.arange(-90, 91, 15))
        all_lamb = wu.deg2rad(np.arange(-180, 180, 1))
        for beta in constants_beta:
            xyz = ell.ell2cart(beta, all_lamb)
            fig.add_trace(go.Scatter3d(
                x=xyz[:, 0], y=xyz[:, 1], z=xyz[:, 2], mode="lines",
                line=dict(width=1, color="black"),
                showlegend=False
            ))

        all_beta = wu.deg2rad(np.arange(-90, 91, 1))
        all_beta[0] += 1e-8
        all_beta[-1] -= 1e-8
        constants_lamb = wu.deg2rad(np.arange(-180, 180, 15))
        for lamb in constants_lamb:
            if lamb != 0 and abs(lamb) != np.pi:
                xyz = ell.ell2cart(all_beta, lamb)
                fig.add_trace(go.Scatter3d(
                    x=xyz[:, 0], y=xyz[:, 1], z=xyz[:, 2], mode="lines",
                    line=dict(width=1, color="black"),
                    showlegend=False
                ))
            else:
                x, y, z = ell.para2cart(wu.deg2rad(np.arange(-90, 91, 1)), lamb)
                fig.add_trace(go.Scatter3d(
                    x=x, y=y, z=z, mode="lines",
                    line=dict(width=1, color="black"),
                    showlegend=False
                ))

    elif coordsystem == "geod":
        constants_phi = wu.deg2rad(np.arange(-90, 91, 15))
        all_lamb = wu.deg2rad(np.arange(-180, 180, 1))
        for phi in constants_phi:
            x, y, z = ell.geod2cart(phi, all_lamb, 0)
            fig.add_trace(go.Scatter3d(
                x=x, y=y, z=z, mode="lines",
                line=dict(width=1, color="black"),
                showlegend=False
            ))

        all_phi = wu.deg2rad(np.arange(-90, 91, 1))
        constants_lamb = wu.deg2rad(np.arange(-180, 180, 15))
        for lamb in constants_lamb:
            x, y, z = ell.geod2cart(all_phi, lamb, 0)
            fig.add_trace(go.Scatter3d(
                x=x, y=y, z=z, mode="lines",
                line=dict(width=1, color="black"),
                showlegend=False
            ))

    return fig


def figure_points(fig, points):
    """

    :param fig: plotly.graph_objects.Figure
    :param points: Punktliste [(name, (x,y,z), color)]
    :return: plotly.graph_objects.Figure
    """
    for name, (px, py, pz), color in points:
        fig.add_trace(go.Scatter3d(
            x=[px], y=[py], z=[pz],
            mode="markers+text",
            marker=dict(size=6, color=color),
            text=[name], textposition="top center",
            name=name, showlegend=False
        ))
    return fig

def figure_lines(fig, line, name, color):
    """

    :param fig: plotly.graph_objects.Figure
    :param line: Punktliste [[x1,y1,z1], [x2,y2,z2]]
    :param name: Name
    :param color: Farbe
    :return: plotly.graph_objects.Figure
    """

    points = np.array(line, dtype=float)
    fig.add_trace(go.Scatter3d(
        x=points[:, 0], y=points[:, 1], z=points[:, 2],
        mode="lines",
        line=dict(width=4, color=color),
        name=name, showlegend=False
    ))
    return fig

# HTML der beiden Tabs
# Tab 1
pane_gha1 = html.Div(
    [
        inputfeld("β₀", "input-GHA1-beta0", "°"),
        inputfeld("λ₀", "input-GHA1-lamb0", "°"),
        inputfeld("s", "input-GHA1-s", "m"),
        inputfeld("α₀", "input-GHA1-a", "°"),

        method_row("Analytisch", "cb-ana-1", "input-ana-1", "70", info="Ordnung"),
        method_row("Numerisch", "cb-num-1", "input-num-n-1", "2000", info="Anzahl Schritte"),
        method_row("Stochastisch (ES)", "cb-stoch-1", "input-stoch-n-1", "1000", info="Länge Streckensegment [m]"),
        method_row("Approximiert", "cb-approx-1", "input-approx-ds-1", "1000", info="Länge Streckensegment [m]"),


        html.Div(
            [
                html.Button(
                    "Berechnen",
                    id="button-calc-gha1",
                    n_clicks=0,
                    className="btn btn-secondary btn-lg shadow",
                    style={"marginRight": "10px", "marginLeft": "10px", "marginTop": "30px"},
                ),
            ],
            style={"marginBottom": "20px"},
        ),

        html.Div(id="tabs-GHA1-out", style={"marginBottom": "10px"}),

        html.Div(id="gha1-header"),
        dcc.Loading(html.Div(id="output-gha1-ana")),
        dcc.Loading(html.Div(id="output-gha1-num")),
        dcc.Loading(html.Div(id="output-gha1-stoch")),
        dcc.Loading(html.Div(id="output-gha1-approx")),

        dcc.Store(id="store-gha1-ana"),
        dcc.Store(id="store-gha1-num"),
        dcc.Store(id="store-gha1-stoch"),
        dcc.Store(id="store-gha1-approx"),
    ],
    id="pane-gha1",
    style={"display": "block"},
)

# Tab2
pane_gha2 = html.Div(
    [
        inputfeld("β₀", "input-GHA2-beta0", "°"),
        inputfeld("λ₀", "input-GHA2-lamb0", "°"),
        inputfeld("β₁", "input-GHA2-beta1", "°"),
        inputfeld("λ₁", "input-GHA2-lamb1", "°"),

        method_row("Numerisch", "cb-num-2", "input-num-n-2", "2000", info="Anzahl Schritte"),
        method_row("Stochastisch", "cb-stoch-2", "input-stoch-n-2", "1000", info="Länge Streckensegment [m]"),
        method_row("Approximiert", "cb-approx-2", "input-approx-ds-2", "1000", info="Länge Streckensegment [m]"),

        html.Div(
            [
                html.Button(
                    "Berechnen",
                    id="button-calc-gha2",
                    n_clicks=0,
                    className="btn btn-secondary btn-lg shadow",
                    style={"marginRight": "10px", "marginLeft": "10px", "marginTop": "30px"},
                ),
            ],
            style={"marginBottom": "20px"},
        ),

        html.Div(id="tabs-GHA2-out", style={"marginBottom": "10px"}),

        html.Div(id="gha2-header"),
        dcc.Loading(html.Div(id="output-gha2-num")),
        dcc.Loading(html.Div(id="output-gha2-stoch")),
        dcc.Loading(html.Div(id="output-gha2-approx")),

        dcc.Store(id="store-gha2-num"),
        dcc.Store(id="store-gha2-stoch"),
        dcc.Store(id="store-gha2-approx"),
    ],
    id="pane-gha2",
    style={"display": "none"},
)


# HTML-Gerüst
app.layout = html.Div(
    style={"fontFamily": "Arial", "padding": "10px", "width": "95%", "margin": "0 auto"},
    children=[
        html.H1("Geodätische Hauptaufgaben"),
        html.H2("für dreiachsige Ellipsoide"),

        html.Div(
            style={
                "display": "flex",
                "alignItems": "flex-start",
                "gap": "24px",
                "flexWrap": "nowrap"
            },
            children=[

                # Linker Bereich
                html.Div(
                    style={"flex": "1 1 420px", "maxWidth": "640px"},
                    children=[

                        html.Label("Ellipsoid wählen:"),
                        dcc.Dropdown(
                            id="dropdown-ellipsoid",
                            options=[
                                {"label": "BursaFialova1993", "value": "BursaFialova1993"},
                                {"label": "BursaSima1980", "value": "BursaSima1980"},
                                {"label": "BursaSima1980round", "value": "BursaSima1980round"},
                                {"label": "Eitschberger1978", "value": "Eitschberger1978"},
                                {"label": "Bursa1972", "value": "Bursa1972"},
                                {"label": "Bursa1970", "value": "Bursa1970"},
                                {"label": "BesselBiaxial", "value": "BesselBiaxial"},
                                {"label": "KarneyTest2024", "value": "KarneyTest2024"},
                                {"label": "Fiction", "value": "Fiction"},

                            ],
                            value="",
                            style={"width": "300px", "marginBottom": "16px"},
                        ),

                        html.P("Halbachsen:", style={"marginBottom": "10px"}),
                        inputfeld("aₓ", "input-ax", "m"),
                        inputfeld("aᵧ", "input-ay", "m"),
                        inputfeld("b", "input-b", "m"),
                        html.Div(id="axes-error", style={"marginLeft": "10px", "marginTop": "5px", "marginBottom": "10px"}),

                        #html.Br(),

                        dcc.Tabs(
                            id="tabs-GHA",
                            value="tab-GHA1",
                            style={"margin": "20px 0 15px", "width": "100%"},
                            children=[
                                dcc.Tab(label="Erste Hauptaufgabe", value="tab-GHA1"),
                                dcc.Tab(label="Zweite Hauptaufgabe", value="tab-GHA2"),
                            ],
                        ),

                        html.Div([pane_gha1, pane_gha2], id="tabs-GHA-out", style={"marginBottom": "10px"}),

                    ],
                ),

                # Rechter Bereich
                html.Div(
                    style={
                        "flex": "1 1 750px",
                        "minWidth": "520px",
                        "position": "sticky",
                        "top": "0",
                        "marginTop": "-50px",
                    },
                    children=[
                        html.Label("Koordinatenart wählen:", style={"marginLeft": "80px"},),
                        dcc.Dropdown(
                            id="dropdown-coors-type",
                            options=[
                                {"label": "Ellipsoidisch", "value": "ell"},
                                {"label": "Parametrisch", "value": "para"},
                                {"label": "Geodätisch", "value": "geod"},

                            ],
                            value="ell",
                            clearable=False,
                            style={"width": "200px", "marginLeft": "80px"},
                        ),
                        dcc.Graph(
                            id="ellipsoid-plot",
                            style={"height": "85vh", "width": "100%"},
                            config={"responsive": True}
                        )
                    ],
                ),
            ],
        ),
        dcc.Store(id="calc-token-gha1", data=0),
        dcc.Store(id="calc-token-gha2", data=0),

        #html.P("© 2026", style={"fontSize": "10px", "color": "gray", "textAlign": "center", "marginTop": "16px"}),
    ],


)



# Funktion zur Wahl der Halbachsen
@app.callback(
    Output("input-ax", "value"),
    Output("input-ay", "value"),
    Output("input-b", "value"),
    Input("dropdown-ellipsoid", "value"),
)
def fill_inputs_from_dropdown(selected_ell):
    if not selected_ell:
        return no_update, no_update, no_update

    ell = EllipsoidTriaxial.init_name(selected_ell)
    return ell.ax, ell.ay, ell.b

# Funktion zur Generierung der Tab-Inhalte
@app.callback(
    Output("pane-gha1", "style"),
    Output("pane-gha2", "style"),
    Input("tabs-GHA", "value"),
)
def switch_tabs(tab):
    show1 = {"display": "block"} if tab == "tab-GHA1" else {"display": "none"}
    show2 = {"display": "block"} if tab == "tab-GHA2" else {"display": "none"}
    return show1, show2

# Funktionen zum Aktivieren der Eingabefelder innerhalb der Checklisten
@app.callback(
    Output("input-ana-1", "disabled"),
    Input("cb-ana-1", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])

@app.callback(
    Output("input-num-n-1", "disabled"),
    Input("cb-num-1", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])

@app.callback(
    Output("input-stoch-n-1", "disabled"),
    Input("cb-stoch-1", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])

@app.callback(
    Output("input-approx-ds-1", "disabled"),
    Input("cb-approx-1", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])


@app.callback(
    Output("input-num-n-2", "disabled"),
    Input("cb-num-2", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])

@app.callback(
    Output("input-stoch-n-2", "disabled"),
    Input("cb-stoch-2", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])

@app.callback(
    Output("input-approx-ds-2", "disabled"),
    Input("cb-approx-2", "value"),
)
def toggle_ds(v):
    return "on" not in (v or [])



# Abfrage ob Berechnungsverfahren gewählt
@app.callback(
    Output("tabs-GHA1-out", "children"),
    Input("button-calc-gha1", "n_clicks"),
    State("cb-ana-1", "value"),
    State("cb-num-1", "value"),
    State("cb-stoch-1", "value"),
    State("cb-approx-1", "value"),
    # Eingaben GHA1
    State("input-GHA1-beta0", "value"),
    State("input-GHA1-lamb0", "value"),
    State("input-GHA1-s", "value"),
    State("input-GHA1-a", "value"),
    # Halbachsen
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def gha1_method_hint(n, a, nu, st, ap, beta0, lamb0, s, a0, ax, ay, b):
    # Ellipsoid
    if ax is None or ay is None or b is None:
        return html.Span("Bitte Ellipsoid wählen bzw. Halbachsen eingeben.", style={"color": "red"})

    # Halbachsen
    try:
        ax_f, ay_f, b_f = float(ax), float(ay), float(b)
    except (TypeError, ValueError):
        return html.Span("Bitte gültige Zahlen für aₓ, aᵧ und b eingeben.", style={"color": "red"})

    if ax_f <= 0 or ay_f <= 0 or b_f <= 0:
        return html.Span("Halbachsen müssen > 0 sein.", style={"color": "red"})

    if not (ax_f >= ay_f >= b_f):
        return html.Span("Ungültige Halbachsen! (aₓ ≥ aᵧ ≥ b)", style={"color": "red"})

    # GHA1-Eingabefelder prüfen
    missing = []
    if beta0 is None:
        missing.append("β₀")
    if lamb0 is None:
        missing.append("λ₀")
    if s is None:
        missing.append("s")
    if a0 is None:
        missing.append("α₀")

    if missing:
        return html.Span(
            "Bitte " + ", ".join(missing) + " eingeben.",
            style={"color": "red"},
        )

    # Berechnungsverfahren
    any_on = any("on" in (v or []) for v in (a, nu, st, ap))
    if not any_on:
        return html.Span("Bitte Berechnungsverfahren wählen.", style={"color": "red"})

    # Eingaben Wertebereiche
    try:
        beta0_f = float(beta0)
        lamb0_f = float(lamb0)
        s_f = float(s)
        a0_f = float(a0)
    except (TypeError, ValueError):
        return html.Span("Bitte gültige Zahlen für β₀, λ₀, s und α₀ eingeben.", style={"color": "red"})

    if not (-90 <= beta0_f <= 90):
        return html.Span("β₀ muss im Bereich [-90°, 90°] liegen.", style={"color": "red"})
    if not (-180 <= lamb0_f <= 180):
        return html.Span("λ₀ muss im Bereich [-180°, 180°] liegen.", style={"color": "red"})
    if s_f <= 0:
        return html.Span("s muss > 0 sein.", style={"color": "red"})
    if not (0 <= a0_f <= 360):
        return html.Span("α₀ muss im Bereich [0°, 360°] liegen.", style={"color": "red"})

    return ""


# -- GHA 1 ---
@app.callback(
    Output("output-gha1-ana", "children"),
    Output("store-gha1-ana", "data"),
    Input("calc-token-gha1", "data"),
    State("cb-ana-1", "value"),
    State("input-ana-1", "value"),
    State("input-GHA1-beta0", "value"),
    State("input-GHA1-lamb0", "value"),
    State("input-GHA1-s", "value"),
    State("input-GHA1-a", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha1_ana(n1, cb_ana, n_in, beta0, lamb0, s, a0, ax, ay, b):
    if not n1:
        return no_update, no_update
    if "on" not in (cb_ana or []):
        return "", None

    n_in = int(n_in) if n_in else 70

    try:
        ell = EllipsoidTriaxial(ax, ay, b)
        beta_rad = wu.deg2rad(float(beta0))
        lamb_rad = wu.deg2rad(float(lamb0))
        alpha_rad = wu.deg2rad(float(a0))
        _, _, alpha_rad_para = alpha_ell2para(ell, beta_rad, lamb_rad, alpha_rad)
        s_val = float(s)

        P0 = ell.ell2cart(beta_rad, lamb_rad)
        P1_ana, alpha2_para = gha1_ana(ell, P0, alpha_rad_para, s_val, n_in)
        u1, v1 = ell.cart2para(P1_ana)
        alpha2 = alpha_para2ell(ell, u1, v1, alpha2_para)
        beta2_ana, lamb2_ana = ell.cart2ell(P1_ana)

        out = html.Div([
            html.Strong("Analytisch: "),
            html.Br(),
            html.Span(f"kartesisch: x₁={P1_ana[0]:.4f} m, y₁={P1_ana[1]:.4f} m, z₁={P1_ana[2]:.4f} m"),
            html.Br(),
            html.Span(f"ellipsoidisch: {aus.gms('β₁', beta2_ana, 4)}, {aus.gms('λ₁', lamb2_ana, 4)}"),
            html.Br(),
        ])

        store = {
            "points": [("P0", P0, "black"), ("P1 (ana)", P1_ana, "red")],
            "polyline": None,
            "name": "Analytisch",
            "color": "red"
        }
        return out, store

    except Exception as e:
        return method_failed("Analytisch", e), None

@app.callback(
    Output("output-gha1-num", "children"),
    Output("store-gha1-num", "data"),
    Input("calc-token-gha1", "data"),
    State("cb-num-1", "value"),
    State("input-num-n-1", "value"),
    State("input-GHA1-beta0", "value"),
    State("input-GHA1-lamb0", "value"),
    State("input-GHA1-s", "value"),
    State("input-GHA1-a", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha1_num(n1, cb_num, n_in, beta0, lamb0, s, a0, ax, ay, b):
    if not n1:
        return no_update, no_update
    if  "on" not in (cb_num or []):
        return "", None

    n_in = int(n_in) if n_in else 2000

    ell = EllipsoidTriaxial(ax, ay, b)
    beta_rad = wu.deg2rad(float(beta0))
    lamb_rad = wu.deg2rad(float(lamb0))
    alpha_rad = wu.deg2rad(float(a0))
    s_val = float(s)

    try:
        P0 = ell.ell2cart(beta_rad, lamb_rad)

        P1_num, alpha1, werte = gha1_num(ell, P0, alpha_rad, s_val, n_in, all_points=True)
        beta2_num, lamb2_num = ell.cart2ell(P1_num)

        out = html.Div([
            html.Strong("Numerisch: "),
            html.Br(),
            html.Span(f"kartesisch: x₁={P1_num[0]:.4f} m, y₁={P1_num[1]:.4f} m, z₁={P1_num[2]:.4f} m"),
            html.Br(),
            html.Span(f"ellipsoidisch: {aus.gms('β₁', beta2_num, 4)}, {aus.gms('λ₁', lamb2_num, 4)}"),
            html.Br(),
        ])

        polyline = [[x1, y1, z1] for x1, _, y1, _, z1, _ in werte]

        store = {
            "points": [("P0", P0, "black"), ("P1 (num)", P1_num, "#ff8c00")],
            "polyline": polyline,
            "name": "Numerisch",
            "color": "#ff8c00"
        }
        return out, store

    except Exception as e:
        return method_failed("Numerisch", e), None

@app.callback(
    Output("output-gha1-stoch", "children"),
    Output("store-gha1-stoch", "data"),
    Input("calc-token-gha1", "data"),
    State("cb-stoch-1", "value"),
    State("input-stoch-n-1", "value"),
    State("input-GHA1-beta0", "value"),
    State("input-GHA1-lamb0", "value"),
    State("input-GHA1-s", "value"),
    State("input-GHA1-a", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha1_stoch(n1, cb_stoch, n_in, beta0, lamb0, s, a0, ax, ay, b):
    if not n1:
        return no_update, no_update
    if "on" not in (cb_stoch or []):
        return "", None

    n_in = float(n_in) if n_in else 1000

    try:
        ell = EllipsoidTriaxial(ax, ay, b)
        beta_rad = wu.deg2rad(float(beta0))
        lamb_rad = wu.deg2rad(float(lamb0))
        alpha_rad = wu.deg2rad(float(a0))
        s_val = float(s)

        P1_stoch, alpha, points = gha1_ES(ell, beta0=beta_rad, omega0=lamb_rad, alpha0=alpha_rad, s_total=s_val, maxSegLen=n_in, all_points=True)

        P0 = ell.ell2cart(beta_rad, lamb_rad)
        beta1_stoch, lamb1_stoch = ell.cart2ell(P1_stoch)

        out = html.Div([
            html.Strong("Stochastisch: "),
            html.Br(),
            html.Span(f"kartesisch: x₁={P1_stoch[0]:.4f} m, y₁={P1_stoch[1]:.4f} m, z₁={P1_stoch[2]:.4f} m"),
            html.Br(),
            html.Span(f"ellipsoidisch: {aus.gms('β₁', beta1_stoch, 4)}, {aus.gms('λ₁', lamb1_stoch, 4)}"),
        ])

        store = {
            "points": [("P0", P0, "black"), ("P1 (ES)", P1_stoch, "#1fa342")],
            "polyline": points,
            "name": "Stochastisch (ES)",
            "color": "#1fa342"
        }
        return out, store

    except Exception as e:
        return method_failed("Stochastisch (ES)", e), None

@app.callback(
    Output("output-gha1-approx", "children"),
    Output("store-gha1-approx", "data"),
    Input("calc-token-gha1", "data"),
    State("cb-approx-1", "value"),
    State("input-approx-ds-1", "value"),
    State("input-GHA1-beta0", "value"),
    State("input-GHA1-lamb0", "value"),
    State("input-GHA1-s", "value"),
    State("input-GHA1-a", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha1_approx(n1, cb_approx, ds_in, beta0, lamb0, s, a0, ax, ay, b):
    if not n1:
        return no_update, no_update
    if not n1 or "on" not in (cb_approx or []):
        return "", None

    ds_in = float(ds_in) if ds_in else 1000

    try:
        ell = EllipsoidTriaxial(ax, ay, b)
        beta_rad = wu.deg2rad(float(beta0))
        lamb_rad = wu.deg2rad(float(lamb0))
        alpha_rad = wu.deg2rad(float(a0))
        s_val = float(s)

        P0 = ell.ell2cart(beta_rad, lamb_rad)
        P1_app, alpha1_app, points, alphas = gha1_approx(ell, P0, alpha_rad, s_val, ds=ds_in, all_points=True)

        beta1_app, lamb1_app = ell.cart2ell(P1_app)

        out = html.Div([
            html.Strong("Approximiert: "),
            html.Br(),
            html.Span(f"kartesisch: x₁={P1_app[0]:.4f} m, y₁={P1_app[1]:.4f} m, z₁={P1_app[2]:.4f} m"),
            html.Br(),
            html.Span(f"ellipsoidisch: {aus.gms('β₁', beta1_app, 4)}, {aus.gms('λ₁', lamb1_app, 4)}"),
        ])

        store = {
            "points": [("P0", P0, "black"), ("P1 (approx)", P1_app, "#00c2fc")],
            "polyline": points,
            "name": "Approximiert",
            "color": "#00c2fc"
        }
        return out, store

    except Exception as e:
        return method_failed("Approximiert", e), None


# --- GHA 2 ---
@app.callback(
    Output("output-gha2-num", "children"),
    Output("store-gha2-num", "data"),
    Input("calc-token-gha2", "data"),
    State("cb-num-2", "value"),
    State("input-num-n-2", "value"),
    State("input-GHA2-beta0", "value"),
    State("input-GHA2-lamb0", "value"),
    State("input-GHA2-beta1", "value"),
    State("input-GHA2-lamb1", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha2_num(n2, cb_num, n_in, beta0, lamb0, beta1, lamb1, ax, ay, b):
    if not n2:
        return no_update, no_update
    if "on" not in (cb_num or []):
        return "", None

    n_in = int(n_in) if n_in else 2000

    try:
        ell = EllipsoidTriaxial(ax, ay, b)

        beta0_rad = wu.deg2rad(float(beta0))
        lamb0_rad = wu.deg2rad(float(lamb0))
        beta1_rad = wu.deg2rad(float(beta1))
        lamb1_rad = wu.deg2rad(float(lamb1))

        P0 = ell.ell2cart(beta0_rad, lamb0_rad)
        P1 = ell.ell2cart(beta1_rad, lamb1_rad)

        a0_num, a1_num, s_num, beta_arr, lamb_arr = gha2_num(ell, beta0_rad, lamb0_rad, beta1_rad, lamb1_rad, all_points=True, n=n_in)

        polyline = []
        for b_rad, l_rad in zip(beta_arr, lamb_arr):
            x, y, z = ell.ell2cart(b_rad, l_rad)
            polyline.append([float(x), float(y), float(z)])

        out = html.Div([
            html.Strong("Numerisch: "),
            html.Span(f"{aus.gms('α₀', a0_num, 4)}, {aus.gms('α₁', a1_num, 4)}, s = {s_num:.4f} m"),
        ])

        store = {
            "points": [("P0", P0, "black"), ("P1", P1, "black")],
            "polyline": polyline,
            "name": "Numerisch",
            "color": "#ff8c00",
        }
        return out, store

    except Exception as e:
        return method_failed("Numerisch", e), None

@app.callback(
    Output("output-gha2-stoch", "children"),
    Output("store-gha2-stoch", "data"),
    Input("calc-token-gha2", "data"),
    State("cb-stoch-2", "value"),
    State("input-stoch-n-2", "value"),
    State("input-GHA2-beta0", "value"),
    State("input-GHA2-lamb0", "value"),
    State("input-GHA2-beta1", "value"),
    State("input-GHA2-lamb1", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha2_stoch(n2, cb_stoch, n_in, beta0, lamb0, beta1, lamb1, ax, ay, b):
    if not n2:
        return no_update, no_update
    if "on" not in (cb_stoch or []):
        return "", None

    n_in = float(n_in) if n_in else 1000

    try:
        ell = EllipsoidTriaxial(ax, ay, b)

        beta0_rad = wu.deg2rad(float(beta0))
        lamb0_rad = wu.deg2rad(float(lamb0))
        beta1_rad = wu.deg2rad(float(beta1))
        lamb1_rad = wu.deg2rad(float(lamb1))

        P0 = ell.ell2cart(beta0_rad, lamb0_rad)
        P1 = ell.ell2cart(beta1_rad, lamb1_rad)

        a0_stoch, a1_stoch, s_stoch, points = gha2_ES(ell, P0, P1, maxSegLen=n_in, all_points=True)

        out = html.Div([
            html.Strong("Stochastisch (ES): "),
            html.Span(f"{aus.gms('α₀', a0_stoch, 4)}, {aus.gms('α₁', a1_stoch, 4)}, s = {s_stoch:.4f} m"),
        ])

        store = {
            "points": [("P0", P0, "black"), ("P1", P1, "black")],
            "polyline": points,
            "name": "Stochastisch (ES)",
            "color": "#1fa342",
        }

        return out, store

    except Exception as e:
        return method_failed("Stochastisch (ES)", e), None

@app.callback(
    Output("output-gha2-approx", "children"),
    Output("store-gha2-approx", "data"),
    Input("calc-token-gha2", "data"),
    State("cb-approx-2", "value"),
    State("input-approx-ds-2", "value"),
    State("input-GHA2-beta0", "value"),
    State("input-GHA2-lamb0", "value"),
    State("input-GHA2-beta1", "value"),
    State("input-GHA2-lamb1", "value"),
    State("input-ax", "value"),
    State("input-ay", "value"),
    State("input-b", "value"),
    prevent_initial_call=True,
)
def compute_gha2_approx(n2, cb_approx, ds_in, beta0, lamb0, beta1, lamb1, ax, ay, b):
    if not n2:
        return no_update, no_update
    if "on" not in (cb_approx or []):
        return "", None

    ds_in = float(ds_in) if ds_in else 1000

    try:
        ell = EllipsoidTriaxial(ax, ay, b)

        beta0_rad = wu.deg2rad(float(beta0))
        lamb0_rad = wu.deg2rad(float(lamb0))
        beta1_rad = wu.deg2rad(float(beta1))
        lamb1_rad = wu.deg2rad(float(lamb1))

        P0 = ell.ell2cart(beta0_rad, lamb0_rad)
        P1 = ell.ell2cart(beta1_rad, lamb1_rad)

        a0_app, a1_app, s_app, points = gha2_approx(ell, P0, P1, ds=ds_in, all_points=True)

        out = html.Div([
            html.Strong("Approximiert: "),
            html.Span(f"{aus.gms('α₀', a0_app, 4)}, {aus.gms('α₁', a1_app, 4)}, s = {s_app:.4f} m"),
        ])

        store = {
            "points": [("P0", P0, "black"), ("P1", P1, "black")],
            "polyline": points,
            "name": "Approximiert",
            "color": "#00c2fc",
        }

        return out, store

    except Exception as e:
        return method_failed("Stochastisch (ES)", e), None

# --- Plot ---
@app.callback(
    Output("ellipsoid-plot", "figure"),
    Input("input-ax", "value"),
    Input("input-ay", "value"),
    Input("input-b", "value"),
    Input("dropdown-coors-type", "value"),
    Input("tabs-GHA", "value"),
    Input("calc-token-gha1", "data"),
    Input("calc-token-gha2", "data"),
    Input("store-gha1-ana", "data"),
    Input("store-gha1-num", "data"),
    Input("store-gha1-stoch", "data"),
    Input("store-gha1-approx", "data"),
    Input("store-gha2-num", "data"),
    Input("store-gha2-stoch", "data"),
    Input("store-gha2-approx", "data"),
)
def render_all(ax, ay, b, coords_type, tab, t1, t2,
               s1a, s1n, s1s, s1p, s2n, s2s, s2p):

    if None in (ax, ay, b):
        return go.Figure()

    try:
        ax = float(ax); ay = float(ay); b = float(b)
    except (TypeError, ValueError):
        return go.Figure()

    if ax <= 0 or ay <= 0 or b <= 0:
        return go.Figure()

    if not (ax >= ay >= b):
        return go.Figure()

    ell = EllipsoidTriaxial(ax, ay, b)

    fig = ellipsoid_figure(ell, title="")
    fig = figure_constant_lines(fig, ell, coords_type)

    legend_added = set()

    def add_legend_for_store(store):
        nonlocal fig
        if not store:
            return
        name = store.get("name")
        color = store.get("color")
        if not name or not color:
            return

        key = (name, color)
        if key in legend_added:
            return
        legend_added.add(key)

        has_line = bool(store.get("polyline"))

        if has_line:
            fig.add_trace(go.Scatter3d(
                x=[None], y=[None], z=[None],
                mode="lines",
                line=dict(width=6, color=color),
                name=name,
                showlegend=True,
                hoverinfo="skip",
            ))
        else:
            fig.add_trace(go.Scatter3d(
                x=[None], y=[None], z=[None],
                mode="markers",
                marker=dict(size=8, color=color),
                name=name,
                showlegend=True,
                hoverinfo="skip",
            ))

    def add_from_store(store):
        nonlocal fig
        if not store:
            return

        pts = store.get("points") or []
        if pts:
            fig = figure_points(fig, pts)

        line = store.get("polyline")
        if line:
            fig = figure_lines(
                fig,
                line,
                store.get("name", ""),
                store.get("color", "#ff8c00"),
            )

    if tab == "tab-GHA1":
        stores = (s1a, s1n, s1s, s1p)
    else:
        stores = (s2n, s2s, s2p)

    for st in stores:
        add_legend_for_store(st)
        add_from_store(st)

    fig.update_layout(
        showlegend=True,
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.02,
            xanchor="left",
            x=0.06,
        ),
    )

    return fig


# Funktion zum Leeren des Plots bei Änderung des Ellipsoids
@app.callback(
    Output("store-gha1-ana", "data", allow_duplicate=True),
    Output("store-gha1-num", "data", allow_duplicate=True),
    Output("store-gha1-stoch", "data", allow_duplicate=True),
    Output("store-gha1-approx", "data", allow_duplicate=True),
    Output("store-gha2-num", "data", allow_duplicate=True),
    Output("store-gha2-stoch", "data", allow_duplicate=True),
    Output("store-gha2-approx", "data", allow_duplicate=True),
    Input("input-ax", "value"),
    Input("input-ay", "value"),
    Input("input-b", "value"),
    prevent_initial_call=True,
)
def clear_all_stores_on_ellipsoid_change(ax, ay, b):
    if None in (ax, ay, b):
        return (no_update,)*7

    return (None, None, None, None, None, None, None)


# Funktionen zur separaten Darstellung der Tabs
@app.callback(
    Output("calc-token-gha1", "data"),
    Output("gha1-header", "children", allow_duplicate=True),
    Output("output-gha1-ana", "children", allow_duplicate=True),
    Output("output-gha1-num", "children", allow_duplicate=True),
    Output("output-gha1-stoch", "children", allow_duplicate=True),
    Output("output-gha1-approx", "children", allow_duplicate=True),
    Output("store-gha1-ana", "data", allow_duplicate=True),
    Output("store-gha1-num", "data", allow_duplicate=True),
    Output("store-gha1-stoch", "data", allow_duplicate=True),
    Output("store-gha1-approx", "data", allow_duplicate=True),
    Input("button-calc-gha1", "n_clicks"),
    State("calc-token-gha1", "data"),
    prevent_initial_call=True,
)
def start_calc_gha1(n, token):
    if not n:
        raise PreventUpdate
    token = (token or 0) + 1
    return token, "", "", "", "", "", None, None, None, None

@app.callback(
    Output("calc-token-gha2", "data"),
    Output("gha2-header", "children", allow_duplicate=True),
    Output("output-gha2-num", "children", allow_duplicate=True),
    Output("output-gha2-stoch", "children", allow_duplicate=True),
    Output("output-gha2-approx", "children", allow_duplicate=True),
    Output("store-gha2-num", "data", allow_duplicate=True),
    Output("store-gha2-stoch", "data", allow_duplicate=True),
    Output("store-gha2-approx", "data", allow_duplicate=True),
    Input("button-calc-gha2", "n_clicks"),
    State("calc-token-gha2", "data"),
    prevent_initial_call=True,
)
def start_calc_gha2(n, token):
    if not n:
        raise PreventUpdate
    token = (token or 0) + 1
    return token, "", "", "", "", None, None, None



# Funktionen zur Erzeugung der Überschriften
@app.callback(
    Output("gha1-header", "children"),
    Input("calc-token-gha1", "data"),
    prevent_initial_call=True,
)
def set_gha1_header(_):
    return html.H4("Erste Hauptaufgabe")

@app.callback(
    Output("gha2-header", "children"),
    Input("calc-token-gha2", "data"),
    prevent_initial_call=True,
)
def set_gha2_header(_):
    return html.H4("Zweite Hauptaufgabe")

# Funktion zur Überprüfung der Halbachsen
@app.callback(
    Output("axes-error", "children"),
    Output("button-calc-gha1", "disabled"),
    Output("button-calc-gha2", "disabled"),
    Input("input-ax", "value"),
    Input("input-ay", "value"),
    Input("input-b", "value"),
)
def validate_axes(ax, ay, b):
    if ax is None or ay is None or b is None:
        return "", True, True

    try:
        ax = float(ax)
        ay = float(ay)
        b = float(b)
    except (TypeError, ValueError):
        return html.Span("Bitte gültige Zahlen für aₓ, aᵧ und b eingeben.", style={"color": "red"}), True, True

    if ax <= 0 or ay <= 0 or b <= 0:
        return html.Span("Halbachsen müssen > 0 sein.", style={"color": "red"}), True, True

    if not (ax >= ay >= b):
        return html.Span("Ungültige Halbachsen! (aₓ ≥ aᵧ ≥ b)", style={"color": "red"}), True, True

    return "", False, False

# Leeren des Dropdowns
@app.callback(
    Output("dropdown-ellipsoid", "value"),
    Input("input-ax", "value"),
    Input("input-ay", "value"),
    Input("input-b", "value"),
    State("dropdown-ellipsoid", "value"),
    prevent_initial_call=True,
)
def clear_dropdown_if_axes_changed(ax, ay, b, selected_ell):
    if not selected_ell:
        return no_update

    if ax is None or ay is None or b is None:
        return ""

    try:
        ax = float(ax); ay = float(ay); b = float(b)
        ell = EllipsoidTriaxial.init_name(selected_ell)
        same = (
            np.isclose(ax, float(ell.ax), rtol=0, atol=1e-9) and
            np.isclose(ay, float(ell.ay), rtol=0, atol=1e-9) and
            np.isclose(b,  float(ell.b),  rtol=0, atol=1e-9)
        )
    except Exception:
        return ""

    return no_update if same else ""


if __name__ == "__main__":
    # Automatisiertes Öffnen der Seite im Browser
    HOST = "127.0.0.1"
    PORT = 8050
    #Timer(1.0, webbrowser.open_new_tab(f"http://{HOST}:{PORT}/")).start

    app.run(host=HOST, port=PORT, debug=False)
