#include <iostream>    // notwendig zur Ausgabe
#include <vector>
#include "hdnum.hh"    // hdnum header

namespace hdnum {

  template<typename REAL>
  class SparseMatrix {

    struct MatrixEntry {
      int i;
      int j;
      REAL value;
    };

  public:

    void AddEntry (int i, int j, REAL value) {
      assert(i >= 0);
      assert(j >= 0);
      if (value != .0)
        entries.push_back(MatrixEntry{.i=i, .j=j, .value=value});
    }

    template<typename V>
    void mv (Vector<V>& y, const Vector<V>& x) {

      zero(y);

      for (MatrixEntry& matrix_entry : entries) {
        assert(y.size() > matrix_entry.i);
        assert(x.size() > matrix_entry.j);
        y[matrix_entry.i] += matrix_entry.value * x[matrix_entry.j];
      }
    }

  //private:
    std::vector<MatrixEntry> entries;
  };

}

// generic iterative updater function
typedef void iterativeUpdater(hdnum::DenseMatrix<double>& A,
                              hdnum::SparseMatrix<double>& A_s,
                              int N,
                              hdnum::Vector<double>& x_tmp,
                              hdnum::Vector<double>& b,
                              hdnum::Vector<double>& d,
                              hdnum::Vector<double>& x);

// solve iterative based on given method
void solveIterative(hdnum::DenseMatrix<double>& A,
                    hdnum::SparseMatrix<double>& A_s,
                    hdnum::Vector <double>& x,
                    hdnum::Vector<double>& b,
                    iterativeUpdater update) {
    int N = A.rowsize();
    // defekt vektor
    hdnum::Vector<double> d(N);
    // temporary variable
    hdnum::Vector<double> x_tmp(N);
    A_s.mv(x_tmp, x);
    d = b - x_tmp;
    double initialDefekt = d.two_norm();
    while (d.two_norm() > initialDefekt * 10e-4) {
        // run one iteration
        update(A, A_s, N, x_tmp, b, d, x);
    }
}

// richardson: W = 1/omega I => W^-1 = omega I
inline void richardson(hdnum::DenseMatrix<double>& A,
                       hdnum::SparseMatrix<double>& A_s,
                       int N,
                       hdnum::Vector<double>& x_tmp,
                       hdnum::Vector<double>& b,
                       hdnum::Vector<double>& d,
                       hdnum::Vector<double>& x) {
    A_s.mv(x_tmp, x);
    d = b - x_tmp;
    // x(k+1) = x(k) + omega * d(k)
    // hier omega = -0.01
    for (int i=0; i<N; i++) {
        x_tmp[i] = -0.5*d[i];
    }
    x += x_tmp;
}

// jacobi: W = D
inline void jacobi(hdnum::DenseMatrix<double>& A,
                   hdnum::SparseMatrix<double>& A_s,
                   int N,
                   hdnum::Vector<double>& x_tmp,
                   hdnum::Vector<double>& b,
                   hdnum::Vector<double>& d,
                   hdnum::Vector<double>& x) {
    A_s.mv(x_tmp, x);
    d = b - x_tmp;
    // jacobi iteration, teile durch Diagonal Elemente
    // x(k+1) = x(k) + D^-1 d(k)
    for (int i=0; i<N; i++) {
        x_tmp[i] = d[i]/A[i][i];
    }
    x += x_tmp;
}

// gauss seidel: W = L + D
inline void gaussSeidel(hdnum::DenseMatrix<double>& A,
                        hdnum::SparseMatrix<double>& A_s,
                        int N,
                        hdnum::Vector<double>& x_tmp,
                        hdnum::Vector<double>& b,
                        hdnum::Vector<double>& d,
                        hdnum::Vector <double>& x) {
    A_s.mv(x_tmp, x);
    d = b - x_tmp;
    x_tmp = d;
    // x(k+1) = x(k) + v(k)
    // W*v(k) = d(k)
    // W untere Dreicksmatrix, loese W*v(k) = d(k) durch Vorwaertseinsetzen
    for (int i = 0; i<N; i++) {
        x_tmp[i] = d[i];
        for (int j=i-1; 0<=j && j<i; j++) { // Elemente abseits der Hauptdiagonale von A sind 0
            x_tmp[i] -= x_tmp[j] * A[i][j];
        }
        x_tmp[i] /= A[i][i];
    }
    x += x_tmp;
}

void testApproximation(hdnum::DenseMatrix<double>& A,
                       hdnum::SparseMatrix<double>& A_s,
                       hdnum::Vector<double>& x,
                       hdnum::Vector<double>& b,
                       iterativeUpdater updater) {
    int N = A.rowsize();
    hdnum::Vector<double> y(N);
    // teste iteration mit gegebenem verfahren
    solveIterative(A,A_s,x,b,updater);
    // vergleiche mit LU Zerleger
    hdnum::linsolve(A,y,b);
    std::cout << "Relativer Fehler: " << (y - x).two_norm() / y.two_norm() << std::endl;
}

int main () {
    for(int n=4; n<=9; n++) {
        int N = pow(2,n);

        // Testmatrix aufsetzen
        hdnum::DenseMatrix<double> A(N,N,.0);
        hdnum::SparseMatrix<double> A_s;
        for (typename hdnum::DenseMatrix<double>::size_type i=0; i<A.rowsize(); ++i) {
            if (i > 0) {
                A[i][i-1] = 1.0;
                A_s.AddEntry(i, i-1, 1.0);
            }
            if (i + 1 < A.colsize()) {
                A[i][i+1] = 1.0;
                A_s.AddEntry(i, i+1, 1.0);
            }
            A[i][i] -= 2.0;
            A_s.AddEntry(i, i, -2.0);
        }

        // Rechte Seite und Lösungsvektor
        hdnum::Vector<double> x(N, 0.0);
        hdnum::Vector<double> b(N, 1.0);

        // Lösen Sie nun A*x=b iterativ

        // Pretty-printing fuer Vektoren
        x.scientific(false);
        x.width(15);

        // testApproximation(A,A_s,x,b,richardson);

        hdnum::Timer myTimer;
        solveIterative(A,A_s,x,b,gaussSeidel);
        //hdnum::linsolve(A,x,b);
        std::cout << N << " " << myTimer.elapsed() << std::endl;
    }
}
