import sympy as sp
from typing import Iterable, List, Sequence, Tuple, Optional


class Datumsfestlegung:

    @staticmethod
    def datumskomponenten(
        auswahl: Iterable[Tuple[str, str]],
        liste_punktnummern: Sequence[str],
        *,
        layout: str = "XYZ"
    ) -> List[int]:
        punkt2pos = {str(p): i for i, p in enumerate(liste_punktnummern)}

        layout = layout.upper()
        if layout != "XYZ":
            raise ValueError("Nur layout='XYZ' unterstützt (wie bei euch).")
        comp2off = {"X": 0, "Y": 1, "Z": 2}

        aktive: List[int] = []
        for pt, comp in auswahl:
            spt = str(pt)
            c = comp.upper()
            if spt not in punkt2pos:
                raise KeyError(f"Punkt '{pt}' nicht in liste_punktnummern.")
            if c not in comp2off:
                raise ValueError(f"Komponente '{comp}' ungültig. Nur X,Y,Z.")
            p = punkt2pos[spt]
            aktive.append(3 * p + comp2off[c])

        # Duplikate entfernen
        out, seen = [], set()
        for i in aktive:
            if i not in seen:
                seen.add(i)
                out.append(i)
        return out

    @staticmethod
    def auswahlmatrix_E(u: int, aktive_unbekannte_indices: Iterable[int]) -> sp.Matrix:
        E = sp.zeros(u, u)
        for idx in aktive_unbekannte_indices:
            i = int(idx)
            if not (0 <= i < u):
                raise IndexError(f"Aktiver Index {i} außerhalb [0,{u-1}]")
            E[i, i] = 1
        return E

    @staticmethod
    def raenderungsmatrix_G(
        x0: sp.Matrix,
        liste_punktnummern: Sequence[str],
        *,
        mit_massstab: bool = True,
        layout: str = "XYZ",
    ) -> sp.Matrix:
        if x0.cols != 1:
            raise ValueError("x0 muss Spaltenvektor sein.")
        layout = layout.upper()
        if layout != "XYZ":
            raise ValueError("Nur layout='XYZ' unterstützt (wie bei euch).")

        nP = len(liste_punktnummern)
        u = x0.rows
        d = 7 if mit_massstab else 6
        G = sp.zeros(u, d)

        for p in range(nP):
            ix, iy, iz = 3*p, 3*p+1, 3*p+2
            xi, yi, zi = x0[ix, 0], x0[iy, 0], x0[iz, 0]

            # Translationen
            G[ix, 0] = 1
            G[iy, 1] = 1
            G[iz, 2] = 1

            # Rotationen
            G[iy, 3] = -zi; G[iz, 3] =  yi     # Rx
            G[ix, 4] =  zi; G[iz, 4] = -xi     # Ry
            G[ix, 5] = -yi; G[iy, 5] =  xi     # Rz

            # Maßstab
            if mit_massstab:
                G[ix, 6] = xi
                G[iy, 6] = yi
                G[iz, 6] = zi

        return G

    @staticmethod
    def berechne_dx_geraendert(N: sp.Matrix, n: sp.Matrix, Gi: sp.Matrix) -> sp.Matrix:
        if N.rows != N.cols:
            raise ValueError("N muss quadratisch sein.")
        if n.cols != 1:
            raise ValueError("n muss Spaltenvektor sein.")
        if Gi.rows != N.rows:
            raise ValueError("Gi hat falsche Zeilenzahl.")

        u = N.rows
        d = Gi.cols
        K = N.row_join(Gi)
        K = K.col_join(Gi.T.row_join(sp.zeros(d, d)))
        rhs = n.col_join(sp.zeros(d, 1))
        sol = K.LUsolve(rhs)
        return sol[:u, :]

    @staticmethod
    def weiches_datum(
        A: sp.Matrix,
        dl: sp.Matrix,
        Q_ll: sp.Matrix,
        x0: sp.Matrix,
        anschluss_indices: Sequence[int],
        anschluss_werte: sp.Matrix,
        Sigma_AA: Optional[sp.Matrix] = None,
    ) -> Tuple[sp.Matrix, sp.Matrix, sp.Matrix]:
        if dl.cols != 1 or x0.cols != 1:
            raise ValueError("dl und x0 müssen Spaltenvektoren sein.")
        if A.rows != dl.rows:
            raise ValueError("A.rows muss dl.rows entsprechen.")
        if A.cols != x0.rows:
            raise ValueError("A.cols muss x0.rows entsprechen.")
        if Q_ll.rows != Q_ll.cols or Q_ll.rows != A.rows:
            raise ValueError("Q_ll muss (n×n) sein und zu A.rows passen.")

        u = A.cols
        idx = [int(i) for i in anschluss_indices]
        m = len(idx)

        if anschluss_werte.cols != 1 or anschluss_werte.rows != m:
            raise ValueError("anschluss_werte muss (m×1) sein.")
        if Sigma_AA is None:
            Sigma_AA = sp.eye(m)
        if Sigma_AA.rows != m or Sigma_AA.cols != m:
            raise ValueError("Sigma_AA muss (m×m) sein.")

        A_A = sp.zeros(m, u)
        for r, j in enumerate(idx):
            if not (0 <= j < u):
                raise IndexError(f"Anschluss-Index {j} außerhalb [0,{u-1}]")
            A_A[r, j] = 1

        x0_A = sp.Matrix([[x0[j, 0]] for j in idx])
        dl_A = anschluss_werte - x0_A

        A_ext = A.col_join(A_A)
        dl_ext = dl.col_join(dl_A)

        Q_ext = sp.zeros(Q_ll.rows + m, Q_ll.cols + m)
        Q_ext[:Q_ll.rows, :Q_ll.cols] = Q_ll
        Q_ext[Q_ll.rows:, Q_ll.cols:] = Sigma_AA

        return A_ext, dl_ext, Q_ext
