/*
 * Copyright 2009-2019 The VOTCA Development Team (http://www.votca.org)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#pragma once
#ifndef __VOTCA_TOOLS_MATRIX_FREE_OPERATOR_H
#define __VOTCA_TOOLS_MATRIX_FREE_OPERATOR_H
#include <votca/xtp/eigen.h>

namespace votca {
namespace xtp {

class MatrixFreeOperator;
}
}  // namespace votca
namespace Eigen {
namespace internal {
// MatrixReplacement looks-like a Matrix, so let's inherits its traits:
template <>
struct traits<votca::xtp::MatrixFreeOperator>
    : public Eigen::internal::traits<Eigen::MatrixXd> {};
}  // namespace internal
}  // namespace Eigen

namespace votca {
namespace xtp {

class MatrixFreeOperator : public Eigen::EigenBase<MatrixFreeOperator> {
 public:
  enum {
    ColsAtCompileTime = Eigen::Dynamic,
    MaxColsAtCompileTime = Eigen::Dynamic,
    IsRowMajor = false
  };

  Index rows() const { return this->_size; }
  Index cols() const { return this->_size; }

  template <typename Vtype>
  Eigen::Product<MatrixFreeOperator, Vtype, Eigen::AliasFreeProduct> operator*(
      const Eigen::MatrixBase<Vtype>& x) const {
    return Eigen::Product<MatrixFreeOperator, Vtype, Eigen::AliasFreeProduct>(
        *this, x.derived());
  }

  // convenience function
  Eigen::MatrixXd get_full_matrix() const;
  Eigen::VectorXd diagonal() const;
  Index size() const;
  void set_size(Index size);

  virtual bool useRow() const { return true; }
  virtual bool useBlock() const { return false; }

  virtual Index getBlocksize() const { return 0; }

  // extract row/col of the operator
  virtual Eigen::RowVectorXd OperatorRow(Index index) const;

  virtual Eigen::MatrixXd OperatorBlock(Index row, Index col) const;

 private:
  Index _size;
};
}  // namespace xtp
}  // namespace votca

namespace Eigen {

namespace internal {

// replacement of the mat*vect operation
template <typename Vtype>
struct generic_product_impl<votca::xtp::MatrixFreeOperator, Vtype, DenseShape,
                            DenseShape, GemvProduct>
    : generic_product_impl_base<
          votca::xtp::MatrixFreeOperator, Vtype,
          generic_product_impl<votca::xtp::MatrixFreeOperator, Vtype>> {

  typedef
      typename Product<votca::xtp::MatrixFreeOperator, Vtype>::Scalar Scalar;

  template <typename Dest>
  static void scaleAndAddTo(Dest& dst, const votca::xtp::MatrixFreeOperator& op,
                            const Vtype& v, const Scalar& alpha) {
    // returns dst = alpha * op * v
    // alpha must be 1 here
    assert(alpha == Scalar(1) && "scaling is not implemented");
    EIGEN_ONLY_USED_FOR_DEBUG(alpha);
    if (op.useRow()) {
// make the mat vect product
#pragma omp parallel for schedule(guided)
      for (Index i = 0; i < op.rows(); i++) {
        dst(i) = op.OperatorRow(i) * v;
      }
    }

    if (op.useBlock()) {
      Index blocksize = op.getBlocksize();
      if (op.size() % blocksize != 0) {
        throw std::runtime_error("blocksize is not a multiple of matrix size");
      }
      Index blocks = op.size() / blocksize;

// this is inefficient if blocks<num_ofthreads
#pragma omp parallel for schedule(guided)
      for (Index i_row = 0; i_row < blocks; i_row++) {
        for (Index i_col = 0; i_col < blocks; i_col++) {
          dst.segment(i_row * blocksize, blocksize) +=
              op.OperatorBlock(i_row, i_col) *
              v.segment(i_col * blocksize, blocksize);
        }
      }
    }
  }
};

// replacement of the mat*mat operation
template <typename Mtype>
struct generic_product_impl<votca::xtp::MatrixFreeOperator, Mtype, DenseShape,
                            DenseShape, GemmProduct>
    : generic_product_impl_base<
          votca::xtp::MatrixFreeOperator, Mtype,
          generic_product_impl<votca::xtp::MatrixFreeOperator, Mtype>> {

  typedef
      typename Product<votca::xtp::MatrixFreeOperator, Mtype>::Scalar Scalar;

  template <typename Dest>
  static void scaleAndAddTo(Dest& dst, const votca::xtp::MatrixFreeOperator& op,
                            const Mtype& m, const Scalar& alpha) {
    // returns dst = alpha * op * v
    // alpha must be 1 here
    assert(alpha == Scalar(1) && "scaling is not implemented");
    EIGEN_ONLY_USED_FOR_DEBUG(alpha);

    // make the mat mat product
    if (op.useRow()) {
#pragma omp parallel for
      for (Index i = 0; i < op.rows(); i++) {
        const Eigen::RowVectorXd row = op.OperatorRow(i) * m;
        dst.row(i) = row;
      }
    }

    if (op.useBlock()) {
      Index blocksize = op.getBlocksize();
      if (op.size() % blocksize != 0) {
        throw std::runtime_error("blocksize is not a multiple of matrix size");
      }
      Index blocks = op.size() / blocksize;
      // this uses the fact that all our matrices are symmetric, i.e we can
      // reuse half the blocks
      std::vector<Eigen::MatrixXd> thread_wiseresult(
          votca::xtp::OPENMP::getMaxThreads(),
          Eigen::MatrixXd::Zero(dst.rows(), dst.cols()));
#pragma omp parallel for schedule(guided)
      for (Index i_row = 0; i_row < blocks; i_row++) {
        for (Index i_col = i_row; i_col < blocks; i_col++) {
          Eigen::MatrixXd blockmat = op.OperatorBlock(i_row, i_col);
          thread_wiseresult[votca::xtp::OPENMP::getThreadId()].block(
              i_row * blocksize, 0, blocksize, dst.cols()) +=
              blockmat * m.block(i_col * blocksize, 0, blocksize, m.cols());
          if (i_row != i_col) {
            thread_wiseresult[votca::xtp::OPENMP::getThreadId()].block(
                i_col * blocksize, 0, blocksize, dst.cols()) +=
                blockmat.transpose() *
                m.block(i_row * blocksize, 0, blocksize, m.cols());
          }
        }
      }
      for (const Eigen::MatrixXd& mat : thread_wiseresult) {
        dst += mat;
      }
    }
  }
};
}  // namespace internal
}  // namespace Eigen

#endif  //__VOTCA_TOOLS_MATRIX_FREE_OPERATOR_H
