#include <vector>
#include "hdnum.hh"    // hdnum header

template<typename REAL>
void solveTriDiag(hdnum::DenseMatrix<REAL> &A, std::vector<REAL> &x, std::vector<REAL> &b) {
    int N = b.size();
    x[0] = A[0][0];
    // LU Zerlegung
    REAL l;
    for (int j=1; j<N; j++) {
        // berechne l faktor
        l = A[j][j-1] / x[j-1];
        // modifiziere diagonalelemente und rechte seite
        x[j] = A[j][j] - l * A[j-1][j];
        b[j] = b[j] - l * b[j-1];
    }
    // Loesen durch Rueckwaertseinsetzen
    x[N-1] = b[N-1] / x[N-1];
    for (int j=N-2; j>=0; j--) {
        x[j] = (b[j] - A[j][j+1]*x[j+1])/x[j];
    }
}

template<typename REAL>
class CubicSpline {
    public:
        // erstelle einen kubischen spline mit vorgegebenen stuetzstellen
        // und werten
        CubicSpline(std::vector<REAL> xs, std::vector<REAL> ys) {
            calculateCoefficients(xs, ys);
        }

        // erstelle einen kubischen spline mit vorgegebenen stuetzstellen
        // und einer zu interpolierenden funktion
        CubicSpline(std::vector<REAL> xs, REAL(*f)(REAL)) {
            int N = xs.size();
            std::vector<double> ys(N);
            for (int i=0; i<N; i++) {
                ys[i] = f(xs[i]);
            }
            calculateCoefficients(xs, ys);
        }

        // erstelle einen kubischen spline an aequidistanten stuetzstellen
        // und einer zu interpolierenden funktion
        CubicSpline(REAL a, REAL b, int N, REAL(*f)(REAL)) {
            std::vector<double> xs(N+1);
            std::vector<double> ys(N+1);
            for (int i=0; i<=N; i++) {
                xs[i] = a + (1.0*i)/N*(b-a);
                ys[i] = f(xs[i]);
            }
            calculateCoefficients(xs, ys);
        }

        // werte kubischen spline an vorgegebener stelle aus
        REAL evaluate(REAL x) {
            for (int i=1; i<x_s.size(); i++) {
                if (x > x_s[i] && i < x_s.size() - 1) {
                    continue;
                } else {
                    return a_0[i-1] + a_1[i-1] *(x - x_s[i]) + a_2[i-1]*std::pow(x - x_s[i], 2) + a_3[i-1]*std::pow(x-x_s[i], 3);
                }
            }
            return 0;
        }

        // gebe alle interpolations polynome aus
        void print() {
            for(int i=0; i<x_s.size()-1; i++) {
                printf("p_%d(x) = %4.2f + %4.2f(x - %4.2f) + %4.2f(x - %4.2f)^2 + %4.2f(x - %4.2f)^3\n",
                       i, a_0[i], a_1[i], x_s[i], a_2[i], x_s[i], a_3[i], x_s[i]);
            }
        }

    private:
        // stuetzstellen
        std::vector<REAL> x_s;
        // koeffizienten
        std::vector<REAL> a_0;
        std::vector<REAL> a_1;
        std::vector<REAL> a_2;
        std::vector<REAL> a_3;

        void calculateCoefficients(std::vector<REAL> xs, std::vector<REAL> ys) {
            // stuetzstellen from x_0 ... to x_n
            int n = xs.size()-1;
            // copy stuetzstellen
            x_s = std::vector<double>(n+1);
            x_s = xs;
            // setup (n-1)x(n-1) matrix for a_2
            hdnum::DenseMatrix<REAL> A(n-1,n-1);
            std::vector<REAL> b(n-1);
            std::vector<REAL> x(n-1);
            REAL h; // h_i
            REAL h1; // h_{i+1}
            // setup LGS for a_2
            // A has tridiagonal structure
            for (int i = 1; i<n; i++) {
                h = xs[i] - xs[i-1];
                h1 = xs[i+1] - xs[i];
                b[i-1] = 3 * ((ys[i+1] - ys[i])/h1 - (ys[i] - ys[i-1])/h);
                if (i > 1) {
                    A[i-1][i-2] = h;
                } if (i < n-1) {
                    A[i-1][i] = h1;
                }
                A[i-1][i-1] = 2 * (h + h1);
            }
            // initialize vectors
            a_0 = std::vector<double>(n);
            a_1 = std::vector<double>(n);
            a_2 = std::vector<double>(n);
            a_3 = std::vector<double>(n);
            solveTriDiag(A,a_2,b);
            // natuerliche randbedingung
            a_2[n-1] = 0;
            // berechne restliche koeffizienten
            for (int i = 1; i<=n; i++) {
                h = xs[i] - xs[i-1]; // h_i
                h1 = xs[i+1] - xs[i]; // h_{i+1}
                a_0[i-1] = ys[i];
                if (i == 1) { // a_2[-1] = 0
                    a_1[i-1] = (ys[i] - ys[i-1])/h + (h/3)*(2*a_2[i-1]);
                    a_3[i-1] = (a_2[i-1])/(3*h);
                } else {
                    a_1[i-1] = (ys[i] - ys[i-1])/h + h/3*(2*a_2[i-1] + a_2[i-2]);
                    a_3[i-1] = (a_2[i-1] - a_2[i-2])/(3 * h);
                }
            }
        }
};

double f1(double x) {
    return std::pow(x,3);
}

int main() {
    // std::vector<double> xs = {0, 1, 2};
    // CubicSpline<double> phi(xs, f1);
    // phi.print();

    // saurier fundstellen
    std::vector<double> xs = {3.75, 3.75, 2.25, 1.25, 0.25, 0.75, 4.00, 5.50, 6.25, 9.00, 12.5, 12.75, 12.25};
    std::vector<double> ys = {0.25, 1.50, 3.25, 4.25, 4.65, 4.83, 2.50, 3.25, 3.65, 3.00, 1.25, 2.150, 3.500};

    std::vector<double> ts(13);
    ts[0] = 0;
    for (int i = 1; i<=12; i++) {
        ts[i] = ts[i-1] + std::sqrt(std::pow(xs[i] - xs[i-1], 2) + std::pow(ys[i] - ys[i-1], 2));
    }

    CubicSpline<double> phi(ts, xs);
    CubicSpline<double> psi(ts, ys);

    double xi;
    for (int j = 0; j<=100; j++) {
        xi = j*ts[12] / 100;
        std::cout << phi.evaluate(xi) << " " << psi.evaluate(xi) << std::endl;
    }
}
