#ifndef VIENNACL_LINALG_DETAIL_AMG_AMG_COARSE_HPP
#define VIENNACL_LINALG_DETAIL_AMG_AMG_COARSE_HPP

/* =========================================================================
   Copyright (c) 2010-2014, Institute for Microelectronics,
                            Institute for Analysis and Scientific Computing,
                            TU Wien.
   Portions of this software are copyright by UChicago Argonne, LLC.

                            -----------------
                  ViennaCL - The Vienna Computing Library
                            -----------------

   Project Head:    Karl Rupp                   rupp@iue.tuwien.ac.at

   (A list of authors and contributors can be found in the PDF manual)

   License:         MIT (X11), see file LICENSE in the base directory
============================================================================= */

/** @file amg_coarse.hpp
    @brief Implementations of several variants of the AMG coarsening procedure (setup phase). Experimental.
*/

#include <cmath>
#include "viennacl/linalg/detail/amg/amg_base.hpp"

#include <map>
#ifdef VIENNACL_WITH_OPENMP
#include <omp.h>
#endif

#include "viennacl/linalg/detail/amg/amg_debug.hpp"

namespace viennacl
{
  namespace linalg
  {
    namespace detail
    {
      namespace amg
      {

    /** @brief Calls the right coarsening procedure
      * @param level    Coarse level identifier
      * @param A    Operator matrix on all levels
      * @param Pointvector   Vector of points on all levels
      * @param Slicing    Partitioning of the system matrix to different processors (only used in RS0 and RS3)
      * @param tag    AMG preconditioner tag
      */
    template <typename InternalType1, typename InternalType2, typename InternalType3>
    void amg_coarse(unsigned int level, InternalType1 & A, InternalType2 & Pointvector, InternalType3 & Slicing, amg_tag & tag)
    {
      switch (tag.get_coarse())
      {
        case VIENNACL_AMG_COARSE_RS: amg_coarse_classic (level, A, Pointvector, tag); break;
        case VIENNACL_AMG_COARSE_ONEPASS: amg_coarse_classic_onepass (level, A, Pointvector, tag); break;
        case VIENNACL_AMG_COARSE_RS0: amg_coarse_rs0 (level, A, Pointvector, Slicing, tag); break;
        case VIENNACL_AMG_COARSE_RS3: amg_coarse_rs3 (level, A, Pointvector, Slicing, tag); break;
        case VIENNACL_AMG_COARSE_AG:   amg_coarse_ag (level, A, Pointvector, tag); break;
      }
    }

    /** @brief Determines strong influences in system matrix, classical approach (RS). Multithreaded!
    * @param level    Coarse level identifier
    * @param A      Operator matrix on all levels
    * @param Pointvector   Vector of points on all levels
    * @param tag    AMG preconditioner tag
    */
    template <typename InternalType1, typename InternalType2>
    void amg_influence(unsigned int level, InternalType1 const & A, InternalType2 & Pointvector, amg_tag & tag)
    {
      typedef typename InternalType1::value_type SparseMatrixType;
      typedef typename InternalType2::value_type PointVectorType;
      typedef typename SparseMatrixType::value_type ScalarType;
      typedef typename SparseMatrixType::value_type ScalarType;
      typedef typename SparseMatrixType::const_iterator1 ConstRowIterator;
      typedef typename SparseMatrixType::const_iterator2 ConstColIterator;

      ScalarType max;
      int diag_sign;
      //unsigned int i;

#ifdef VIENNACL_WITH_OPENMP
      #pragma omp parallel for private (max,diag_sign)
#endif
      for (long i=0; i<static_cast<long>(A[level].size1()); ++i)
      {
        diag_sign = 1;
        if (A[level](i,i) < 0)
          diag_sign = -1;

        ConstRowIterator row_iter = A[level].begin1();
        row_iter += i;
        // Find greatest non-diagonal negative value (positive if diagonal is negative) in row
        max = 0;
        for (ConstColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
        {
            if (i == (unsigned int) col_iter.index2()) continue;
            if (diag_sign == 1)
              if (max > *col_iter)  max = *col_iter;
            if (diag_sign == -1)
              if (max < *col_iter)  max = *col_iter;
        }

        // If maximum is 0 then the row is independent of the others
        if (max == 0)
          continue;

        // Find all points that strongly influence current point (Yang, p.5)
        for (ConstColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
        {
          unsigned int j = static_cast<unsigned int>(col_iter.index2());
          if (i == j) continue;
          if (diag_sign * (-*col_iter) >= tag.get_threshold() * (diag_sign * (-max)))
          {
            // Strong influence from j to i found, save information
            Pointvector[level][i]->add_influencing_point(Pointvector[level][j]);
          }
        }
      }

      #ifdef VIENNACL_AMG_DEBUG
      std::cout << "Influence Matrix: " << std::endl;
      boost::numeric::ublas::matrix<bool> mat;
      Pointvector[level].get_influence_matrix(mat);
      printmatrix (mat);
      #endif

      // Save influenced points
      for (typename PointVectorType::iterator iter = Pointvector[level].begin(); iter != Pointvector[level].end(); ++iter)
      {
        for (typename amg_point::iterator iter2 = (*iter)->begin_influencing(); iter2 != (*iter)->end_influencing(); ++iter2)
        {
          (*iter2)->add_influenced_point(*iter);
        }
      }

      #ifdef VIENNACL_AMG_DEBUG
      std::cout << "Influence Measures: " << std::endl;
      boost::numeric::ublas::vector<unsigned int> temp;
      Pointvector[level].get_influence(temp);
      printvector (temp);
      std::cout << "Point Sorting: " << std::endl;
      Pointvector[level].get_sorting(temp);
      printvector (temp);
      #endif
    }

    /** @brief Classical (RS) one-pass coarsening. Single-Threaded! (VIENNACL_AMG_COARSE_CLASSIC_ONEPASS)
    * @param level     Course level identifier
    * @param A      Operator matrix on all levels
    * @param Pointvector   Vector of points on all levels
    * @param tag    AMG preconditioner tag
    */
    template <typename InternalType1, typename InternalType2>
    void amg_coarse_classic_onepass(unsigned int level, InternalType1 & A, InternalType2 & Pointvector, amg_tag & tag)
    {
      amg_point* c_point, *point1, *point2;

      // Check and save all strong influences
      amg_influence (level, A, Pointvector, tag);

      // Traverse through points and calculate initial influence measure
      long i;
#ifdef VIENNACL_WITH_OPENMP
      #pragma omp parallel for private (i)
#endif
      for (i=0; i<static_cast<long>(Pointvector[level].size()); ++i)
  Pointvector[level][i]->calc_influence();

       // Do initial sorting
      Pointvector[level].sort();

      // Get undecided point with highest influence measure
      while ((c_point = Pointvector[level].get_nextpoint()) != NULL)
      {
        // Make this point C point
        Pointvector[level].make_cpoint(c_point);

        // All strongly influenced points become F points
        for (typename amg_point::iterator iter = c_point->begin_influenced(); iter != c_point->end_influenced(); ++iter)
        {
          point1 = *iter;
          // Found strong influence from C point (c_point influences point1), check whether point is still undecided, otherwise skip
          if (!point1->is_undecided()) continue;
          // Make this point F point if it is still undecided point
          Pointvector[level].make_fpoint(point1);

          // Add +1 to influence measure for all undecided points that strongly influence new F point
          for (typename amg_point::iterator iter2 = point1->begin_influencing(); iter2 != point1->end_influencing(); ++iter2)
          {
            point2 = *iter2;
            // Found strong influence to F point (point2 influences point1)
            if (point2->is_undecided())
              Pointvector[level].add_influence(point2,1);
          }
        }
      }

      // If a point is neither C nor F point but is nevertheless influenced by other points make it F point
      // (this situation can happen when this point does not influence other points and the points that influence this point became F points already)
      /*#pragma omp parallel for private (i,point1)
      for (long i=0; i<static_cast<long>(Pointvector[level].size()); ++i)
      {
        point1 = Pointvector[level][i];
        if (point1->is_undecided())
        {
          // Undecided point found. Check whether it is influenced by other point and if so: Make it F point.
          if (point1->number_influencing() > 0)
          {
            #pragma omp critical
            Pointvector[level].make_fpoint(point1);
          }
        }
      }*/

      #if defined (VIENNACL_AMG_DEBUG)//  or defined (VIENNACL_AMG_DEBUGBENCH)
      unsigned int c_points = Pointvector[level].get_cpoints();
      unsigned int f_points = Pointvector[level].get_fpoints();
      std::cout << "1st pass: Level " << level << ": ";
      std::cout << "No of C points = " << c_points << ", ";
      std::cout << "No of F points = " << f_points << std::endl;
      #endif

      #ifdef VIENNACL_AMG_DEBUG
      std::cout << "Coarse Points:" << std::endl;
      boost::numeric::ublas::vector<bool> C;
      Pointvector[level].get_C(C);
      printvector (C);
      std::cout << "Fine Points:" << std::endl;
      boost::numeric::ublas::vector<bool> F;
      Pointvector[level].get_F(F);
      printvector (F);
      #endif
    }

    /** @brief Classical (RS) two-pass coarsening. Single-Threaded! (VIENNACL_AMG_COARSE_CLASSIC)
    * @param level    Coarse level identifier
    * @param A      Operator matrix on all levels
    * @param Pointvector   Vector of points on all levels
    * @param tag    AMG preconditioner tag
    */
    template <typename InternalType1, typename InternalType2>
    void amg_coarse_classic(unsigned int level, InternalType1 & A, InternalType2 & Pointvector, amg_tag & tag)
    {
      typedef typename InternalType2::value_type PointVectorType;

      bool add_C;
      amg_point *c_point, *point1, *point2;

      // Use one-pass-coarsening as first pass.
      amg_coarse_classic_onepass(level, A, Pointvector, tag);

      // 2nd pass: Add more C points if F-F connection does not have a common C point.
      for (typename PointVectorType::iterator iter = Pointvector[level].begin(); iter != Pointvector[level].end(); ++iter)
      {
        point1 = *iter;
        // If point is F point, check for strong connections.
        if (point1->is_fpoint())
        {
          // Check for strong connections from influencing and influenced points.
          amg_point::iterator iter2 = point1->begin_influencing();
          amg_point::iterator iter3 = point1->begin_influenced();

          // Iterate over both lists at once. This makes sure that points are no checked twice when influence relation is symmetric (which is often the case).
          // Note: Only works because influencing and influenced lists are sorted by point-index.
          while(iter2 != point1->end_influencing() || iter3 != point1->end_influenced())
          {
            if (iter2 == point1->end_influencing())
            {
              point2 = *iter3;
              ++iter3;
            }
            else if (iter3 == point1->end_influenced())
            {
              point2 = *iter2;
              ++iter2;
            }
            else
            {
              if ((*iter2)->get_index() == (*iter3)->get_index())
              {
                point2 = *iter2;
                ++iter2;
                ++iter3;
              }
              else if ((*iter2)->get_index() < (*iter3)->get_index())
              {
                point2 = *iter2;
                ++iter2;
              }
              else
              {
                point2 = *iter3;
                ++iter3;
              }
            }
            // Only check points with higher index as points with lower index have been checked already.
            if (point2->get_index() < point1->get_index())
              continue;

            // If there is a strong connection then it has to either be a C point or a F point with common C point.
            // C point? Then skip as everything is ok.
            if (point2->is_cpoint())
              continue;
            // F point? Then check whether F points point1 and point2 have a common C point.
            if (point2->is_fpoint())
            {
              add_C = true;
              // C point is common for two F points if they are both strongly influenced by that C point.
              // Compare strong influences for point1 and point2.
              for (amg_point::iterator iter3 = point1->begin_influencing(); iter3 != point1 -> end_influencing(); ++iter3)
              {
                c_point = *iter3;
                // Stop search when strong common influence is found via c_point.
                if (c_point->is_cpoint())
                {
                  if (point2->is_influencing(c_point))
                  {
                    add_C = false;
                    break;
                  }
                }
              }
              // No common C point found? Then make second F point to C point.
              if (add_C == true)
                Pointvector[level].switch_ftoc(point2);
            }
          }
        }
      }

      #ifdef VIENNACL_AMG_DEBUG
      std::cout << "After 2nd pass:" << std::endl;
      std::cout << "Coarse Points:" << std::endl;
      boost::numeric::ublas::vector<bool> C;
      Pointvector[level].get_C(C);
      printvector (C);
      std::cout << "Fine Points:" << std::endl;
      boost::numeric::ublas::vector<bool> F;
      Pointvector[level].get_F(F);
      printvector (F);
      #endif

      #ifdef VIENNACL_AMG_DEBUG
#ifdef VIENNACL_WITH_OPENMP
      #pragma omp critical
#endif
      {
      std::cout << "No C and no F point: ";
      for (typename PointVectorType::iterator iter = Pointvector[level].begin(); iter != Pointvector[level].end(); ++iter)
  if ((*iter)->is_undecided())
    std::cout << (*iter)->get_index() << " ";
      std::cout << std::endl;
      }
      #endif
    }

    /** @brief Parallel classical RS0 coarsening. Multi-Threaded! (VIENNACL_AMG_COARSE_RS0 || VIENNACL_AMG_COARSE_RS3)
    * @param level    Coarse level identifier
    * @param A      Operator matrix on all level
    * @param Pointvector   Vector of points on all levels
    * @param Slicing    Partitioning of the system matrix and the other data structures to different processors
    * @param tag    AMG preconditioner tag
    */
    template <typename InternalType1, typename InternalType2, typename InternalType3>
    void amg_coarse_rs0(unsigned int level, InternalType1 & A, InternalType2 & Pointvector, InternalType3 & Slicing, amg_tag & tag)
    {
      unsigned int total_points;

      // Slice matrix into parts such that points are distributed among threads
      Slicing.slice(level, A, Pointvector);

      // Run classical coarsening in parallel
      total_points = 0;
#ifdef VIENNACL_WITH_OPENMP
      #pragma omp parallel for
#endif
      for (long i=0; i<static_cast<long>(Slicing.threads_); ++i)
      {
        amg_coarse_classic(level,Slicing.A_slice[i],Slicing.Pointvector_slice[i],tag);

        // Save C points (using Slicing.Offset on the next level as temporary memory)
        // Note: Number of C points for point i is saved in i+1!! (makes it easier later to compute offset)
        Slicing.Offset[i+1][level+1] = Slicing.Pointvector_slice[i][level].get_cpoints();
      #ifdef VIENNACL_WITH_OPENMP
        #pragma omp critical
      #endif
        total_points += Slicing.Pointvector_slice[i][level].get_cpoints();
      }

      // If no coarser level can be found on any level then resume and coarsening will stop in amg_coarse()
      if (total_points != 0)
      {
      #ifdef VIENNACL_WITH_OPENMP
        #pragma omp parallel for
      #endif
        for (long i=0; i<static_cast<long>(Slicing.threads_); ++i)
        {
          // If no higher coarse level can be found on slice i (saved in Slicing.Offset[i+1][level+1]) then pull C point(s) to the next level
          if (Slicing.Offset[i+1][level+1] == 0)
          {
            // All points become C points
            for (unsigned int j=0; j<Slicing.A_slice[i][level].size1(); ++j)
              Slicing.Pointvector_slice[i][level].make_cpoint(Slicing.Pointvector_slice[i][level][j]);
            Slicing.Offset[i+1][level+1] = static_cast<unsigned int>(Slicing.A_slice[i][level].size1());
          }
        }

        // Build slicing offset from number of C points (offset = total sum of C points on threads with lower number)
        for (unsigned int i=2; i<=Slicing.threads_; ++i)
          Slicing.Offset[i][level+1] += Slicing.Offset[i-1][level+1];

        // Join C and F points
        Slicing.join(level, Pointvector);
      }

      // Calculate global influence measures for interpolation and/or RS3.
      amg_influence(level, A, Pointvector, tag);

      #if defined(VIENNACL_AMG_DEBUG)// or defined (VIENNACL_AMG_DEBUGBENCH)
      for (unsigned int i=0; i<Slicing._threads; ++i)
      {
        unsigned int c_points = Slicing.Pointvector_slice[i][level].get_cpoints();
        unsigned int f_points = Slicing.Pointvector_slice[i][level].get_fpoints();
        std::cout << "Thread " << i << ": ";
        std::cout << "No of C points = " << c_points << ", ";
        std::cout << "No of F points = " << f_points << std::endl;
      }
      #endif
    }

    /** @brief RS3 coarsening. Single-Threaded! (VIENNACL_AMG_COARSE_RS3)
    * @param level    Coarse level identifier
    * @param A      Operator matrix on all levels
    * @param Pointvector   Vector of points on all levels
    * @param Slicing    Partitioning of the system matrix and the other data structures to different processors
    * @param tag    AMG preconditioner tag
    */
    template <typename InternalType1, typename InternalType2, typename InternalType3>
    void amg_coarse_rs3(unsigned int level, InternalType1 & A, InternalType2 & Pointvector, InternalType3 & Slicing, amg_tag & tag)
    {
      amg_point *c_point, *point1, *point2;
      bool add_C;
      unsigned int i, j;

      // Run RS0 first (parallel).
      amg_coarse_rs0(level, A, Pointvector, Slicing, tag);

      // Save slicing offset
      boost::numeric::ublas::vector<unsigned int> Offset = boost::numeric::ublas::vector<unsigned int> (Slicing.Offset.size());
      for (i=0; i<Slicing.Offset.size(); ++i)
        Offset[i] = Slicing.Offset[i][level];

      // Correct the coarsening with a third pass: Don't allow strong F-F connections without common C point
      for (i=0; i<Slicing.threads_; ++i)
      {
      //for (j=Slicing.Offset[i][level]; j<Slicing.Offset[i+1][level]; ++j)
      for (j=Offset[i]; j<Offset[i+1]; ++j)
      {
        point1 = Pointvector[level][j];
        // If point is F point, check for strong connections.
        if (point1->is_fpoint())
        {
          // Check for strong connections from influencing and influenced points.
          amg_point::iterator iter2 = point1->begin_influencing();
          amg_point::iterator iter3 = point1->begin_influenced();

          // Iterate over both lists at once. This makes sure that points are no checked twice when influence relation is symmetric (which is often the case).
          // Note: Only works because influencing and influenced lists are sorted by point-index.
          while(iter2 != point1->end_influencing() || iter3 != point1->end_influenced())
          {
            if (iter2 == point1->end_influencing())
            {
              point2 = *iter3;
              ++iter3;
            }
            else if (iter3 == point1->end_influenced())
            {
              point2 = *iter2;
              ++iter2;
            }
            else
            {
              if ((*iter2)->get_index() == (*iter3)->get_index())
              {
                point2 = *iter2;
                ++iter2;
                ++iter3;
              }
              else if ((*iter2)->get_index() < (*iter3)->get_index())
              {
                point2 = *iter2;
                ++iter2;
              }
              else
              {
                point2 = *iter3;
                ++iter3;
              }
            }

            // Only check points with higher index as points with lower index have been checked already.
            if (point2->get_index() < point1->get_index())
              continue;

            // Only check points that are outside the slicing boundaries (interior F-F connections have already been checked in second pass)
            //if (point2->get_index() >= Slicing.Offset[i][level] || point2->get_index() < Slicing.Offset[i+1][level])
            if (point2->get_index() >= Offset[i] && point2->get_index() < Offset[i+1])
              continue;

            // If there is a strong connection then it has to either be a C point or a F point with common C point.
            // C point? Then skip as everything is ok.
            if (point2->is_cpoint())
              continue;
            // F point? Then check whether F points point1 and point2 have a common C point.
            if (point2->is_fpoint())
            {
              add_C = true;
              // C point is common for two F points if they are both strongly influenced by that C point.
              // Compare strong influences for point1 and point2.
              for (amg_point::iterator iter3 = point1->begin_influencing(); iter3 != point1 -> end_influencing(); ++iter3)
              {
                c_point = *iter3;
                // Stop search when strong common influence is found via c_point.
                if (c_point->is_cpoint())
                {
                  if (point2->is_influencing(c_point))
                  {
                    add_C = false;
                    break;
                  }
                }
              }
              // No common C point found? Then make second F point to C point.
              if (add_C == true)
              {
                Pointvector[level].switch_ftoc(point2);
                // Add +1 to offsets as one C point has been added.
                for (unsigned int j=i+1; j<=Slicing.threads_; ++j)
                  Slicing.Offset[j][level+1]++;
              }
                  }
                }
              }
            }
          }

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "After 3rd pass:" << std::endl;
          std::cout << "Coarse Points:" << std::endl;
          boost::numeric::ublas::vector<bool> C;
          Pointvector[level].get_C(C);
          printvector (C);
          std::cout << "Fine Points:" << std::endl;
          boost::numeric::ublas::vector<bool> F;
          Pointvector[level].get_F(F);
          printvector (F);
          #endif
        }

        /** @brief AG (aggregation based) coarsening. Single-Threaded! (VIENNACL_AMG_COARSE_SA)
        *
        * @param level    Coarse level identifier
        * @param A      Operator matrix on all levels
        * @param Pointvector   Vector of points on all levels
        * @param tag    AMG preconditioner tag
        */
        template <typename InternalType1, typename InternalType2>
        void amg_coarse_ag(unsigned int level, InternalType1 & A, InternalType2 & Pointvector, amg_tag & tag)
        {
          typedef typename InternalType1::value_type SparseMatrixType;
          typedef typename InternalType2::value_type PointVectorType;
          typedef typename SparseMatrixType::value_type ScalarType;

          typedef typename SparseMatrixType::iterator1 InternalRowIterator;
          typedef typename SparseMatrixType::iterator2 InternalColIterator;

          long x,y;
          ScalarType diag;
          amg_point *pointx, *pointy;

          // Cannot determine aggregates if size == 1 as then a new aggregate would always consist of this point (infinite loop)
          if (A[level].size1() == 1) return;

          // SA algorithm (Vanek et al. p.6)
          // Build neighborhoods
    #ifdef VIENNACL_WITH_OPENMP
          #pragma omp parallel for private (x,y,diag)
    #endif
          for (x=0; x<static_cast<long>(A[level].size1()); ++x)
          {
            InternalRowIterator row_iter = A[level].begin1();
            row_iter += x;
            diag = A[level](x,x);
            for (InternalColIterator col_iter = row_iter.begin(); col_iter != row_iter.end(); ++col_iter)
            {
              y = static_cast<long>(col_iter.index2());
              if (y == x || (std::fabs(*col_iter) >= tag.get_threshold()*pow(0.5, static_cast<double>(level-1)) * std::sqrt(std::fabs(diag*A[level](y,y)))))
              {
                // Neighborhood x includes point y
                Pointvector[level][x]->add_influencing_point(Pointvector[level][y]);
              }
            }
          }

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "Neighborhoods:" << std::endl;
          boost::numeric::ublas::matrix<bool> mat;
          Pointvector[level].get_influence_matrix(mat);
          printmatrix (mat);
          #endif

          // Build aggregates from neighborhoods
          for (typename PointVectorType::iterator iter = Pointvector[level].begin(); iter != Pointvector[level].end(); ++iter)
          {
            pointx = (*iter);

            if (pointx->is_undecided())
            {
              // Make center of aggregate to C point and include it to aggregate x.
              Pointvector[level].make_cpoint(pointx);
              pointx->set_aggregate (pointx->get_index());
              for (amg_point::iterator iter2 = pointx->begin_influencing(); iter2 != pointx->end_influencing(); ++iter2)
              {
              pointy = (*iter2);

                if (pointy->is_undecided())
                {
                  // Make neighbor y to F point and include it to aggregate x.
                  Pointvector[level].make_fpoint(pointy);
                  pointy->set_aggregate (pointx->get_index());
                }
              }
            }
          }

          #ifdef VIENNACL_AMG_DEBUG
          std::cout << "After aggregation:" << std::endl;
          std::cout << "Coarse Points:" << std::endl;
          boost::numeric::ublas::vector<bool> C;
          Pointvector[level].get_C(C);
          printvector (C);
          std::cout << "Fine Points:" << std::endl;
          boost::numeric::ublas::vector<bool> F;
          Pointvector[level].get_F(F);
          printvector (F);
          std::cout << "Aggregates:" << std::endl;
          printvector (Aggregates[level]);
          #endif
        }
      } //namespace amg
    }
  }
}

#endif
