#ifndef MATRIX_H_
#define MATRIX_H_

#include <cstddef>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <numeric>
#include <vector>

#include <xxhash.h>

template <class T> struct Matrix {
  std::uint32_t rows;
  std::uint32_t cols;
  std::vector<T> mem;

  Matrix<T>(std::uint32_t _rows, std::uint32_t _cols)
      : rows(_rows), cols(_cols) {
    mem.resize(rows * cols);
  }

  Matrix<T>(const char *filepath) {
    std::ifstream matfs(filepath);

    if (matfs.fail() || !matfs.is_open()) {
      throw std::runtime_error("Error opening matrix file");
    }

    matfs >> this->rows >> this->cols;
    this->mem.resize(this->rows * this->cols);

    for (std::uint32_t i = 0; i < rows; i++) {
      for (std::uint32_t j = 0; j < cols; j++) {
        matfs >> (*this)(i, j);
      }
    }

    matfs.close();
  }

  T &operator()(std::uint32_t row_i, std::uint32_t col_j) {
    return mem[row_i * cols + col_j];
  }

  const T &operator()(std::uint32_t row_i, std::uint32_t col_j) const {
    return mem[row_i * cols + col_j];
  }

  Matrix<T> &operator=(const Matrix<T> &mat) {
    this->rows = mat.rows;
    this->cols = mat.cols;
    this->data = mem;

    return *this;
  }

  Matrix<T> t() const {
    Matrix<T> transposed = *this;
    for (std::uint32_t i = 0; i < this->rows; i++) {
      for (std::uint32_t j = 0; j < this->cols; j++) {
        transposed(j, i) = (*this)(i, j);
      }
    }
    return transposed;
  }

  XXH32_hash_t chksum() const {
    constexpr XXH32_hash_t HASH_SEED = 42;
    return XXH32(this->mem.data(), mem.size(), HASH_SEED);
  }

  std::size_t bytes() const { return this->mem.size() * sizeof(T); }
};

template <class T> std::ostream &operator<<(std::ostream &os, Matrix<T> &mat) {
  for (std::uint32_t i = 0; i < mat.rows; i++) {
    for (std::uint32_t j = 0; j < mat.cols; j++) {
      os << mat(i, j) << "\t";
    }
    os << "\n";
  }

  return os;
}

#endif // MATRIX_H_
