import numpy as np
import plotly.graph_objects as go
from scipy.stats import f
import pandas as pd
import Berechnungen


class Genauigkeitsmaße:


    @staticmethod
    def berechne_s0apost(v: np.ndarray, P: np.ndarray, r: int) -> float:
        vTPv_matrix = v.T @ P @ v
        vTPv = float(vTPv_matrix.item())
        s0apost = np.sqrt(vTPv / r)
        return float(s0apost)


    @staticmethod
    def helmert_punktfehler(Qxx, s0_apost, unbekannten_liste, dim=3):
        diagQ = np.diag(Qxx)
        daten = []
        namen_str = [str(sym) for sym in unbekannten_liste]

        punkt_ids = []
        for n in namen_str:
            if n.upper().startswith('X'):
                punkt_ids.append(n[1:])

        for pid in punkt_ids:
            try:
                idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
                idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())

                qx = diagQ[idx_x]
                qy = diagQ[idx_y]
                qz = 0.0

                if dim == 3:
                    try:
                        idx_z = next(i for i, n in enumerate(namen_str) if n.upper() == f"Z{pid}".upper())
                        qz = diagQ[idx_z]
                    except StopIteration:
                        qz = 0.0

                sx = s0_apost * np.sqrt(qx)
                sy = s0_apost * np.sqrt(qy)
                sz = s0_apost * np.sqrt(qz) if dim == 3 else 0

                sP = s0_apost * np.sqrt(qx + qy + qz)

                daten.append([pid, float(sx), float(sy), float(sz), float(sP)])
            except:
                continue

        helmert_punktfehler = pd.DataFrame(daten, columns=["Punkt", "σx", "σy", "σz", f"σP_{dim}D"])
        return helmert_punktfehler



    @staticmethod
    def standardellipse(Qxx, s0_apost, unbekannten_liste):
        Qxx = np.asarray(Qxx, float)
        daten = []
        namen_str = [str(sym) for sym in unbekannten_liste]

        punkt_ids = []
        for n in namen_str:
            if n.upper().startswith('X'):
                punkt_ids.append(n[1:])

        for pid in punkt_ids:
            try:
                idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
                idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())

                qxx = Qxx[idx_x, idx_x]
                qyy = Qxx[idx_y, idx_y]
                qyx = Qxx[idx_y, idx_x]

                # Standardabweichungen
                sx = s0_apost * np.sqrt(qxx)
                sy = s0_apost * np.sqrt(qyy)
                sxy = (s0_apost ** 2) * qyx

                k = np.sqrt((qxx - qyy) ** 2 + 4 * (qyx ** 2))

                # Q_dmax/min = 0.5 * (Qyy + Qxx +/- k)
                q_dmax = 0.5 * (qyy + qxx + k)
                q_dmin = 0.5 * (qyy + qxx - k)

                # Halbachsen
                s_max = s0_apost * np.sqrt(q_dmax)
                s_min = s0_apost * np.sqrt(q_dmin)

                # Richtungswinkel theta in gon:
                zaehler = 2 * qyx
                nenner = qxx - qyy
                t_grund = 0.5 * np.arctan(abs(zaehler) / abs(nenner)) * (200 / np.pi)

                # Quadrantenabfrage
                if nenner > 0 and qyx > 0:  # Qxx - Qyy > 0 und Qyx > 0
                    t_gon = t_grund  # 0 - 50 gon
                elif nenner < 0 and qyx > 0:  # Qxx - Qyy < 0 und Qyx > 0
                    t_gon = 100 - t_grund  # 50 - 100 gon
                elif nenner < 0 and qyx < 0:  # Qxx - Qyy < 0 und Qyx < 0
                    t_gon = 100 + t_grund  # 100 - 150 gon
                elif nenner > 0 and qyx < 0:  # Qxx - Qyy > 0 und Qyx < 0
                    t_gon = 200 - t_grund  # 150 - 200 gon
                else:
                    t_gon = 0.0

                daten.append([
                    pid,
                    float(sx), float(sy), float(sxy),
                    float(s_max), float(s_min),
                    float(t_gon)
                ])
            except:
                continue
            standardellipse = pd.DataFrame(daten, columns=["Punkt", "σx [m]", "σy [m]", "σxy [m]", "Große Halbachse [m]", "Kleine Halbachse [m]", "θ [gon]"])
        return standardellipse



    @staticmethod
    def konfidenzellipse(Qxx, s0_apost, unbekannten_liste, R, alpha):
        Qxx = np.asarray(Qxx, float)
        daten = []
        namen_str = [str(sym) for sym in unbekannten_liste]

        punkt_ids = [n[1:] for n in namen_str if n.upper().startswith('X')]

        # Faktor für Konfidenzellipse (F-Verteilung)
        kk = float(np.sqrt(2.0 * f.ppf(1.0 - alpha, 2, R)))

        for pid in punkt_ids:
            try:
                idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
                idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())

                qxx = Qxx[idx_x, idx_x]
                qyy = Qxx[idx_y, idx_y]
                qyx = Qxx[idx_y, idx_x]

                # Standardabweichungen
                sx = s0_apost * np.sqrt(qxx)
                sy = s0_apost * np.sqrt(qyy)
                sxy = (s0_apost ** 2) * qyx

                k = np.sqrt((qxx - qyy) ** 2 + 4 * (qyx ** 2))

                # Q_dmax/min = 0.5 * (Qyy + Qxx +/- k)
                q_dmax = 0.5 * (qyy + qxx + k)
                q_dmin = 0.5 * (qyy + qxx - k)

                # Halbachsen der Standardellipse
                s_max = s0_apost * np.sqrt(q_dmax)
                s_min = s0_apost * np.sqrt(q_dmin)

                # Halbachsen der Konfidenzellipse
                A_K = kk * s_max
                B_K = kk * s_min

                # Richtungswinkel theta in gon:
                zaehler = 2 * qyx
                nenner = qxx - qyy
                t_grund = 0.5 * np.arctan(abs(zaehler) / abs(nenner)) * (200 / np.pi)

                # Quadrantenabfrage
                if nenner > 0 and qyx > 0:
                    t_gon = t_grund  # 0 - 50 gon
                elif nenner < 0 and qyx > 0:
                    t_gon = 100 - t_grund  # 50 - 100 gon
                elif nenner < 0 and qyx < 0:
                    t_gon = 100 + t_grund  # 100 - 150 gon
                elif nenner > 0 and qyx < 0:
                    t_gon = 200 - t_grund  # 150 - 200 gon
                else:
                    t_gon = 0.0

                daten.append([
                    pid,
                    float(sx), float(sy), float(sxy),
                    float(A_K), float(B_K),
                    float(t_gon)
                ])

            except:
                continue

        konfidenzellipse = pd.DataFrame(daten, columns=["Punkt", "σx [m]", "σy [m]", "σxy [m]", "Große Halbachse [m]",
                                                        "Kleine Halbachse [m]", "θ [gon]"])
        return konfidenzellipse



class Plot:


    @staticmethod
    def netzplot_ellipsen(
            Koord_ENU,
            unbekannten_labels,
            beobachtungs_labels,
            df_konf_ellipsen_enu,
            v_faktor=1000,
            n_ellipse_pts=60,
            title="Netzplot im ENU-System mit Konfidenzellipsen"
    ):
        names = [str(s).strip() for s in unbekannten_labels]

        if "θ_EN [gon]" in df_konf_ellipsen_enu.columns:
            theta_col = "θ_EN [gon]"
        elif "θ [gon]" in df_konf_ellipsen_enu.columns:
            theta_col = "θ [gon]"
        else:
            raise ValueError("Spalte 'θ_EN [gon]' oder 'θ [gon]' fehlt im DataFrame.")

        punkt_ids = sorted({nm[1:] for nm in names if nm and nm[0].upper() in ("X", "Y", "Z")})

        fig = go.Figure()

        # 1) Darstellungen der Beobachtungen
        beob_typen = {
            'GNSS-Basislinien': {'pattern': 'gnss', 'color': 'rgba(255, 100, 0, 0.4)'},
            'Tachymeter-Beob': {'pattern': '', 'color': 'rgba(100, 100, 100, 0.3)'}
        }

        for typ, info in beob_typen.items():
            x_l, y_l = [], []
            for bl in beobachtungs_labels:
                bl_str = str(bl).lower()
                is_typ = ((info['pattern'] in bl_str and info['pattern'] != '') or
                          (info['pattern'] == '' and 'gnss' not in bl_str and 'niv' not in bl_str))
                if not is_typ:
                    continue

                bl_raw = str(bl)
                pts = []
                for pid in punkt_ids:
                    if (f"_{pid}" in bl_raw) or bl_raw.startswith(f"{pid}_"):
                        if pid in Koord_ENU:
                            pts.append(pid)

                if len(pts) >= 2:
                    p1, p2 = pts[0], pts[1]
                    x_l.extend([Koord_ENU[p1][0], Koord_ENU[p2][0], None])  # E
                    y_l.extend([Koord_ENU[p1][1], Koord_ENU[p2][1], None])  # N

            if x_l:
                fig.add_trace(go.Scatter(x=x_l, y=y_l, mode='lines', name=typ,
                                         line=dict(color=info['color'], width=1)))

        # 2) Darstellung der Konfidenzellipsen
        t = np.linspace(0, 2 * np.pi, n_ellipse_pts)
        first = True
        for _, row in df_konf_ellipsen_enu.iterrows():
            pid = str(row["Punkt"])
            if pid not in Koord_ENU:
                continue

            a = float(row["a_K"]) * v_faktor
            b = float(row["b_K"]) * v_faktor
            theta = float(row[theta_col]) * np.pi / 200.0  # gon->rad

            ex = a * np.cos(t)
            ey = b * np.sin(t)

            c, s = np.cos(theta), np.sin(theta)
            xr = c * ex - s * ey
            yr = s * ex + c * ey

            E0, N0, _ = Koord_ENU[pid]

            fig.add_trace(go.Scatter(
                x=E0 + xr, y=N0 + yr,
                mode="lines",
                line=dict(color="red", width=1.5),
                name=f"Ellipsen (×{v_faktor})",
                legendgroup="Ellipsen",
                showlegend=first,
                hoverinfo="skip"
            ))
            first = False

        # 3) Darstellung der Punkte
        xs, ys, texts, hovers = [], [], [], []
        for pid in punkt_ids:
            if pid not in Koord_ENU:
                continue
            E, N, U = Koord_ENU[pid]
            xs.append(E);
            ys.append(N);
            texts.append(pid)
            hovers.append(f"Punkt {pid}<br>E={E:.4f} m<br>N={N:.4f} m<br>U={U:.4f} m")

        fig.add_trace(go.Scatter(
            x=xs, y=ys, mode="markers+text",
            text=texts, textposition="top center",
            marker=dict(size=8, color="black"),
            name="Netzpunkte",
            hovertext=hovers, hoverinfo="text"
        ))

        fig.update_layout(
            title=f"{title} (Ellipsen ×{v_faktor})",
            xaxis=dict(title="E [m]", scaleanchor="y", scaleratio=1, showgrid=True, gridcolor="lightgrey"),
            yaxis=dict(title="N [m]", showgrid=True, gridcolor="lightgrey"),
            width=1100, height=900,
            template="plotly_white",
            plot_bgcolor="white"
        )

        fig.add_annotation(
            text=f"<b>Maßstab Ellipsen:</b><br>Dargestellte Größe = Konfidenzellipse × {v_faktor}",
            align='left', showarrow=False, xref='paper', yref='paper', x=0.02, y=0.05,
            bgcolor="white", bordercolor="black", borderwidth=1
        )

        fig.show(config={'scrollZoom': True})