/*  This file is part of the OpenLB library
 *
 *  Copyright (C) 2012-2017 Lukas Baron, Tim Dornieden, Mathias J. Krause,
 *  Albert Mink, Fabian Klemens, Benjamin Förster, Marie-Luise Maier,
 *  Adrian Kummerlaender
 *  E-mail contact: info@openlb.net
 *  The most recent release of OpenLB can be downloaded at
 *  
 *
 *  This program 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 2
 *  of the License, or (at your option) any later version.
 *
 *  This program 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 this program; if not, write to the Free
 *  Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *  Boston, MA  02110-1301, USA.
 */
#ifndef REDUCTION_F_3D_HH
#define REDUCTION_F_3D_HH
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#include 
#include "functors/lattice/reductionF3D.h"
#include "dynamics/lbHelpers.h"  // for computation of lattice rho and velocity
namespace olb {
template
SuperLatticeFfromAnalyticalF3D::SuperLatticeFfromAnalyticalF3D(
  FunctorPtr>&& f,
  SuperLattice3D&   sLattice)
  : SuperLatticeF3D(sLattice, f->getTargetDim()),
    _f(std::move(f))
{
  this->getName() = "fromAnalyticalF(" + _f->getName() + ")";
  LoadBalancer&     load   = sLattice.getLoadBalancer();
  CuboidGeometry3D& cuboid = sLattice.getCuboidGeometry();
  for (int iC = 0; iC < load.size(); ++iC) {
    this->_blockF.emplace_back(
      new BlockLatticeFfromAnalyticalF3D(
        *_f,
        sLattice.getExtendedBlockLattice(iC),
        cuboid.get(load.glob(iC)))
    );
  }
}
template
bool SuperLatticeFfromAnalyticalF3D::operator()(
  T output[], const int input[])
{
  T physR[3] = {};
  this->_sLattice.getCuboidGeometry().getPhysR(physR,input);
  return _f(output,physR);
}
template
BlockLatticeFfromAnalyticalF3D::BlockLatticeFfromAnalyticalF3D(
  AnalyticalF3D&                    f,
  BlockLatticeStructure3D& lattice,
  Cuboid3D&                            cuboid)
  : BlockLatticeF3D(lattice, f.getTargetDim()),
    _f(f),
    _cuboid(cuboid)
{
  this->getName() = "blockFfromAnalyticalF(" + _f.getName() + ")";
}
template
bool BlockLatticeFfromAnalyticalF3D::operator()(
  T output[], const int input[])
{
  T physR[3] = {};
  _cuboid.getPhysR(physR,input);
  return _f(output,physR);
}
//////////// not yet working // symbolically ///////////////////
////////////////////////////////////////////////
template
SmoothBlockIndicator3D::SmoothBlockIndicator3D(
  IndicatorF3D& f, T h, T eps, int wa)
  : BlockDataF3D((int)((f.getMax()[0] - f.getMin()[0]) / h + (wa+1)*eps)+2,
                       (int)((f.getMax()[1] - f.getMin()[1]) / h + (wa+1)*eps)+2,
                       (int)((f.getMax()[2] - f.getMin()[2]) / h + (wa+1)*eps)+2),
    _f(f),
    _h(h),
    _eps(eps),
    _wa(wa)
{
  this->getName() = "SmoothBlockIndicator3D";
  // shrink epsilon boundary to one size
  this->_eps = this->_eps/((this->_wa-1)/2.0);
  T value;
  // Important parameter of the gaussian psf (point spread function), but adaption should not be necessary
  T sigma = 1.0;
  T weights[this->_wa][this->_wa][this->_wa];
  T sum = 0;
  int iStart = floor(this->_wa/2.0);
  int iEnd = ceil(this->_wa/2.0);
  // calculate weights: they are constants, but calculation here is less error-prone than hardcoding these parameters
  for (int x = -iStart; x < iEnd; x++) {
    for (int y = -iStart; y < iEnd; y++) {
      for (int z = -iStart; z < iEnd; z++) {
        weights[x+iStart][y+iStart][z+iStart] = exp(-(x*x+y*y+z*z)/(2*sigma*sigma)) / (pow(sigma,3)*sqrt(pow(2,3)*pow(M_PI,3)));
        // important because sum of all weigths only equals 1 for this->_wa -> infinity
        sum += weights[x+iStart][y+iStart][z+iStart];
      }
    }
  }
  for (int iX=0; iXgetBlockData().getNx(); iX++) {
    for (int iY=0; iYgetBlockData().getNy(); iY++) {
      for (int iZ=0; iZgetBlockData().getNz(); iZ++) {
        bool output[1];
        value = 0;
        // input: regarded point (centre)
        T input[] = {
          _f.getMin()[0] + (iX-iStart*_eps)*_h,
          _f.getMin()[1] + (iY-iStart*_eps)*_h,
          _f.getMin()[2] + (iZ-iStart*_eps)*_h
        };
        /*
         * three loops to look at every point (which weight is not 0) around the regarded point
         * sum all weighted porosities
         */
        for (int x = -iStart; x < iEnd; x++) {
          for (int y = -iStart; y < iEnd; y++) {
            for (int z = -iStart; z < iEnd; z++) {
              // move from regarded point to point in neighborhood
              input[0] += x*_eps*_h;
              input[1] += y*_eps*_h;
              input[2] += z*_eps*_h;
              // get porosity
              _f(output,input);
              // sum porosity
              value += output[0] * weights[x+iStart][y+iStart][z+iStart];
              // move back to centre
              input[0] -= x*_eps*_h;
              input[1] -= y*_eps*_h;
              input[2] -= z*_eps*_h;
            }
          }
        }
        /*
         * Round to 3 decimals
         * See above sum != 1.0, that's the reason for devision, otherwise porosity will never reach 0
         */
        this->getBlockData().get(iX,iY,iZ,0) = nearbyint(1000*value/sum)/1000.0;
      }
    }
  }
}
/*
template
bool SmoothBlockIndicator3D::operator()(
  T output[], const int input[])
{
  T physR[3] = {};
  _superGeometry.getPhysR(physR,input[0],input[1],input[2] );
  _f(output,physR);
  return true;
}*/
template
SuperLatticeInterpPhysVelocity3Degree3D::
SuperLatticeInterpPhysVelocity3Degree3D(
  SuperLattice3D& sLattice, UnitConverter& conv, int range)
  : SuperLatticeF3D(sLattice, 3)
{
  this->getName() = "Interp3DegreeVelocity";
  int maxC = this->_sLattice.getLoadBalancer().size();
  this->_blockF.reserve(maxC);
  for (int iC = 0; iC < maxC; iC++) {
    BlockLatticeInterpPhysVelocity3Degree3D* foo =
      new BlockLatticeInterpPhysVelocity3Degree3D(
      sLattice.getExtendedBlockLattice(iC),
      conv,
      &sLattice.getCuboidGeometry().get(this->_sLattice.getLoadBalancer().
                                        glob(iC)),
      sLattice.getOverlap(),
      range);
    _bLattices.push_back(foo);
  }
}
template
void SuperLatticeInterpPhysVelocity3Degree3D::operator()(
  T output[], const T input[], const int iC)
{
  _bLattices[this->_sLattice.getLoadBalancer().loc(iC)]->operator()(output,
      input);
}
template
BlockLatticeInterpPhysVelocity3Degree3D::
BlockLatticeInterpPhysVelocity3Degree3D(
  BlockLatticeStructure3D& blockLattice, UnitConverter& conv,
  Cuboid3D* c, int overlap, int range)
  : BlockLatticeF3D(blockLattice, 3),
    _conv(conv),
    _cuboid(c),
    _overlap(overlap),
    _range(range)
{
  this->getName() = "BlockLatticeInterpVelocity3Degree3D";
}
template
BlockLatticeInterpPhysVelocity3Degree3D::
BlockLatticeInterpPhysVelocity3Degree3D(
  const BlockLatticeInterpPhysVelocity3Degree3D& rhs) :
  BlockLatticeF3D(rhs._blockLattice, 3),
  _conv(rhs._conv),
  _cuboid(rhs._cuboid),
  _overlap(rhs._overlap),
  _range(rhs._range)
{
}
template
void BlockLatticeInterpPhysVelocity3Degree3D::operator()(
  T output[3], const T input[3])
{
  T u[3], rho, volume;
  int latIntPos[3] = {0};
  T latPhysPos[3] = {T()};
  _cuboid->getFloorLatticeR(latIntPos, &input[0]);
  _cuboid->getPhysR(latPhysPos, latIntPos);
  latIntPos[0] += _overlap;
  latIntPos[1] += _overlap;
  latIntPos[2] += _overlap;
  volume=T(1);
  for (int i = -_range; i <= _range+1; ++i) {
    for (int j = -_range; j <= _range+1; ++j) {
      for (int k = -_range; k <= _range+1; ++k) {
        this->_blockLattice.get(latIntPos[0]+i, latIntPos[1]+j,
                                latIntPos[2]+k).computeRhoU(rho, u);
        for (int l = -_range; l <= _range+1; ++l) {
          if (l != i) {
            volume *= (input[0] - (latPhysPos[0]+ l *_cuboid->getDeltaR()))
                      / (latPhysPos[0] + i *_cuboid->getDeltaR()
                         - (latPhysPos[0] + l *_cuboid->getDeltaR()));
          }
        }
        for (int m = -_range; m <= _range+1; ++m) {
          if (m != j) {
            volume *= (input[1]
                       - (latPhysPos[1] + m *_cuboid->getDeltaR()))
                      / (latPhysPos[1] + j * _cuboid->getDeltaR()
                         - (latPhysPos[1] + m * _cuboid->getDeltaR()));
          }
        }
        for (int n = -_range; n <= _range+1; ++n) {
          if (n != k) {
            volume *= (input[2]
                       - (latPhysPos[2] + n * _cuboid->getDeltaR()))
                      / (latPhysPos[2] + k * _cuboid->getDeltaR()
                         - (latPhysPos[2] + n * _cuboid->getDeltaR()));
          }
        }
        output[0] += u[0] * volume;
        output[1] += u[1] * volume;
        output[2] += u[2] * volume;
        volume=T(1);
      }
    }
  }
  output[0] = _conv.getPhysVelocity(output[0]);
  output[1] = _conv.getPhysVelocity(output[1]);
  output[2] = _conv.getPhysVelocity(output[2]);
}
template
SuperLatticeInterpDensity3Degree3D::SuperLatticeInterpDensity3Degree3D(
  SuperLattice3D& sLattice, SuperGeometry3D& sGeometry,
  UnitConverter& conv, int range) :
  SuperLatticeF3D(sLattice, 3)
{
  this->getName() = "Interp3DegreeDensity";
  int maxC = this->_sLattice.getLoadBalancer().size();
  this->_blockF.reserve(maxC);
  for (int lociC = 0; lociC < maxC; lociC++) {
    int globiC = this->_sLattice.getLoadBalancer().glob(lociC);
    BlockLatticeInterpDensity3Degree3D* foo =
      new BlockLatticeInterpDensity3Degree3D(
      sLattice.getExtendedBlockLattice(lociC),
      sGeometry.getExtendedBlockGeometry(lociC),
      conv,
      &sLattice.getCuboidGeometry().get(globiC),
      sLattice.getOverlap(), range);
    _bLattices.push_back(foo);
    if (sLattice.getOverlap() <= range + 1)
      std::cout << "lattice overlap has to be larger than (range + 1)"
                << std::endl;
  }
}
template
SuperLatticeInterpDensity3Degree3D::~SuperLatticeInterpDensity3Degree3D()
{
  // first deconstruct vector elements
  for ( auto it : _bLattices) {
    delete it;
  }
  // then delete std::vector
  _bLattices.clear();
}
template
void SuperLatticeInterpDensity3Degree3D::operator()(T output[],
    const T input[], const int globiC)
{
  if (this->_sLattice.getLoadBalancer().rank(globiC) == singleton::mpi().getRank()) {
    _bLattices[this->_sLattice.getLoadBalancer().loc(globiC)]->operator()(output,
        input);
  }
}
template
BlockLatticeInterpDensity3Degree3D::BlockLatticeInterpDensity3Degree3D(
  BlockLatticeStructure3D& blockLattice,
  BlockGeometryStructure3D& blockGeometry, UnitConverter& conv,
  Cuboid3D* c, int overlap, int range) :
  BlockLatticeF3D(blockLattice, 3), _blockGeometry(blockGeometry),
  _conv(conv), _cuboid(c), _overlap(overlap), _range(range)
{
  this->getName() = "BlockLatticeInterpDensity3Degree3D";
}
template
BlockLatticeInterpDensity3Degree3D::BlockLatticeInterpDensity3Degree3D(
  const BlockLatticeInterpDensity3Degree3D& rhs) :
  BlockLatticeF3D(rhs._blockLattice, 3),
  _blockGeometry(rhs._blockGeometry),_conv(rhs._conv), _cuboid(
    rhs._cuboid), _overlap(rhs._overlap), _range(rhs._range)
{
}
template
void BlockLatticeInterpDensity3Degree3D::operator()(
  T output[DESCRIPTOR::q], const T input[3])
{
  T volume = T(1);
  T f_iPop = 0.;
  /** neighbor position on grid of input value in lattice units
   *referred to local cuboid
   */
  int latIntPos[3] = { 0 };
  // neighbor position on grid of input value in physical units
  T latPhysPos[3] = { T() };
  // input is physical position on grid
  _cuboid->getFloorLatticeR(latIntPos, input);
  // latPhysPos is global physical position on geometry
  _cuboid->getPhysR(latPhysPos, latIntPos);
  // point on cuboid equals cell on blocklattice (extended) shifted by _overlap
  latIntPos[0] += _overlap;
  latIntPos[1] += _overlap;
  latIntPos[2] += _overlap;
  for (unsigned iPop = 0; iPop < DESCRIPTOR::q; ++iPop) {
    output[iPop] = T(0);
    for (int i = -_range; i <= _range + 1; ++i) {
      for (int j = -_range; j <= _range + 1; ++j) {
        for (int k = -_range; k <= _range + 1; ++k) {
          f_iPop = 0.;
          // just if material of cell != 1 there may be information of fluid density
          if (_blockGeometry.getMaterial(latIntPos[0] + i, latIntPos[1] + j,
                                         latIntPos[2] + k) != 0) {
            // because of communication it is possible to get density information
            // from neighboring cuboid
            f_iPop = this->_blockLattice.get(latIntPos[0] + i, latIntPos[1] + j,
                                             latIntPos[2] + k)[iPop];
          }
          for (int l = -_range; l <= _range + 1; ++l) {
            if (l != i) {
              volume *= (input[0] - (latPhysPos[0] + l * _cuboid->getDeltaR()))
                        / (latPhysPos[0] + i * _cuboid->getDeltaR()
                           - (latPhysPos[0] + l * _cuboid->getDeltaR()));
            }
          }
          for (int m = -_range; m <= _range + 1; ++m) {
            if (m != j) {
              volume *= (input[1] - (latPhysPos[1] + m * _cuboid->getDeltaR()))
                        / (latPhysPos[1] + j * _cuboid->getDeltaR()
                           - (latPhysPos[1] + m * _cuboid->getDeltaR()));
            }
          }
          for (int n = -_range; n <= _range + 1; ++n) {
            if (n != k) {
              volume *= (input[2] - (latPhysPos[2] + n * _cuboid->getDeltaR()))
                        / (latPhysPos[2] + k * _cuboid->getDeltaR()
                           - (latPhysPos[2] + n * _cuboid->getDeltaR()));
            }
          }
          output[iPop] += f_iPop * volume;
          volume = T(1);
        }
      }
    }
  }
}
template
SuperLatticeSmoothDiracDelta3D::SuperLatticeSmoothDiracDelta3D(
  SuperLattice3D& sLattice,
  UnitConverter& conv, SuperGeometry3D& sGeometry) :
  SuperLatticeF3D(sLattice, 3)
{
  this->getName() = "SuperLatticeSmoothDiracDelta3D";
  int maxC = this->_sLattice.getLoadBalancer().size();
  this->_blockF.reserve(maxC);
  for (int lociC = 0; lociC < maxC; lociC++) {
    int globiC = this->_sLattice.getLoadBalancer().glob(lociC);
    BlockLatticeSmoothDiracDelta3D* foo =
      new BlockLatticeSmoothDiracDelta3D(
      sLattice.getExtendedBlockLattice(lociC),
      conv, &sLattice.getCuboidGeometry().get(globiC)
    );
    _bLattices.push_back(foo);
  }
}
template
SuperLatticeSmoothDiracDelta3D::~SuperLatticeSmoothDiracDelta3D()
{
  for ( auto it : _bLattices) {
    delete it;
  }
  _bLattices.clear();
}
template
void SuperLatticeSmoothDiracDelta3D::operator()(T delta[4][4][4],
    const T physPos[3], const int globiC)
{
  if (this->_sLattice.getLoadBalancer().rank(globiC) == singleton::mpi().getRank()) {
    _bLattices[this->_sLattice.getLoadBalancer().loc(globiC)]->operator()(delta,
        physPos);
  }
}
template
BlockLatticeSmoothDiracDelta3D::BlockLatticeSmoothDiracDelta3D(
  BlockLattice3D& blockLattice, UnitConverter& conv, Cuboid3D* cuboid)
  : BlockLatticeF3D(blockLattice, 3), _conv(conv), _cuboid(cuboid)
{
  this->getName() = "BlockLatticeSmoothDiracDelta3D";
}
template
BlockLatticeSmoothDiracDelta3D::BlockLatticeSmoothDiracDelta3D(
  const BlockLatticeSmoothDiracDelta3D& rhs)
  :
  BlockLatticeF3D(rhs._blockLattice, 3), _conv(rhs._conv), _cuboid(rhs._cuboid)
{
}
template
void BlockLatticeSmoothDiracDelta3D::operator()(
  T delta[4][4][4], const T physPos[])
{
  int range = 1;
  T a, b, c = T();
  int latticeRoundedPosP[3] = { 0 };
  T physRoundedPosP[3] = { T() };
  T physLatticeL = _conv.getConversionFactorLength();
  T counter = 0.;
  _cuboid->getLatticeR(latticeRoundedPosP, physPos);
  _cuboid->getPhysR(physRoundedPosP, latticeRoundedPosP);
  for (int i = -range; i <= range + 1; ++i) {
    for (int j = -range; j <= range + 1; ++j) {
      for (int k = -range; k <= range + 1; ++k) {
        delta[i+range][j+range][k+range] = T(1);
        // a, b, c in lattice units cause physical ones get cancelled
        a = (physRoundedPosP[0] + i * physLatticeL - physPos[0])
            / physLatticeL;
        b =  (physRoundedPosP[1] + j * physLatticeL - physPos[1])
             / physLatticeL;
        c = (physRoundedPosP[2] + k * physLatticeL - physPos[2])
            / physLatticeL;
        // the for loops already define that a, b, c are smaller than 2
        delta[i+range][j+range][k+range] *= 1. / 4 * (1 + cos(M_PI * a / 2.));
        delta[i+range][j+range][k+range] *= 1. / 4 * (1 + cos(M_PI * b / 2.));
        delta[i+range][j+range][k+range] *= 1. / 4 * (1 + cos(M_PI * c / 2.));
        counter += delta[i+range][j+range][k+range];
      }
    }
  }
  //  if (!util::nearZero(counter - T(1))){
  //    // sum of delta has to be one
  //    std::cout << "[" << this->getName() << "] " <<
  //        "Delta summed up does not equal 1 but = " <<
  //        counter << std::endl;
  //  }
}
}  // end namespace olb
#endif