/* ----------------------------------------------------------------------
   LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator

   Original Version:
   http://lammps.sandia.gov, Sandia National Laboratories
   Steve Plimpton, sjplimp@sandia.gov

   See the README file in the top-level LAMMPS directory.

   -----------------------------------------------------------------------

   USER-CUDA Package and associated modifications:
   https://sourceforge.net/projects/lammpscuda/

   Christian Trott, christian.trott@tu-ilmenau.de
   Lars Winterfeld, lars.winterfeld@tu-ilmenau.de
   Theoretical Physics II, University of Technology Ilmenau, Germany

   See the README file in the USER-CUDA directory.

   This software is distributed under the GNU General Public License.
------------------------------------------------------------------------- */

#define SBBITS 30

__global__ void Binning_Kernel(int* binned_id, int bin_nmax, int bin_dim_x, int bin_dim_y, int bin_dim_z,
                               CUDA_FLOAT rez_bin_size_x, CUDA_FLOAT rez_bin_size_y, CUDA_FLOAT rez_bin_size_z)
{
  int i = (blockIdx.x * gridDim.y + blockIdx.y) * blockDim.x + threadIdx.x;

  /*int* bin_count=(int*) _buffer;
  bin_count=bin_count+20;
  CUDA_FLOAT* binned_x=(CUDA_FLOAT*)(bin_count+bin_dim_x*bin_dim_y*bin_dim_z);*/
  CUDA_FLOAT* binned_x = (CUDA_FLOAT*) _buffer;
  binned_x = &binned_x[2];
  int* bin_count = (int*) &binned_x[3 * bin_dim_x * bin_dim_y * bin_dim_z * bin_nmax];

  if(i < _nall) {
    // copy atom position from global device memory to local register
    // in this 3 steps to get as much coalesced access as possible
    X_FLOAT* my_x = _x + i;
    CUDA_FLOAT x_i = *my_x;
    my_x += _nmax;
    CUDA_FLOAT y_i = *my_x;
    my_x += _nmax;
    CUDA_FLOAT z_i = *my_x;


    // calculate flat bin index
    int bx = __float2int_rd(rez_bin_size_x * (x_i - _sublo[0])) + 2;
    int by = __float2int_rd(rez_bin_size_y * (y_i - _sublo[1])) + 2;
    int bz = __float2int_rd(rez_bin_size_z * (z_i - _sublo[2])) + 2;

    bx -= bx * negativCUDA(1.0f * bx);
    bx -= (bx - bin_dim_x + 1) * negativCUDA(1.0f * bin_dim_x - 1.0f - 1.0f * bx);
    by -= by * negativCUDA(1.0f * by);
    by -= (by - bin_dim_y + 1) * negativCUDA(1.0f * bin_dim_y - 1.0f - 1.0f * by);
    bz -= bz * negativCUDA(1.0f * bz);
    bz -= (bz - bin_dim_z + 1) * negativCUDA(1.0f * bin_dim_z - 1.0f - 1.0f * bz);


    const unsigned j = bin_dim_z * (bin_dim_y * bx + by) + bz;

    // add new atom to bin, get bin-array position
    const unsigned k = atomicAdd(& bin_count[j], 1);

    if(k < bin_nmax) {
      binned_id [bin_nmax * j + k] = i;
      binned_x [3 * bin_nmax * j + k] = x_i;
      binned_x [3 * bin_nmax * j + k + bin_nmax] = y_i;
      binned_x [3 * bin_nmax * j + k + 2 * bin_nmax] = z_i;
    } else {
      // normally, this should not happen:
      int errorn = atomicAdd((int*) _buffer, 1);
      MYEMUDBG(printf("# CUDA: Binning_Kernel: WARNING: atom %i ignored, no place left in bin %u\n", i, j);)
    }
  }
}


__device__ inline int exclusion(int &i, int &j, int &itype, int &jtype)
{
  int m;

  if(_nex_type)
    if(_ex_type[itype * _cuda_ntypes + jtype]) return 1;

  if(_nex_group) {
    for(m = 0; m < _nex_group; m++) {
      if(_mask[i] & _ex1_bit[m] && _mask[j] & _ex2_bit[m]) return 1;

      if(_mask[i] & _ex2_bit[m] && _mask[j] & _ex1_bit[m]) return 1;
    }
  }

  if(_nex_mol) {
    if(_molecule[i] == _molecule[j])
      for(m = 0; m < _nex_mol; m++)
        if(_mask[i] & _ex_mol_bit[m] && _mask[j] & _ex_mol_bit[m]) return 1;
  }

  return 0;
}

extern __shared__ CUDA_FLOAT shared[];

__device__ inline int find_special(int3 &n, int* list, int &tag, int3 flag)
{
  int k = n.z;

  for(int l = 0; l < n.z; l++) k = ((list[l] == tag) ? l : k);

  return k < n.x ? flag.x : (k < n.y ? flag.y : (k < n.z ? flag.z : 0));
}

template <const unsigned int exclude>
__global__ void NeighborBuildFullBin_Kernel(int* binned_id, int bin_nmax, int bin_dim_x, int bin_dim_y, CUDA_FLOAT globcutoff, int block_style, bool neighall)
{
  int natoms = neighall ? _nall : _nlocal;
  //const bool domol=false;
  int bin_dim_z = gridDim.y;
  CUDA_FLOAT* binned_x = (CUDA_FLOAT*) _buffer;
  binned_x = &binned_x[2];
  int* bin_count = (int*) &binned_x[3 * bin_dim_x * bin_dim_y * bin_dim_z * bin_nmax];
  int bin = __mul24(gridDim.y, blockIdx.x) + blockIdx.y;
  int bin_x = blockIdx.x / bin_dim_y;
  int bin_y = blockIdx.x - bin_x * bin_dim_y;
  int bin_z = blockIdx.y;
  int bin_c = bin_count[bin];


  CUDA_FLOAT cut;

  if(globcutoff > 0)
    cut = globcutoff;

  int i = _nall;
  CUDA_FLOAT* my_x;
  CUDA_FLOAT x_i, y_i, z_i;

  for(int actOffset = 0; actOffset < bin_c; actOffset += blockDim.x) {

    int actIdx = threadIdx.x + actOffset;
    CUDA_FLOAT* other_x = shared;
    int* other_id = (int*) &other_x[3 * blockDim.x];

    if(actIdx < bin_c) {
      i = binned_id[__mul24(bin, bin_nmax) + actIdx];
      my_x = binned_x + __mul24(__mul24(bin, 3), bin_nmax) + actIdx;
      x_i = *my_x;
      my_x += bin_nmax;
      y_i = *my_x;
      my_x += bin_nmax;
      z_i = *my_x;
    } else
      i = 2 * _nall;

    __syncthreads();

    int jnum = 0;
    int itype;

    if(i < natoms) {
      jnum = 0;
      _ilist[i] = i;
      itype = _type[i];
    }

    //__syncthreads();


    for(int otherActOffset = 0; otherActOffset < bin_c; otherActOffset += blockDim.x) {
      int otherActIdx = threadIdx.x + otherActOffset;

      if(otherActIdx < bin_c) {
        if(otherActOffset == actOffset) {
          other_id[threadIdx.x] = i;
          other_x[threadIdx.x] = x_i;
          other_x[threadIdx.x + blockDim.x] = y_i;
          other_x[threadIdx.x + 2 * blockDim.x] = z_i;
        } else {
          other_id[threadIdx.x] = binned_id[__mul24(bin, bin_nmax) + otherActIdx];
          my_x = binned_x + __mul24(__mul24(bin, 3), bin_nmax) + otherActIdx;
          other_x[threadIdx.x] = *my_x;
          my_x += bin_nmax;
          other_x[threadIdx.x + blockDim.x] = *my_x;
          my_x += bin_nmax;
          other_x[threadIdx.x + __mul24(2, blockDim.x)] = *my_x;

        }
      }

      __syncthreads();
      int kk = threadIdx.x;

      for(int k = 0; k < MIN(bin_c - otherActOffset, blockDim.x); ++k) {
        if(i < natoms) {
          kk++;
          kk = kk < MIN(bin_c - otherActOffset, blockDim.x) ? kk : 0;
          int j = other_id[kk];

          if(exclude && exclusion(i, j, itype, _type[j])) continue;

          if(globcutoff < 0) {
            int jtype = _type[j];
            cut = _cutneighsq[itype * _cuda_ntypes + jtype];
          }

          CUDA_FLOAT delx = x_i - other_x[kk];
          CUDA_FLOAT dely = y_i - other_x[kk + blockDim.x];
          CUDA_FLOAT delz = z_i - other_x[kk + 2 * blockDim.x];
          CUDA_FLOAT rsq = delx * delx + dely * dely + delz * delz;


          if(rsq <= cut && i != j) {
            if(jnum < _maxneighbors) {
              if(block_style)
                _neighbors[i * _maxneighbors + jnum] = j;
              else
                _neighbors[i + jnum * natoms] = j;
            }

            ++jnum;
          }
        }
      }

      __syncthreads();

    }

    for(int obin_x = bin_x - 1; obin_x < bin_x + 2; obin_x++)
      for(int obin_y = bin_y - 1; obin_y < bin_y + 2; obin_y++)
        for(int obin_z = bin_z - 1; obin_z < bin_z + 2; obin_z++) {
          if(obin_x < 0 || obin_y < 0 || obin_z < 0) continue;

          if(obin_x >= bin_dim_x || obin_y >= bin_dim_y || obin_z >= bin_dim_z) continue;

          int other_bin = bin_dim_z * (bin_dim_y * obin_x + obin_y) + obin_z;

          if(other_bin == bin) continue;

          int obin_c = bin_count[other_bin];

          for(int otherActOffset = 0; otherActOffset < obin_c; otherActOffset += blockDim.x) {
            int otherActIdx = otherActOffset + threadIdx.x;

            if(threadIdx.x < MIN(blockDim.x, obin_c - otherActOffset)) {
              other_id[threadIdx.x] = binned_id[__mul24(other_bin, bin_nmax) + otherActIdx];
              my_x = binned_x + __mul24(__mul24(other_bin, 3), bin_nmax) + otherActIdx;
              other_x[threadIdx.x] = *my_x;
              my_x += bin_nmax;
              other_x[threadIdx.x + blockDim.x] = *my_x;
              my_x += bin_nmax;
              other_x[threadIdx.x + 2 * blockDim.x] = *my_x;
            }

            __syncthreads();

            for(int k = 0; k < MIN(blockDim.x, obin_c - otherActOffset); ++k) {
              if(i < natoms) {
                int j = other_id[k];

                if(exclude && exclusion(i, j, itype, _type[j])) continue;

                if(globcutoff < 0) {
                  int jtype = _type[j];
                  cut = _cutneighsq[itype * _cuda_ntypes + jtype];
                }

                CUDA_FLOAT delx = x_i - other_x[k];
                CUDA_FLOAT dely = y_i - other_x[k + blockDim.x];
                CUDA_FLOAT delz = z_i - other_x[k + 2 * blockDim.x];
                CUDA_FLOAT rsq = delx * delx + dely * dely + delz * delz;

                if(rsq <= cut && i != j) {
                  if(jnum < _maxneighbors) {
                    if(block_style)
                      _neighbors[i * _maxneighbors + jnum] = j;
                    else
                      _neighbors[i + jnum * natoms] = j;
                  }

                  ++jnum;
                }
              }
            }

            __syncthreads();

          }
        }

    if(jnum > _maxneighbors)((int*)_buffer)[0] = -jnum;

    if(i < natoms)
      _numneigh[i] = jnum;
  }
}


__global__ void FindSpecial(int block_style)
{
  int ii = (blockIdx.x * gridDim.y + blockIdx.y) * blockDim.x + threadIdx.x;
  int which;
  int tag_mask = 0;
  int3 spec_flag;

  int3 mynspecial = {0, 0, 1};

  if(ii >= _nlocal) return;

  int special_id[CUDA_MAX_NSPECIAL];

  int i = _ilist[ii];

  if(i >= _nlocal) return;

  int jnum = _numneigh[i];

  if(_special_flag[1] == 0) spec_flag.x = -1;
  else if(_special_flag[1] == 1) spec_flag.x = 0;
  else spec_flag.x = 1;

  if(_special_flag[2] == 0) spec_flag.y = -1;
  else if(_special_flag[2] == 1) spec_flag.y = 0;
  else spec_flag.y = 2;

  if(_special_flag[3] == 0) spec_flag.z = -1;
  else if(_special_flag[3] == 1) spec_flag.z = 0;
  else spec_flag.z = 3;

  mynspecial.x = _nspecial[i];
  mynspecial.y = _nspecial[i + _nmax];
  mynspecial.z = _nspecial[i + 2 * _nmax];

  if(i < _nlocal) {
    int* list = &_special[i];

    for(int k = 0; k < mynspecial.z; k++) {
      special_id[k] = list[k * _nmax];
      tag_mask = tag_mask | special_id[k];
    }
  }


  for(int k = 0; k < MIN(jnum, _maxneighbors); k++) {
    int j;

    if(block_style)
      j = _neighbors[i * _maxneighbors + k];
    else
      j = _neighbors[i + k * _nlocal];

    int tag_j = _tag[j];
    which = 0;

    if((tag_mask & tag_j) == tag_j) {
      which = find_special(mynspecial, special_id, tag_j, spec_flag);

      if(which > 0) {
        if(block_style)
          _neighbors[i * _maxneighbors + k] = j ^ (which << SBBITS);
        else
          _neighbors[i + k * _nlocal] = j ^ (which << SBBITS);
      } else if(which < 0) {
        if(block_style)
          _neighbors[i * _maxneighbors + k] = _neighbors[i * _maxneighbors + jnum - 1];
        else
          _neighbors[i + k * _nlocal] = _neighbors[i + (jnum - 1) * _nlocal];

        jnum--;
        k--;
      }
    }
  }

  _numneigh[i] = jnum;
}

__global__ void NeighborBuildFullBin_OverlapComm_Kernel(int* binned_id, int bin_nmax, int bin_dim_x, int bin_dim_y, CUDA_FLOAT globcutoff, int block_style)
{
  int bin_dim_z = gridDim.y;
  CUDA_FLOAT* binned_x = (CUDA_FLOAT*) _buffer;
  binned_x = &binned_x[2];
  int* bin_count = (int*) &binned_x[3 * bin_dim_x * bin_dim_y * bin_dim_z * bin_nmax];
  int bin = __mul24(gridDim.y, blockIdx.x) + blockIdx.y;
  int bin_x = blockIdx.x / bin_dim_y;
  int bin_y = blockIdx.x - bin_x * bin_dim_y;
  int bin_z = blockIdx.y;
  int bin_c = bin_count[bin];


  CUDA_FLOAT cut;

  if(globcutoff > 0)
    cut = globcutoff;

  int i = _nall;
  CUDA_FLOAT* my_x;
  CUDA_FLOAT x_i, y_i, z_i;

  for(int actOffset = 0; actOffset < bin_c; actOffset += blockDim.x) {

    int actIdx = threadIdx.x + actOffset;
    CUDA_FLOAT* other_x = shared;
    int* other_id = (int*) &other_x[3 * blockDim.x];

    if(actIdx < bin_c) {
      i = binned_id[__mul24(bin, bin_nmax) + actIdx];
      my_x = binned_x + __mul24(__mul24(bin, 3), bin_nmax) + actIdx;
      x_i = *my_x;
      my_x += bin_nmax;
      y_i = *my_x;
      my_x += bin_nmax;
      z_i = *my_x;
    } else
      i = 2 * _nall;

    __syncthreads();

    int jnum = 0;
    int jnum_border = 0;
    int jnum_inner = 0;
    int i_border = -1;
    int itype;

    if(i < _nlocal) {
      jnum = 0;
      _ilist[i] = i;
      itype = _type[i];
    }

    __syncthreads();


    for(int otherActOffset = 0; otherActOffset < bin_c; otherActOffset += blockDim.x) {
      int otherActIdx = threadIdx.x + otherActOffset;

      if(otherActIdx < bin_c) {
        if(otherActOffset == actOffset) {
          other_id[threadIdx.x] = i;
          other_x[threadIdx.x] = x_i;
          other_x[threadIdx.x + blockDim.x] = y_i;
          other_x[threadIdx.x + 2 * blockDim.x] = z_i;
        } else {
          other_id[threadIdx.x] = binned_id[__mul24(bin, bin_nmax) + otherActIdx];
          my_x = binned_x + __mul24(__mul24(bin, 3), bin_nmax) + otherActIdx;
          other_x[threadIdx.x] = *my_x;
          my_x += bin_nmax;
          other_x[threadIdx.x + blockDim.x] = *my_x;
          my_x += bin_nmax;
          other_x[threadIdx.x + __mul24(2, blockDim.x)] = *my_x;

        }
      }

      __syncthreads();
      int kk = threadIdx.x;

      for(int k = 0; k < MIN(bin_c - otherActOffset, blockDim.x); ++k) {
        if(i < _nlocal) {
          kk++;
          kk = kk < MIN(bin_c - otherActOffset, blockDim.x) ? kk : 0;
          int j = other_id[kk];

          if(globcutoff < 0) {
            int jtype = _type[j];
            cut = _cutneighsq[itype * _cuda_ntypes + jtype];
          }

          CUDA_FLOAT delx = x_i - other_x[kk];
          CUDA_FLOAT dely = y_i - other_x[kk + blockDim.x];
          CUDA_FLOAT delz = z_i - other_x[kk + 2 * blockDim.x];
          CUDA_FLOAT rsq = delx * delx + dely * dely + delz * delz;


          if(rsq <= cut && i != j) {
            if((j >= _nlocal) && (i_border < 0))
              i_border = atomicAdd(_inum_border, 1);

            if(jnum < _maxneighbors) {
              if(block_style) {
                _neighbors[i * _maxneighbors + jnum] = j;

                if(j >= _nlocal) {
                  _neighbors_border[i_border * _maxneighbors + jnum_border] = j;
                } else {
                  _neighbors_inner[i * _maxneighbors + jnum_inner] = j;
                }
              } else {
                _neighbors[i + jnum * _nlocal] = j;

                if(j >= _nlocal) {
                  _neighbors_border[i_border + jnum_border * _nlocal] = j;
                } else {
                  _neighbors_inner[i + jnum_inner * _nlocal] = j;
                }
              }
            }

            ++jnum;

            if(j >= _nlocal)
              jnum_border++;
            else
              jnum_inner++;
          }
        }
      }

      __syncthreads();
    }

    for(int obin_x = bin_x - 1; obin_x < bin_x + 2; obin_x++)
      for(int obin_y = bin_y - 1; obin_y < bin_y + 2; obin_y++)
        for(int obin_z = bin_z - 1; obin_z < bin_z + 2; obin_z++) {
          if(obin_x < 0 || obin_y < 0 || obin_z < 0) continue;

          if(obin_x >= bin_dim_x || obin_y >= bin_dim_y || obin_z >= bin_dim_z) continue;

          int other_bin = bin_dim_z * (bin_dim_y * obin_x + obin_y) + obin_z;

          if(other_bin == bin) continue;

          int obin_c = bin_count[other_bin];

          for(int otherActOffset = 0; otherActOffset < obin_c; otherActOffset += blockDim.x) {
            int otherActIdx = otherActOffset + threadIdx.x;

            if(threadIdx.x < MIN(blockDim.x, obin_c - otherActOffset)) {
              other_id[threadIdx.x] = binned_id[__mul24(other_bin, bin_nmax) + otherActIdx];
              my_x = binned_x + __mul24(__mul24(other_bin, 3), bin_nmax) + otherActIdx;
              other_x[threadIdx.x] = *my_x;
              my_x += bin_nmax;
              other_x[threadIdx.x + blockDim.x] = *my_x;
              my_x += bin_nmax;
              other_x[threadIdx.x + 2 * blockDim.x] = *my_x;
            }

            __syncthreads();

            for(int k = 0; k < MIN(blockDim.x, obin_c - otherActOffset); ++k) {
              if(i < _nlocal) {
                int j = other_id[k];

                if(globcutoff < 0) {
                  int jtype = _type[j];
                  cut = _cutneighsq[itype * _cuda_ntypes + jtype];
                }

                CUDA_FLOAT delx = x_i - other_x[k];
                CUDA_FLOAT dely = y_i - other_x[k + blockDim.x];
                CUDA_FLOAT delz = z_i - other_x[k + 2 * blockDim.x];
                CUDA_FLOAT rsq = delx * delx + dely * dely + delz * delz;

                if(rsq <= cut && i != j) {
                  if((j >= _nlocal) && (i_border < 0))
                    i_border = atomicAdd(_inum_border, 1);

                  if(jnum < _maxneighbors) {
                    if(block_style) {
                      _neighbors[i * _maxneighbors + jnum] = j;

                      if(j >= _nlocal) {
                        _neighbors_border[i_border * _maxneighbors + jnum_border] = j;
                      } else {
                        _neighbors_inner[i * _maxneighbors + jnum_inner] = j;
                      }
                    } else {
                      _neighbors[i + jnum * _nlocal] = j;

                      if(j >= _nlocal) {
                        _neighbors_border[i_border + jnum_border * _nlocal] = j;
                      } else {
                        _neighbors_inner[i + jnum_inner * _nlocal] = j;
                      }
                    }
                  }

                  ++jnum;

                  if(j >= _nlocal)
                    jnum_border++;
                  else
                    jnum_inner++;
                }
              }
            }

            __syncthreads();
          }
        }

    if(jnum > _maxneighbors)((int*)_buffer)[0] = -jnum;

    if(i < _nlocal) {
      _numneigh[i] = jnum;
      _numneigh_inner[i] = jnum_inner;

      if(i_border >= 0) _numneigh_border[i_border] = jnum_border;

      if(i_border >= 0) _ilist_border[i_border] = i;

    }
  }
}

__global__ void NeighborBuildFullNsq_Kernel()
{
  int i = (blockIdx.x * gridDim.y + blockIdx.y) * blockDim.x + threadIdx.x;
  int* buffer = (int*) _buffer;

  if(i < _nlocal) {
    X_FLOAT* my_x = _x + i;
    CUDA_FLOAT x_i = *my_x;
    my_x += _nmax;
    CUDA_FLOAT y_i = *my_x;
    my_x += _nmax;
    CUDA_FLOAT z_i = *my_x;
    int jnum = 0;
    int* jlist = _firstneigh[i];
    _ilist[i] = i;

    int itype = _type[i];
    __syncthreads();

    for(int j = 0; j < _nall; ++j) {
      my_x = _x + j;
      CUDA_FLOAT x_j = *my_x;
      my_x += _nmax;
      CUDA_FLOAT y_j = *my_x;
      my_x += _nmax;
      CUDA_FLOAT z_j = *my_x;
      CUDA_FLOAT delx = x_i - x_j;
      CUDA_FLOAT dely = y_i - y_j;
      CUDA_FLOAT delz = z_i - z_j;
      CUDA_FLOAT rsq = delx * delx + dely * dely + delz * delz;
      int jtype = _type[j];

      if(rsq <= _cutneighsq[itype * _cuda_ntypes + jtype] && i != j) {
        if(jnum < _maxneighbors)
          jlist[jnum] = j;

        if(i == 151)((int*)_buffer)[jnum + 2] = j;

        ++jnum;
      }

      __syncthreads();
    }

    if(jnum > _maxneighbors) buffer[0] = 0;

    _numneigh[i] = jnum;

    if(i == 151)((int*)_buffer)[1] = jnum;
  }
}

