#include <math.h>
#include <mpi.h>
#include <stdlib.h>

#include "simulation.h"
#include "random.h"
#include "mpicomm.h"

// --------------------------------------------- Helper function declarations

MPI_Comm get_mpi_communicator(const conf_t *configuration, MPI_Comm original);
int communicator_reordered(MPI_Comm new_comm);
void sim_info_local(const sim_t *s, sim_info_t *local_info);
void allocate_memory(sim_t *s);
void deallocate_memory(sim_t *s);
void init_local_particles(sim_t *s);
void classify_particles(sim_t *s);
void relocate_particles(sim_t *s);

// ==========================================================================

void sim_init(sim_t *s, const conf_t* configuration, const MPI_Comm communicator)
{
   s->configuration = configuration;
   s->communicator = get_mpi_communicator(configuration, communicator);
   s->rma_window = MPI_WIN_NULL;

   // allocate storage and initialize local particles with random values
   allocate_memory(s);
   init_local_particles(s);
}

void sim_free(sim_t *s)
{
   deallocate_memory(s);
   MPI_Comm_free(&s->communicator);
   if(s->rma_window != MPI_WIN_NULL) {
      MPI_Win_free(&s->rma_window);
   }
}

void sim_info(const sim_t *s, sim_info_t *info)
{
   sim_info_t local_info;

   sim_info_local(s, &local_info);
   mpi_reduce_sim_info(&local_info, info);
}

void sim_info_local(const sim_t *s, sim_info_t *local_info)
{
   const part_t *particles = &s->local_particles;
   int count = particles->count;

   double *vx = particles->velocity.x;
   double *vy = particles->velocity.y;
   double *vsqr = malloc(count * sizeof(*vsqr));
   double vsqr_max = 0.;
   int i;

   for(i = 0; i < count; i++) {
      vsqr[i] = vx[i] * vx[i] + vy[i] * vy[i];
   }
   for(i = 0; i < count; i++) {
      if(vsqr_max < vsqr[i]) vsqr_max = vsqr[i];
   }
   free(vsqr);

   local_info->n_particles_total = s->local_particles.count;
   local_info->min_particles_per_cell = count;
   local_info->max_particles_per_cell = count;
   local_info->max_velocity = sqrt(vsqr_max);
}

void sim_calc_forces(sim_t *s)
{
   part_t *particles = &(s->local_particles);
   int count = particles->count;

   // just use random values for demonstration purpose
   double max_force = s->configuration->max_force;
   vec_t* force = &(particles->force);
   vec_set_random(force, max_force, count);
}

void sim_move_particles(sim_t *s)
{
   part_t *particles = &s->local_particles;
   int count = particles->count;

   const vec_t *force = &particles->force;
   const double *mass = particles->mass;
   double delta_t = s->configuration->delta_t;

   vec_t tmp_vector;
   vec_init(&tmp_vector, count);

   vec_t *acceleration = &tmp_vector;
   vec_t *velocity = &particles->velocity;
   vec_t *location = &particles->location;

   // acceleration = force / mass
   vec_arr_divide(acceleration, force, mass, count);

   // velocity += acceleration * delta_t
   vec_add_scaled(velocity, acceleration, delta_t, count);

   // location += velocity * delta_t, taking care of periodicity
   double modulus_x = s->configuration->ncells[0];
   double modulus_y = s->configuration->ncells[1];
   vec_add_scaled_mod(location, velocity, delta_t, modulus_x, modulus_y, count);

   vec_free(&tmp_vector);
}

void sim_communicate_particles(sim_t *s)
{
   int mode = s->configuration->transmission_mode;

   classify_particles(s);
   relocate_particles(s);

   switch(mode) {
      case DYNAMIC:
          mpi_communicate_particles_dynamic(s); break;
      case COLLECTIVE:
          mpi_communicate_particles_collective(s); break;
      case RMA:
          mpi_communicate_particles_rma(s); break;
   }
}

// ---------------------------------------------------------- Helper functions

void allocate_memory(sim_t *s)
{
   // set up storage for particles in local cell and temporary working vector
   int count = s->configuration->particles_per_cell_initial;
   int capacity = count + count / 4;

   part_init(&s->local_particles, capacity);

   // set up buffer for particles leaving the local cell
   // (capacity will be increased on demand)
   part_init(&s->leaving_particles, 0);
}

void deallocate_memory(sim_t *s)
{
   part_free(&s->local_particles);

   part_free(&s->leaving_particles);
}

MPI_Comm get_mpi_communicator(const conf_t *configuration, MPI_Comm original)
{
   MPI_Comm new_comm;

   if(configuration->use_cartesian_topology) {
      int ndims = 2;
      const int *dims = configuration->ncells;
      int isperiodic[] = {1, 1};
      int allow_reorder = 1;
      int reordered;

      MPI_Cart_create(original, ndims, dims, isperiodic, allow_reorder, &new_comm);

      reordered = communicator_reordered(new_comm);
      if(info_enabled(configuration)) {
         printf("INFO: MPI reordered ranks: %s\n",
                reordered ? "YES" : "NO");
      }
   } else {
      MPI_Comm_dup(original, &new_comm);
   }
   return new_comm;
}

int communicator_reordered(MPI_Comm new_comm)
{
   int comm_world_rank, new_rank;
   int local_ranks_different;
   int ranks_reordered = 0;

   MPI_Comm_rank(MPI_COMM_WORLD, &comm_world_rank);
   MPI_Comm_rank(new_comm, &new_rank);

   local_ranks_different = comm_world_rank != new_rank;
   MPI_Allreduce(
         &local_ranks_different, &ranks_reordered, 1, MPI_INT,
         MPI_LOR, MPI_COMM_WORLD
   );
   return ranks_reordered;
}

void init_local_particles(sim_t *s)
{
   int count = s->configuration->particles_per_cell_initial;
   part_t *particles = &s->local_particles;
   part_resize(particles, count);

   // particle mass
   double min_mass = s->configuration->min_mass;
   double max_mass = s->configuration->max_mass;
   array_set_random(particles->mass, min_mass, max_mass, count);

   // particle location
   int mpi_rank, cell_x, cell_y;
   MPI_Comm_rank(s->communicator, &mpi_rank);

   if(s->configuration->use_cartesian_topology) {
      int coords[2];
      MPI_Cart_coords(s->communicator, mpi_rank, 2, coords);
      cell_x = coords[0];
      cell_y = coords[1];
   } else {
      int nx = s->configuration->ncells[0];
      cell_x = mpi_rank % nx;
      cell_y = mpi_rank / nx;
   }

   double min_x = cell_x, max_x = min_x + 1.;
   double min_y = cell_y, max_y = min_y + 1.;
   vec_set_random_box(&particles->location, min_x, max_x, min_y, max_y, count);

   // particle velocity
   double vmax = s->configuration->max_velocity_initial;
   vec_set_random(&particles->velocity, vmax, count);

   // forces
   vec_set_zero(&particles->force, count);
}

void classify_particles(sim_t *s)
{
   part_t *particles = &s->local_particles;
   double *x = particles->location.x;
   double *y = particles->location.y;
   int *owner = particles->owner_rank;
   int count = particles->count;
   int i;

   if(s->configuration->use_cartesian_topology) {
      int coords[2];
      for(i = 0; i < count; i++) {
         coords[0] = (int)x[i];
         coords[1] = (int)y[i];
         MPI_Cart_rank(s->communicator, coords, &owner[i]);
      }
   } else {
      int nx = s->configuration->ncells[0];

      for(i = 0; i < count; i++) {
         owner[i] = (int)x[i] + nx * (int)y[i];
      }
   }
}

void relocate_particles(sim_t *s)
{
   int count = s->local_particles.count;
   int my_rank;
   MPI_Comm_rank(s->communicator, &my_rank);

   part_t *particles = &s->local_particles;
   int *owner = particles->owner_rank;

   int i, j, k;
   int block_start, block_len;

   i = 0; j = 0; k = 0;
   while(k < count)
   {
      // scan for particles leaving local cell
      block_start = k;
      for(; k < count && owner[k] != my_rank; k++);
      block_len = k - block_start;

      if(block_len > 0) {
         part_copy(&s->leaving_particles, j, &s->local_particles, block_start, block_len);
         j += block_len;
      }

      // scan for particles staying in local cell
      block_start = k;
      for(; k < count && owner[k] == my_rank; k++);
      block_len = k - block_start;
      if(block_len > 0) {
         if(i != block_start) {
            part_copy(&s->local_particles, i, &s->local_particles, block_start, block_len);
         }
         i += block_len;
      }
   }
   s->local_particles.count = i;
   s->leaving_particles.count = j;
}

