/**
 * DATA STRUCTURES
 */

typedef struct MM {
    float   x;
    float   y;
    int     carbone;
    int     dormance;
} MM;

typedef struct OM {
    float   x;
    float   y;
    int     carbone;
    int     lock;
} OM;

typedef struct MiorWorld {
    int     nbMM;
    int     nbOM;
    int     RA;
    float   RR;
    float   GR;
    float   K;
    int     width;
    int     minSize;
    int     CO2;
    int     lock;
} MiorWorld;


typedef struct RandomGen {
	ulong a;
	ulong b;
	ulong c;
} RandomGen;


/**
 * CONSTANTS
 */

#define OFFER_INVALID -1
#define OFFER_UNINIT  -2

/**
 * HELPER FUNCTIONS
 */

// Compatibility macros for OpenCL <= 1.1

#if __OPENCL_VERSION__ <= CL_VERSION_1_0
    #define atomic_cmpxchg(p, o, v) atom_cmpxchg(p, o, v)
#endif

// End of compatibility macros


#define LOCK_P(lock, id) while (atomic_cmpxchg(&(lock), -1, id) != id) {}
#define LOCK_V(lock, id) while (atomic_cmpxchg(&(lock), id, -1) != -1) {}

// RNG

void rng_init(RandomGen *r, long seed)
{
    r->a = seed;
    r->b = 0;
    r->c = 362436;
}

unsigned long rng_rand(RandomGen *r)
{
    const long old = r->b;
    r->b = r->a * 1103515245 + 12345;
    r->a = (~old ^ (r->b >> 3)) - r->c++;
    return r->b;
}

float rng_rand_01(RandomGen *r)
{
    return (rng_rand(r) & 4294967295) / 4294967295.0f;
}

void shuffle_indexes(global int * array, int size, RandomGen *r)
{   
    for (int i = size - 1; i >= 1; i--) {
		const int j = rng_rand_01(r) * (i + 1);
		
		const int t = array[j];
		array[j] = array[i];
		array[i] = t;
	}
}

void shuffle_help(global int * array, int size, RandomGen *r)
{   
    for (int i = size - 1; i >= 1; i--) {
		const int j = rng_rand_01(r) * (i + 1);
		
		const int t = array[j];
		array[j] = array[i];
		array[i] = t;
	}
}


kernel void shuffle_test(global int * array, int size)
{
	RandomGen rng;
	rng_init(&rng, 42L);
	
	for (int i = 0; i < size; i++) {
		array[i] = i;
	}
	
	shuffle_help(array, size, &rng);
}
	
// Cleanup

void topo_clean(global int * targets, int size)
{
    for (int i = 0; i < size; i++) {
        if (targets[i] != OFFER_INVALID) {
            targets[i] = OFFER_UNINIT;
        }
    }
}

/**
 * KERNELS
 */

kernel void topology(
    global MM *mmList,
    global OM *omList,
    global int *topo,
    global MiorWorld *world)
{
    const int iMM = get_global_id(0);
    const int iOM = get_global_id(1);
    
	//float dx = omList[iOM].x - mmList[iMM].x;
	//float dy = omList[iOM].y - mmList[iMM].y;
	//float distance = sqrt(dx * dx + dy * dy);
	
    //if (distance <= world->RA) {
	//    topo[iMM * world->nbOM + iOM] = OFFER_UNINIT;
	//} else {
	//    topo[iMM * world->nbOM + iOM] = OFFER_INVALID;
	//}
	
	const float dx = omList[iOM].x - mmList[iMM].x;
	const float dy = omList[iOM].y - mmList[iMM].y;
	const float distance = hypot(dx, dy);
	
	if (distance <= world->RA) {
	    topo[iMM * world->nbOM + iOM] = OFFER_UNINIT;
	} else {
	    topo[iMM * world->nbOM + iOM] = OFFER_INVALID;
	}
}

int live_breath(
    int iMM,
    global MM * mmList,
    global OM * omList,
    global int * targets,
    global MiorWorld * world,
    global int *omIndexes)
{
    global MM * currentMM = mmList + iMM;
    global OM * currentOM;
    
    const int totalNeed = currentMM->carbone * world->RR;
    int remainingNeed = totalNeed;
    const int nbOM = world->nbOM;
    int i = 0, iOM = 0;
    int offer, consum;
    
    while (i < nbOM && remainingNeed > 0) {
        iOM = omIndexes[i];
        
   		if (targets[iOM] != OFFER_INVALID) {
    		currentOM = omList + iOM;
            
            offer = world->K * currentOM->carbone;
            consum = min(offer, remainingNeed);
            remainingNeed -= consum;
    	}
    	
    	i++;
    }
    
    if (remainingNeed > 0) {
    	currentMM->dormance = 1;
    } else {
    	currentMM->dormance = 0;
    	remainingNeed = totalNeed;
  
		i = 0;
		while (i < nbOM && remainingNeed > 0) {
		    iOM = omIndexes[i];
		    
		    if (targets[iOM] != OFFER_INVALID) {
		        currentOM = omList + iOM;
		         
		        LOCK_P(currentOM->lock, iMM);
		        
		        if (targets[iOM] != OFFER_UNINIT) {
		            offer = targets[iOM];
		        } else {
		            offer = world->K * currentOM->carbone;
		        }
		        
		        consum = min(offer, remainingNeed);
		        
		        remainingNeed -= consum;
		        currentOM->carbone -= consum;
		        
		        LOCK_V(currentOM->lock, iMM);
		        
		        LOCK_P(world->lock, iMM);
		        world->CO2 += consum;
		        LOCK_V(world->lock, iMM);
		        
		        targets[iOM] = offer - consum;
		    }
		    
		    i++;
		}
	}
    
    return remainingNeed;
}

int live_grow(
    int iMM,
    global MM * mmList,
    global OM * omList,
    global int * targets,
    global MiorWorld * world,
    global int *omIndexes)
{
    global MM * currentMM = mmList + iMM;
    global OM * currentOM;
    
    const int totalNeed = currentMM->carbone * world->GR;
    int remainingNeed = totalNeed;
    const int nbOM = world->nbOM;
    int i = 0, iOM = 0;
    int offer, consum;
    
    while (i < nbOM && remainingNeed > 0) {
        iOM = omIndexes[i];
        
        if (targets[iOM] != OFFER_INVALID) {
            currentOM = omList + iOM;
            
            LOCK_P(currentOM->lock, iMM);
            
            if (targets[iOM] != OFFER_UNINIT) {
                offer = targets[iOM];
            } else {
                offer = world->K * currentOM->carbone;
            }
            
            consum = min(offer, remainingNeed);
            
            remainingNeed -= consum;
            currentOM->carbone -= consum;
            
            LOCK_V(currentOM->lock, iMM);
            
            currentMM->carbone += consum;
        }
        
        i++;
    }
    
    return remainingNeed;
}

kernel void live(
    global MM *mmList,
    global OM *omList,
    global int *topo,
    global MiorWorld *world,
    global int *omIndexes,
    long   seed)
{
    int iMM = get_global_id(0);
    global int * targets = topo + iMM * world->nbOM;
    global int * indexes = omIndexes + iMM * world->nbOM;
    RandomGen rng;
    
    for (int i = 0; i < world->nbOM; i++) {
        indexes[i] = i;
    }
    
    if (seed != 0) {
        rng_init(&rng, seed + iMM);
        shuffle_indexes(indexes, world->nbOM, &rng);
    }
    
    // BREATHING
    live_breath(iMM, mmList, omList, targets, world, indexes);
    
    if (mmList[iMM].dormance == 0) {
    
    	// GROWTH
    	live_grow(iMM, mmList, omList, targets, world, indexes);
    }
    
    // CLEANUP
    topo_clean(targets, world->nbOM);
}

kernel void autolive(
    global MM *mmList,
    global OM *omList,
    global int *topo,
    global MiorWorld *world,
    global int *omIndexes,
    long   seed)
{
    LOCK_P(world->lock, get_global_id(0));
    int CO2 = world->CO2;
    LOCK_V(world->lock, get_global_id(0));
    
    int oldCO2 = -1;
    
    while (CO2 != oldCO2) {
        live(mmList, omList, topo, world, omIndexes, seed);
        
        oldCO2 = CO2;
        LOCK_P(world->lock, get_global_id(0));
        CO2 = world->CO2;
        LOCK_V(world->lock, get_global_id(0));
    }   
}
