/*

Lippman, Essential C++, Exercise 4.5 / 6.2

Implement a Matrix class supporting at least the following general
interface:  addition and multiplication of two Matrix objects, a print()
member function, a compound += operator, and subscripting supported
through a pair of overloaded function call operators, as follows:

    float& operator()(int row, int column);
    float  operator()(int row, int column) const;

Lösung von Reinhard Zierke, 17.10.2005

*/

#ifndef _MATRIX_H_
#define _MATRIX_H_

#include <iostream>
#include <vector>
#include <string>
using namespace std;

//
// Fehlerklasse ArgumentsNotMatching,
// wird in Matrix-Operatoren + und * geworfen, wenn die beiden Argumente
// nicht zueinander passen, d.h. unterschiedliche Dimensionen haben.
//
class ArgumentsNotMatching {};

//
// Nun die eigentliche Matrix-Klasse
//
template <typename T>
class Matrix {
public:
  Matrix(size_t rows = 1, size_t columns = 1, T init = 0);
  Matrix(size_t rows, size_t columns, const T*);
  Matrix(const Matrix&);
  ~Matrix();
  
  size_t getRows() const { return rows_; }
  size_t getColumns() const { return columns_; }
  
  Matrix& operator=(const Matrix& rhs);
  
  bool operator==(const Matrix& rhs) const;
  bool operator!=(const Matrix& rhs) const;

  std::ostream& print(std::ostream&) const;

  Matrix& operator+=(const Matrix& rhs);
  Matrix& operator*=(const Matrix& rhs);
  Matrix& operator*=(const T& rhs);

  T operator()(int row, int column) const {
    return daten_[row*columns_ + column];
  }
  T& operator()(int row, int column) {
    return daten_[row*columns_ + column];
  }

private:
  size_t rows_;
  size_t columns_;
  T *daten_;
};

//
// Diese Funktionen für Addition, Multiplikation und Ausgabe sind
// ausserhalb der Klasse Matrix<T> definiert:
//

template <typename T>
Matrix<T> operator+(const Matrix<T> &lhs, const Matrix<T> &rhs);
template <typename T>
Matrix<T> operator*(const Matrix<T> &lhs, const Matrix<T> &rhs);
template <typename T>
Matrix<T> operator*(const T &lhs, const Matrix<T> &rhs);
template <typename T>
std::ostream& operator<<(std::ostream &os, const Matrix<T> &mat);
  
// Konstruktoren

//
// Default-Konstruktor:
// erzeugt ein Matrix-Objekt mit rows Zeilen und columns Spalten
// und besetzt alle Werte mit 0 bis auf die Diagonalelemente, welche
// auf init gesetzt werden.
// So bekommt man mit Matrix(n, n, 1) eine nXn-Einheitsmatrix.
//
template <typename T>
Matrix<T>::Matrix(size_t rows, size_t columns, T init)
 : rows_(rows), columns_(columns) {
 daten_ = new T[rows_*columns_];
 for (size_t i = 0; i < rows_*columns_; i++) {
   daten_[i] = 0;
 }
 if (init != 0) {
   for (size_t i = 0; i < min(rows_, columns_); i++) {
     daten_[i*columns_ + i] = init;
   }
 }
}

//
// "Array-Konstruktor":
// erzeugt ein Matrix-Objekt mit rows Zeilen und columns Spalten
// und besetzt es mit den in *array angegebenen Daten.
// Warnung: keine Prüfung, ob array groß genug ist!
//
template <typename T>
Matrix<T>::Matrix( size_t rows, size_t columns, const T *array )
 : rows_(rows), columns_(columns) {
  daten_ = new T[rows_*columns_];
  for (size_t i = 0; i < rows_*columns_; i++) {
    daten_[i] = array[i];
  }
}

//
// Kopier-Konstruktor:
// erzeugt ein Matrix-Objekt als Kopie der als Argument angegebenen
// Matrix.
//
template <typename T>
Matrix<T>::Matrix(const Matrix& rhs)
 : rows_(rhs.rows_), columns_(rhs.columns_) {
  daten_ = new T[rows_*columns_];
  for (size_t i = 0; i < rows_*columns_; i++) {
    daten_[i] = rhs.daten_[i];
  }
}

//
// Destruktor:
// muss  das dynamisch angelegte Feld daten_ löschen
//
template <typename T>
Matrix<T>::~Matrix() {
  delete [] daten_;
}

//
// Zuweisungs-Operator:
// kopiert die als Argument angegebene Matrix in das aktuelle Objekt.
// Dabei kann die Größe der Matrix verándert werden; deshalb muss das
// Feld daten_ neu angelegt werden.
//
template <typename T>
Matrix<T>& Matrix<T>::operator=(const Matrix &rhs) {
  if (operator!=(rhs)) {
    rows_ = rhs.rows_;
    columns_ = rhs.columns_;
    delete [] daten_;
    daten_ = new T[rows_*columns_];
    for (size_t i = 0; i < rows_*columns_; i++) {
      daten_[i] = rhs.daten_[i];
    }
  }
  return *this;
}

//
// Vergleichsoperatoren == und !=
// Ich prüfe hier wie Javas equal()-Methode auf gleiche Inhalte, auch
// wenn die Objekte selbst verschieden sind.
//
template <typename T>
bool Matrix<T>::operator==(const Matrix &rhs) const {
  if (this == &rhs) {	// dasselbe Objekt?
  	return true;
  }
  if (rows_ != rhs.rows_ || columns_ != rhs.columns_) {
  	return false;
  }
  for (size_t i = 0; i < rows_*columns_; i++) {
    if (daten_[i] != rhs.daten_[i]) {
      return false;
    }
  }
  return true;
}

template <typename T>
inline bool Matrix<T>::operator!=(const Matrix &rhs) const {
  return ! operator==(rhs);
}

//
// Operator +=
// Addiert zweite Matrix rhs zum aktuellen Objekt *this
// Ich lasse ausdrücklich zu, dass die zweite Matrix andere Dimensionen
// hat, sofern sie mindestens so viele Elemente enthält wie *this
//
template <typename T>
Matrix<T>& Matrix<T>::operator+=(const Matrix& rhs) {
  if (rows_*columns_ > rhs.rows_*rhs.columns_) {
  	throw ArgumentsNotMatching();
  }
  for (size_t i = 0; i < rows_*columns_; i++) {
    daten_[i] += rhs.daten_[i];
  }
  return *this;
}

//
// Operator +
// Damit + symmetrisch funktioniert (ggfs. Upcasting der linken Seite),
// ist dieser Operator als Funktion ausserhalb der Klasse
// Matrix<T> definiert.
// Der Operator + wird auf den Operator += zurückgeführt.
//
template <typename T>
Matrix<T> operator+(const Matrix<T>& lhs,
                            const Matrix<T>& rhs) {
  Matrix<T> result(lhs);
  result += rhs;
  return result;
}

// Operator *
// Damit * symmetrisch funktioniert (ggfs. Upcasting der linken Seite),
// ist dieser Operator als Funktion ausserhalb der Klasse
// Matrix<T> definiert.
//
template <typename T>
Matrix<T> operator*(const Matrix<T> &lhs,
                            const Matrix<T> &rhs) {
  if (lhs.getColumns() != rhs.getRows()) {
  	throw ArgumentsNotMatching();
  }
  Matrix<T> result(lhs.getRows(),rhs.getColumns());
  for (size_t i = 0; i < lhs.getRows(); i++) {
    for (size_t j = 0; j < rhs.getColumns(); j++) {
      T sum = 0;
      for (size_t k = 0; k < lhs.getColumns(); k++) {
        sum += lhs(i,k)*rhs(k,j);
      }
      result(i,j) = sum;
    }
  } 
  return result;
}

// Operator *=
// Der Operator *= wird auf den Operator * zurückgeführt.
//
template <typename T>
Matrix<T>& Matrix<T>::operator*=(const Matrix &m2) {
  *this = *this * m2;
  return *this;
}

//
// Operator für Skalarmultiplikation: Matrix<elemtype> *= T
//
template <typename T>
Matrix<T>& Matrix<T>::operator*=(const T& rhs) {
  for (size_t i = 0; i < rows_*columns_; i++) {
    daten_[i] *= rhs;
  }
  return *this;
}

// Operator Skalarmultiplikation: T * Matrlx<T>
// Der Operator *= wird auf den Operator * zurückgeführt.
//
template <typename T>
Matrix<T> operator*(const T &lhs,
                            const Matrix<T> &rhs) {
  Matrix<T> result(rhs);
  result *= lhs;
  return result;
}

//
// Methode print()
// gibt das aktuelle Objekt auf dem OutputStream os aus.
// 
template <typename T>
std::ostream& Matrix<T>::print(std::ostream &os) const {
  size_t pos = 0;
  for (size_t i = 0; i < rows_; i++) {
  	for (size_t j = 0; j < columns_; j++) {
      os << daten_[pos++] << ' ';
  	}
  	os << std::endl;
  }
  return os;
}

//
// ostream-Operator <<
// ausserhalb der Klasse Matrix<T> definiert,
// wird auf die Methode print() zurückgeführt.
//
template <typename T>
std::ostream& operator<<(std::ostream &os, const Matrix<T> &mat) {
  return mat.print(os);
}

#endif

