#include <v_lib.h>
#include "rh_side.h"
#include "phasesp.h"
#include "psys.h"

#define BII_MIN_DIST 1e-10 /* minimal distance for binary_interaction */
#define BNI_MIN_DIST 1e-10 /* minimal distance for boundary_interaction */

/*
#define USE_EARLY_OUT
#define DEBUG_EARLY_OUT
*/

static unsigned int compute_forces_call_count = 0;

void
compute_binary_dist(particle_system_p ps)
{
	u32_t i, j;
	vec_p posi, posj;
	vec_t dr, surf_dist;
	vec3_t rij;
	for(i = 0;i < ps->num_particles - 1;i++)
	{
		posi = PS_POS(ps->cur_phase,i);
		for(j = i + 1;j < ps->num_particles;j++)
		{
			posj = PS_POS(ps->cur_phase,j);
			vec3Sub(rij, posj, posi);
			dr = vec3Abs(rij);
			surf_dist = dr - ps->particle_radius[j] - ps->particle_radius[i];
			if(NULL != ps->binary_interaction && surf_dist <= 0)
			{
				puts("compute_binary_dist(): Current parameters maked RK45 to diverge, or incorrect IC was specified.");
				printf("compute_forces_call_count = %u\n", compute_forces_call_count);
				printf("Distance between centers %g, sum of sizes %g\n",dr,ps->particle_radius[j] + ps->particle_radius[i]);
				printf("Particles intersect each other. Phase trashed, aborting%s.\n",
					ps->final_filename[0] ? " without save" : "");
				fflush(stdout);
				exit(-1);
			}
			ps->dist->cell[i][j] = ps->dist->cell[j][i] = dr;
			ps->surf_dist->cell[i][j] = ps->dist->cell[j][i] = surf_dist;
		}
	}
}

V_LOCAL void
apply_binary_forces(particle_system_p ps)
{
	vec_p pos1, pos2;
	vec3_t r12, f12;
	vec_t f;
	vec_p phase = ps->cur_phase;
	vec_t realdist;
	float_controller_t binary_interaction = ps->binary_interaction;
	vec_p f1, f2;
	u32_t i, j;
	u32_t count = 0, count_max;
	if(!binary_interaction)
		return;
	count_max = ps->num_particles * (ps->num_particles-1) / 2;
	for(i=0;i<ps->num_particles-1;i++)
	{
		pos1 = PS_POS(phase,i);
		f1 = ps->force[i];
		for(j=i+1;j<ps->num_particles;j++)
		{
			realdist = ps->surf_dist->cell[i][j];
			if(realdist >= ps->binary_interaction_max_dist)
				continue;
			if(realdist < BII_MIN_DIST)
			{
				ps->fail_counter++;
				continue;
			}
			count++;
			pos2 = PS_POS(phase,j);
			vec3Sub(r12,pos2,pos1);
			f2 = ps->force[j];
			/* call controller */
			f = (*binary_interaction)(ps, realdist, ps->binary_interaction_arg);
			/*
			 * 'f' now contains modulus of the force acting to particle
			 * #1 from particle #2; exploit 3rd Newton's law and update
			 * both forces.
			 */
			vec3Scale(f12,r12,f / ps->dist->cell[i][j]); /* normalize r12 and than scale by f */
			vec3Add(f1,f1,f12);
			vec3Sub(f2,f2,f12);
		}
	}
	ps->bskip_ratio = 100 * (1 - (vec_t)count / (vec_t)count_max);
}

V_LOCAL void
compute_contacts(particle_system_p ps)
{
	vec_t dr, d1, sizei, sizej, ri, rj;
	u32_t i, j;
	for(i = 0;i < ps->num_particles;i++)
		ps->contact->cell[i][i] = 0;
	for(i = 0;i < ps->num_particles - 1;i++)
	{
		sizei = ps->particle_radius[i];
		for(j = i+1;j < ps->num_particles;j++)
		{
			sizej = ps->particle_radius[j];
			if(ps->surf_dist->cell[i][j] >= ps->dglue[i] + ps->dglue[j])
			{
				/* there is no contact */
				ps->contact->cell[i][j] = ps->contact->cell[j][i] = 0;
				continue;
			}
			dr = ps->dist->cell[i][j];
			/* compute surface of contact */
			ri = sizei + ps->dglue[i];
			rj = sizej + ps->dglue[j];
			d1 = (ri * ri - rj * rj + dr * dr);
			d1 *= 0.25 * d1 / (dr * dr);
			if(ri * ri < d1)
			{
				puts("compute_contacts(): current parameters maked RK45 to diverge, or incorrect IC was specified.");
				printf("compute_forces_call_count = %u\n", compute_forces_call_count);
				printf("Distance between centers %g, sum of sizes %g, ps->dglue %g & %g.\n",
					dr, sizei + sizej, ps->dglue[i], ps->dglue[j]);
				printf("Particles intersect each other. Phase trashed, aborting%s.\n",
					ps->final_filename[0] ? " without save" : "");
				fflush(stdout);
				exit(-1);
			}
			d1 = ri * ri - d1;
			/* V_ASSERT(d1 >= 0); */
			ps->contact->cell[i][j] = M_PI * d1;
			ps->contact->cell[j][i] = ps->contact->cell[i][j];
		}
	}
}

V_LOCAL void
apply_tangent_forces(particle_system_p ps)
{
	vec_p posi, posj;
	vec3_t rij, rij0, vin, vit, fij;
	vec_p fi, vi;
	vec_t f;
	vec_p phase = ps->cur_phase;
	u32_t i, j;
	for(i = 0;i < ps->num_particles;i++)
	{
		posi = PS_POS(phase,i);
		vi = PS_VEL(phase,i);
		fi = ps->force[i];
		for(j = 0;j < ps->num_particles;j++)
		{
			if(i == j)
				continue;
			if(ps->contact->cell[i][j] <= 1e-30)
				continue;
			posj = PS_POS(phase,j);
			vec3Sub(rij, posj, posi);
			/* compute tangent velocity */
			vec3Scale(rij0, rij, 1 / ps->dist->cell[i][j]);
			vec3Scale(vin, rij0, vec3Dot(vi, rij0));
			vec3Sub(vit, vi, vin);
			/* tangent force */
			f = - ps->contact->cell[i][j] * ps->hglue / ps->surf_dist->cell[i][j];
			vec3Scale(fij, vit, f);
			vec3Add(fi,fi,fij);
		}
	}
}

/*
 * FIXME: change to use CSG boundary && fixed skeleton
 */
V_LOCAL void
apply_boundary_forces(particle_system_p ps)
{
	u32_t i, j;
	vec_p pos;
	vec3_t df;
	vec3_t rsp;
	vec_t drsp;
	vec_t f;
	vec_p phase = ps->cur_phase;
	vec_t realdist;
	vec_p c_arg, plane, sphere, pf;
	float_controller_t boundary_force;
	for(i=0;i<ps->num_boundary_planes;i++)
	{
		plane = ps->boundary_plane[i];
		boundary_force = ps->plane_interaction[i];
		c_arg = ps->plane_interaction_arg[i];
		for(j=0;j<ps->num_particles;j++)
		{
			pos = PS_POS(phase,j);
			pf = ps->force[j];
			realdist = vec3Dot(plane,pos) - plane[3] - ps->particle_radius[j];
			if(realdist > BNI_MIN_DIST)
			{
				/* call controller */
				f = (*boundary_force)(ps,realdist,c_arg);
				vec3Scale(df,plane,f);
				vec3Add(pf,pf,df);
			}
			else
			{
				/* too close / other side */
				ps->fail_counter++;
				continue;
			}
		}
	}
	for(i=0;i<ps->num_boundary_spheres;i++)
	{
		sphere = ps->boundary_sphere[i];
		boundary_force = ps->sphere_interaction[i];
		c_arg = ps->sphere_interaction_arg[i];
		for(j=0;j<ps->num_particles;j++)
		{
			pos = PS_POS(phase,j);
			pf = ps->force[j];
			vec3Sub(rsp,pos,sphere);
			drsp = vec3Abs(rsp);
			if(drsp < BII_MIN_DIST)
			{
				/* particle too close to center: no work necessary */
				continue;
			}
			realdist = v_abs(drsp - sphere[3]) - ps->particle_radius[j];
			if(realdist < BII_MIN_DIST)
			{
				/* particle too close to boundary: no work can be done */
				ps->fail_counter++;
				continue;
			}
			/* call controller */
			f = (*boundary_force)(ps,realdist,c_arg);
			/*
			 * 'f' now contains modulus of the force acting from sphere to particle 
			 */
			vec3Scale(df,rsp,- f / drsp);
			vec3Add(pf,pf,df);
		}
	}
}

V_LOCAL void
apply_unary_forces(particle_system_p ps)
{
	vec3_t f;
	vec3_controller_t unary_force = ps->unary_force;
	vec_p pf, unary_force_arg = ps->unary_force_arg;
	if(!ps->unary_force)
		return;
	for(ps->cur_idx=0;ps->cur_idx < ps->num_particles;ps->cur_idx++)
	{
		pf = ps->force[ps->cur_idx];
		vec3Zero(f);
		(*unary_force)(ps,unary_force_arg,f);
		vec3Add(pf,pf,f);
	}
}

V_LOCAL void
apply_unary_accels(particle_system_p ps)
{
	vec3_t a;
	vec3_controller_t unary_accel = ps->unary_accel;
	vec_p pf, unary_accel_arg = ps->unary_accel_arg;
	if(!ps->unary_accel)
		return;
	for(ps->cur_idx=0;ps->cur_idx < ps->num_particles;ps->cur_idx++)
	{
		pf = ps->force[ps->cur_idx];
		vec3Zero(a);
		(*unary_accel)(ps,unary_accel_arg,a);
		vec3Scale(a,a,ps->particle_mass_delta[ps->cur_idx]);
		vec3Add(pf,pf,a);
	}
}

V_LOCAL void
apply_viscous_forces(particle_system_p ps)
{
	vec3_t v;
	vec3_controller_t env_vel = ps->env_velocity;
	vec_p pf, pv, env_vel_arg = ps->env_velocity_arg;
	pv = PS_VEL(ps->cur_phase,0);
	if(!env_vel)
		return;
	for(ps->cur_idx=0;ps->cur_idx < ps->num_particles;ps->cur_idx++)
	{
		pf = ps->force[ps->cur_idx];
		vec3Zero(v);
		(*env_vel)(ps,env_vel_arg,v);
		vec3Sub(v,v,pv);
		vec3Scale(v,v,ps->particle_6_pi_eta_r[ps->cur_idx]);
		vec3Add(pf,pf,v);
		pv += PHASESPACE_DIM;
	}
}

V_LOCAL vboolean
compute_forces(particle_system_p ps)
{
#ifdef  USE_EARLY_OUT
	vboolean need_reject_step;
#endif	/* USE_EARLY_OUT */
	ps->fail_counter = 0;
	compute_forces_call_count++;
#ifdef  USE_EARLY_OUT
	need_reject_step = false;
	for(;;)
	{
		memZero(ps->force,ps->num_particles * sizeof(vec3_t));
		compute_binary_dist(ps);
		apply_binary_forces(ps);
		if(ps->fail_counter)
		{
			conlPrintf(LOG_TRACE,
				"compute_forces(): %u fails after binary_interaction()\n",
				ps->fail_counter);
			need_reject_step = true;
			break;
		}
		if(ps->tangent_forces)
		{
			compute_contacts(ps);
			apply_tangent_forces(ps);
		}
		apply_boundary_forces(ps);
		if(ps->fail_counter)
		{
			conlPrintf(LOG_TRACE,
				"compute_forces(): %u fails after boundary_interaction()\n",
				ps->fail_counter);
			need_reject_step = true;
			break;
		}
		apply_unary_forces(ps);
		if(ps->fail_counter)
		{
			conlPrintf(LOG_TRACE,
				"compute_forces(): %u fails after unary_interaction()\n",
				ps->fail_counter);
			need_reject_step = true;
			break;
		}
		apply_unary_accels(ps);
		if(ps->fail_counter)
		{
			conlPrintf(LOG_TRACE,
				"compute_forces(): %u fails after unary_accels()\n",
				ps->fail_counter);
			need_reject_step = true;
			break;
		}
		apply_viscous_forces(ps);
		if(ps->fail_counter)
		{
			conlPrintf(LOG_TRACE,
				"compute_forces(): %u fails after environment_interaction()\n",
				ps->fail_counter);
			need_reject_step = true;
			break;
		}
		break;
	}
	if(need_reject_step)
	{
		if(ps->step_rejection_count == MAX_STEP_REJECTION)
			return false;
#ifdef	DEBUG_EARLY_OUT
		printf("fail_counter: %u; odeRejectStep()\n",ps->fail_counter);
#endif
		odeRejectStep(ps->ode_solver);
		ps->step_rejection_count++;
	}
	else
		ps->step_rejection_count = 0;
	return true;
#else   /* !USE_EARLY_OUT */
	memZero(ps->force,ps->num_particles * sizeof(vec3_t));
	compute_binary_dist(ps);
	apply_binary_forces(ps);
	if(ps->tangent_forces)
	{
		compute_contacts(ps);
		apply_tangent_forces(ps);
	}
	apply_boundary_forces(ps);
	apply_unary_forces(ps);
	apply_unary_accels(ps);
	apply_viscous_forces(ps);
	if(ps->fail_counter)
	{
		conlPrintf(LOG_TRACE,
			"compute_forces(): %u fails.\n",ps->fail_counter);
	}
	return true;
#endif  /* !USE_EARLY_OUT */
}

/*
 * Routine to call from the ODE solver
 */
void V_CALL
problem_right_side(void_p arg,vec_t deriv[],vec_t t,vec_t x[])
{
	u32_t idx;
	particle_system_p ps;
	/* vec_p pos = PS_POS(x,0); */
	vec_p vel = PS_VEL(x,0);
	vec_p pos_deriv = PS_POS(deriv,0);
	vec_p vel_deriv = PS_VEL(deriv,0);
	ps = (particle_system_p)arg;
	ps->cur_time = t;
	ps->cur_phase = x;
	if(!compute_forces(ps))
		odeTerminateIntegration(ps->ode_solver);
	for(idx=0;idx < ps->num_particles;idx++)
	{
		vec3Scale(vel_deriv,ps->force[idx],ps->particle_reciprocal_mass[idx]);
		vec3Copy(pos_deriv,vel);
		/* pos += PHASESPACE_DIM; */
		vel += PHASESPACE_DIM;
		pos_deriv += PHASESPACE_DIM;
		vel_deriv += PHASESPACE_DIM;
	}
}
