/*
 * Copyright © 2004-2011 Ondra Kamenik
 * Copyright © 2019-2024 Dynare Team
 *
 * This file is part of Dynare.
 *
 * Dynare is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Dynare is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Dynare.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifndef GENERAL_MATRIX_HH
#define GENERAL_MATRIX_HH

#include "SylvException.hh"
#include "Vector.hh"

#include <algorithm>
#include <memory>
#include <string>
#include <utility>

template<class T>
class TransposedMatrix
{
  friend class GeneralMatrix;
  template<class T2>
  friend GeneralMatrix operator*(const ConstGeneralMatrix& a, const TransposedMatrix<T2>& b);
  template<class T2>
  friend GeneralMatrix operator*(const TransposedMatrix<T2>& a, const ConstGeneralMatrix& b);
  template<class T1, class T2>
  friend GeneralMatrix operator*(const TransposedMatrix<T1>& a, const TransposedMatrix<T2>& b);

private:
  T& orig;

public:
  TransposedMatrix(T& orig_arg) : orig {orig_arg}
  {
  }
};

// Syntactic sugar for representing a transposed matrix
template<class T>
TransposedMatrix<T>
transpose(T& m)
{
  return TransposedMatrix<T>(m);
}

class GeneralMatrix;

class ConstGeneralMatrix
{
  friend class GeneralMatrix;
  friend GeneralMatrix operator*(const ConstGeneralMatrix& a, const ConstGeneralMatrix& b);
  template<class T>
  friend GeneralMatrix operator*(const ConstGeneralMatrix& a, const TransposedMatrix<T>& b);
  template<class T>
  friend GeneralMatrix operator*(const TransposedMatrix<T>& a, const ConstGeneralMatrix& b);
  template<class T1, class T2>
  friend GeneralMatrix operator*(const TransposedMatrix<T1>& a, const TransposedMatrix<T2>& b);

protected:
  ConstVector data; // Has unit-stride
  int rows;
  int cols;
  int ld;

public:
  ConstGeneralMatrix(ConstVector d, int m, int n) : data(std::move(d)), rows(m), cols(n), ld(m)
  {
    if (data.skip() > 1)
      throw SYLV_MES_EXCEPTION("Vector must have unit-stride");
    if (data.length() < m * n)
      throw SYLV_MES_EXCEPTION("Vector is too small");
  }
  ConstGeneralMatrix(const ConstGeneralMatrix& m) = default;
  ConstGeneralMatrix(ConstGeneralMatrix&& m) = default;
  // Implicit conversion from GeneralMatrix is ok, since it's cheap
  ConstGeneralMatrix(const GeneralMatrix& m);
  // Create submatrix (with data sharing)
  ConstGeneralMatrix(const GeneralMatrix& m, int i, int j, int nrows, int ncols);
  // Create submatrix (with data sharing)
  ConstGeneralMatrix(const ConstGeneralMatrix& m, int i, int j, int nrows, int ncols);
  explicit ConstGeneralMatrix(const mxArray* p) :
      data(p), rows {static_cast<int>(mxGetM(p))}, cols {static_cast<int>(mxGetN(p))}, ld {rows}
  {
  }
  virtual ~ConstGeneralMatrix() = default;

  ConstGeneralMatrix& operator=(const ConstGeneralMatrix& v) = delete;
  ConstGeneralMatrix& operator=(ConstGeneralMatrix&& v) = delete;

  [[nodiscard]] const double&
  get(int i, int j) const
  {
    return data[j * ld + i];
  }
  [[nodiscard]] int
  nrows() const
  {
    return rows;
  }
  [[nodiscard]] int
  ncols() const
  {
    return cols;
  }
  [[nodiscard]] int
  getLD() const
  {
    return ld;
  }
  [[nodiscard]] const double*
  base() const
  {
    return data.base();
  }
  [[nodiscard]] const ConstVector&
  getData() const
  {
    return data;
  }
  [[nodiscard]] ConstVector getRow(int row) const;
  [[nodiscard]] ConstVector getCol(int col) const;

  [[nodiscard]] double getNormInf() const;
  [[nodiscard]] double getNorm1() const;
  /* x = scalar(a)*x + scalar(b)*this*d */
  void multVec(double a, Vector& x, double b, const ConstVector& d) const;
  /* x = scalar(a)*x + scalar(b)*this'*d */
  void multVecTrans(double a, Vector& x, double b, const ConstVector& d) const;
  /* x = x + this*d */
  void
  multaVec(Vector& x, const ConstVector& d) const
  {
    multVec(1.0, x, 1.0, d);
  }
  /* x = x + this'*d */
  void
  multaVecTrans(Vector& x, const ConstVector& d) const
  {
    multVecTrans(1.0, x, 1.0, d);
  }
  /* x = x - this*d */
  void
  multsVec(Vector& x, const ConstVector& d) const
  {
    multVec(1.0, x, -1.0, d);
  }
  /* x = x - this'*d */
  void
  multsVecTrans(Vector& x, const ConstVector& d) const
  {
    multVecTrans(1.0, x, -1.0, d);
  }
  /* m = inv(this)*m */
  void multInvLeft(GeneralMatrix& m) const;
  /* m = inv(this')*m */
  void multInvLeftTrans(GeneralMatrix& m) const;
  /* d = inv(this)*d */
  void multInvLeft(Vector& d) const;
  /* d = inv(this')*d */
  void multInvLeftTrans(Vector& d) const;

  [[nodiscard]] bool isFinite() const;
  /** Returns true of the matrix is exactly zero. */
  [[nodiscard]] bool isZero() const;

  virtual void print() const;

protected:
  void multInvLeft(const std::string& trans, int mrows, int mcols, int mld, double* d) const;
};

class GeneralMatrix
{
  friend class ConstGeneralMatrix;
  friend GeneralMatrix operator*(const ConstGeneralMatrix& a, const ConstGeneralMatrix& b);
  template<class T>
  friend GeneralMatrix operator*(const ConstGeneralMatrix& a, const TransposedMatrix<T>& b);
  template<class T>
  friend GeneralMatrix operator*(const TransposedMatrix<T>& a, const ConstGeneralMatrix& b);
  template<class T1, class T2>
  friend GeneralMatrix operator*(const TransposedMatrix<T1>& a, const TransposedMatrix<T2>& b);

protected:
  Vector data; // Has unit-stride
  int rows;
  int cols;
  int ld;

public:
  GeneralMatrix(int m, int n) : data(m * n), rows(m), cols(n), ld(m)
  {
  }
  GeneralMatrix(Vector d, int m, int n) : data(std::move(d)), rows(m), cols(n), ld(m)
  {
    if (data.skip() > 1)
      throw SYLV_MES_EXCEPTION("Vector must have unit-stride");
    if (data.length() < m * n)
      throw SYLV_MES_EXCEPTION("Vector is too small");
  }

  /* The copies will have ld==rows, for memory efficiency (hence we do not use
     the default copy constructor) */
  GeneralMatrix(const GeneralMatrix& m);
  // We don't want implict conversion from ConstGeneralMatrix, since it's expensive
  explicit GeneralMatrix(const ConstGeneralMatrix& m);

  GeneralMatrix(GeneralMatrix&& m) = default;

  template<class T>
  explicit GeneralMatrix(const TransposedMatrix<T>& m) :
      data(m.orig.rows * m.orig.cols), rows(m.orig.cols), cols(m.orig.rows), ld(rows)
  {
    for (int i = 0; i < rows; i++)
      for (int j = 0; j < cols; j++)
        get(i, j) = m.orig.get(j, i);
  }

  // Create submatrix (with data copy)
  GeneralMatrix(const GeneralMatrix& m, int i, int j, int nrows, int ncols);
  // Create submatrix (with data sharing)
  GeneralMatrix(GeneralMatrix& m, int i, int j, int nrows, int ncols);

  explicit GeneralMatrix(mxArray* p) :
      data(p), rows {static_cast<int>(mxGetM(p))}, cols {static_cast<int>(mxGetN(p))}, ld {rows}
  {
  }

  virtual ~GeneralMatrix() = default;
  GeneralMatrix& operator=(const GeneralMatrix& m) = default;
  GeneralMatrix& operator=(GeneralMatrix&& m) = default;
  GeneralMatrix& operator=(const ConstGeneralMatrix& m);

  [[nodiscard]] const double&
  get(int i, int j) const
  {
    return data[j * ld + i];
  }
  double&
  get(int i, int j)
  {
    return data[j * ld + i];
  }
  [[nodiscard]] int
  nrows() const
  {
    return rows;
  }
  [[nodiscard]] int
  ncols() const
  {
    return cols;
  }
  [[nodiscard]] int
  getLD() const
  {
    return ld;
  }
  double*
  base()
  {
    return data.base();
  }
  [[nodiscard]] const double*
  base() const
  {
    return data.base();
  }
  Vector&
  getData()
  {
    return data;
  }
  [[nodiscard]] ConstVector
  getData() const
  {
    return data;
  }
  Vector getRow(int row);
  Vector getCol(int col);
  [[nodiscard]] ConstVector getRow(int row) const;
  [[nodiscard]] ConstVector getCol(int col) const;

  [[nodiscard]] double
  getNormInf() const
  {
    return ConstGeneralMatrix(*this).getNormInf();
  }
  [[nodiscard]] double
  getNorm1() const
  {
    return ConstGeneralMatrix(*this).getNorm1();
  }

  /* place matrix m to the position (i,j) */
  void place(const ConstGeneralMatrix& m, int i, int j);
  void
  place(const GeneralMatrix& m, int i, int j)
  {
    place(ConstGeneralMatrix(m), i, j);
  }

  // this = a·b
  void mult(const ConstGeneralMatrix& a, const ConstGeneralMatrix& b);
  void
  mult(const GeneralMatrix& a, const GeneralMatrix& b)
  {
    mult(ConstGeneralMatrix(a), ConstGeneralMatrix(b));
  }

  // this = this + scalar·a·b
  void multAndAdd(const ConstGeneralMatrix& a, const ConstGeneralMatrix& b, double mult = 1.0);
  void
  multAndAdd(const GeneralMatrix& a, const GeneralMatrix& b, double mult = 1.0)
  {
    multAndAdd(ConstGeneralMatrix(a), ConstGeneralMatrix(b), mult);
  }

  // this = this + scalar·a·bᵀ
  void multAndAdd(const ConstGeneralMatrix& a, const ConstGeneralMatrix& b, const std::string& dum,
                  double mult = 1.0);
  void
  multAndAdd(const GeneralMatrix& a, const GeneralMatrix& b, const std::string& dum,
             double mult = 1.0)
  {
    multAndAdd(ConstGeneralMatrix(a), ConstGeneralMatrix(b), dum, mult);
  }

  // this = this + scalar·aᵀ·b
  void multAndAdd(const ConstGeneralMatrix& a, const std::string& dum, const ConstGeneralMatrix& b,
                  double mult = 1.0);
  void
  multAndAdd(const GeneralMatrix& a, const std::string& dum, const GeneralMatrix& b,
             double mult = 1.0)
  {
    multAndAdd(ConstGeneralMatrix(a), dum, ConstGeneralMatrix(b), mult);
  }

  // this = this + scalar·aᵀ·bᵀ
  void multAndAdd(const ConstGeneralMatrix& a, const std::string& dum1, const ConstGeneralMatrix& b,
                  const std::string& dum2, double mult = 1.0);
  void
  multAndAdd(const GeneralMatrix& a, const std::string& dum1, const GeneralMatrix& b,
             const std::string& dum2, double mult = 1.0)
  {
    multAndAdd(ConstGeneralMatrix(a), dum1, ConstGeneralMatrix(b), dum2, mult);
  }

  // this = this + scalar·a·aᵀ
  void addOuter(const ConstVector& a, double mult = 1.0);
  void
  addOuter(const Vector& a, double mult = 1.0)
  {
    addOuter(ConstVector(a), mult);
  }

  // this = this·m
  void multRight(const ConstGeneralMatrix& m);
  void
  multRight(const GeneralMatrix& m)
  {
    multRight(ConstGeneralMatrix(m));
  }

  // this = m·this
  void multLeft(const ConstGeneralMatrix& m);
  void
  multLeft(const GeneralMatrix& m)
  {
    multLeft(ConstGeneralMatrix(m));
  }

  // this = this·mᵀ
  void multRightTrans(const ConstGeneralMatrix& m);
  void
  multRightTrans(const GeneralMatrix& m)
  {
    multRightTrans(ConstGeneralMatrix(m));
  }

  // this = mᵀ·this
  void multLeftTrans(const ConstGeneralMatrix& m);
  void
  multLeftTrans(const GeneralMatrix& m)
  {
    multLeftTrans(ConstGeneralMatrix(m));
  }

  // x = scalar(a)·x + scalar(b)·this·d
  void
  multVec(double a, Vector& x, double b, const ConstVector& d) const
  {
    ConstGeneralMatrix(*this).multVec(a, x, b, d);
  }

  // x = scalar(a)·x + scalar(b)·thisᵀ·d
  void
  multVecTrans(double a, Vector& x, double b, const ConstVector& d) const
  {
    ConstGeneralMatrix(*this).multVecTrans(a, x, b, d);
  }

  // x = x + this·d
  void
  multaVec(Vector& x, const ConstVector& d) const
  {
    ConstGeneralMatrix(*this).multaVec(x, d);
  }

  // x = x + thisᵀ·d */
  void
  multaVecTrans(Vector& x, const ConstVector& d) const
  {
    ConstGeneralMatrix(*this).multaVecTrans(x, d);
  }

  // x = x - this·d
  void
  multsVec(Vector& x, const ConstVector& d) const
  {
    ConstGeneralMatrix(*this).multsVec(x, d);
  }

  // x = x - thisᵀ·d
  void
  multsVecTrans(Vector& x, const ConstVector& d) const
  {
    ConstGeneralMatrix(*this).multsVecTrans(x, d);
  }

  // this = zero
  void zeros();

  // this = unit (on main diagonal)
  void unit();

  // this = NaN
  void nans();

  // this = ∞
  void infs();

  // this = scalar·this
  void mult(double a);

  // this = this + scalar·m
  void add(double a, const ConstGeneralMatrix& m);
  void
  add(double a, const GeneralMatrix& m)
  {
    add(a, ConstGeneralMatrix(m));
  }

  // this = this + scalar·mᵀ
  void add(double a, const ConstGeneralMatrix& m, const std::string& dum);
  void
  add(double a, const GeneralMatrix& m, const std::string& dum)
  {
    add(a, ConstGeneralMatrix(m), dum);
  }

  [[nodiscard]] bool
  isFinite() const
  {
    return (ConstGeneralMatrix(*this)).isFinite();
  }

  [[nodiscard]] bool
  isZero() const
  {
    return (ConstGeneralMatrix(*this)).isZero();
  }

  virtual void
  print() const
  {
    ConstGeneralMatrix(*this).print();
  }

private:
  void copy(const ConstGeneralMatrix& m, int ioff = 0, int joff = 0);
  void
  copy(const GeneralMatrix& m, int ioff = 0, int joff = 0)
  {
    copy(ConstGeneralMatrix(m), ioff, joff);
  }

  void gemm(const std::string& transa, const ConstGeneralMatrix& a, const std::string& transb,
            const ConstGeneralMatrix& b, double alpha, double beta);
  void
  gemm(const std::string& transa, const GeneralMatrix& a, const std::string& transb,
       const GeneralMatrix& b, double alpha, double beta)
  {
    gemm(transa, ConstGeneralMatrix(a), transb, ConstGeneralMatrix(b), alpha, beta);
  }

  /* this = this * op(m) (without whole copy of this) */
  void gemm_partial_right(const std::string& trans, const ConstGeneralMatrix& m, double alpha,
                          double beta);
  void
  gemm_partial_right(const std::string& trans, const GeneralMatrix& m, double alpha, double beta)
  {
    gemm_partial_right(trans, ConstGeneralMatrix(m), alpha, beta);
  }

  // this = op(m)·this (without whole copy of ‘this’)
  void gemm_partial_left(const std::string& trans, const ConstGeneralMatrix& m, double alpha,
                         double beta);
  void
  gemm_partial_left(const std::string& trans, const GeneralMatrix& m, double alpha, double beta)
  {
    gemm_partial_left(trans, ConstGeneralMatrix(m), alpha, beta);
  }

  /* number of rows/columns for copy used in gemm_partial_* */
  static constexpr int md_length = 23;
};

// Computes a·b
inline GeneralMatrix
operator*(const ConstGeneralMatrix& a, const ConstGeneralMatrix& b)
{
  GeneralMatrix m(a.rows, b.cols);
  m.gemm("N", a, "N", b, 1.0, 0.0);
  return m;
}

// Computes a·bᵀ
template<class T>
GeneralMatrix
operator*(const ConstGeneralMatrix& a, const TransposedMatrix<T>& b)
{
  GeneralMatrix m(a.rows, b.orig.rows);
  m.gemm("N", a, "T", b.orig, 1.0, 0.0);
  return m;
}

// Computes aᵀ·b
template<class T>
GeneralMatrix
operator*(const TransposedMatrix<T>& a, const ConstGeneralMatrix& b)
{
  GeneralMatrix m(a.orig.cols, b.cols);
  m.gemm("T", a.orig, "N", b, 1.0, 0.0);
  return m;
}

// Computes aᵀ·bᵀ
template<class T1, class T2>
GeneralMatrix
operator*(const TransposedMatrix<T1>& a, const TransposedMatrix<T2>& b)
{
  GeneralMatrix m(a.orig.cols, b.orig.rows);
  m.gemm("T", a.orig, "T", b.orig, 1.0, 0.0);
  return m;
}

class SVDDecomp
{
protected:
  // Minimum of number of rows and columns of the decomposed matrix
  const int minmn;
  // Singular values
  Vector sigma;
  // Orthogonal matrix U
  GeneralMatrix U;
  // Orthogonal matrix Vᵀ
  GeneralMatrix VT;
  // Convered flag
  bool conv {false};

public:
  SVDDecomp(const GeneralMatrix& A) :
      minmn(std::min<int>(A.nrows(), A.ncols())),
      sigma(minmn),
      U(A.nrows(), A.nrows()),
      VT(A.ncols(), A.ncols())
  {
    construct(A);
  }
  [[nodiscard]] const GeneralMatrix&
  getU() const
  {
    return U;
  }
  [[nodiscard]] const GeneralMatrix&
  getVT() const
  {
    return VT;
  }
  void solve(const ConstGeneralMatrix& B, GeneralMatrix& X) const;
  void
  solve(const ConstVector& b, Vector& x) const
  {
    GeneralMatrix xmat(x, x.length(), 1);
    solve(ConstGeneralMatrix(b, b.length(), 1), xmat);
  }

private:
  void construct(const GeneralMatrix& A);
};

#endif
