// Control code for the raytracer.
// Written by Nils Liaaen Corneliusen.
// License: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication license
// https://www.ignorantus.com

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#include "raytracer.h"

static const v4 coltable[] = {
	{ 1.0f, 1.0f, 0.0f, 0.0f },
	{ 0.0f, 0.0f, 1.0f, 0.0f },
	{ 0.0f, 1.0f, 0.0f, 0.0f },
	{ 0.0f, 1.0f, 1.0f, 0.0f },
	{ 1.0f, 0.0f, 1.0f, 0.0f },
	{ 1.0f, 1.0f, 1.0f, 0.0f },
	{ 0.0f, 0.0f, 0.0f, 0.0f },
	{ 1.0f, 0.0f, 0.5f, 0.0f },
};

#define COLNUM (sizeof(coltable)/sizeof(coltable[0]))

static unsigned int colctr = 0;

static v4 getcol( void )
{
	v4 v;

	v = coltable[colctr++];
	if (colctr >= COLNUM) colctr = 0;

	return v;
}


static void sphere_update( worldinfo *w, int spid, int orbit_center, float orbit_radius, float orbit_tilt, int orbit_speed, float obj_radius, v4 obj_col, int obj_offset )
{
	sphere *sp = &w->spheres[spid];
	sp->orbit_center = orbit_center;
	sp->orbit_radius = orbit_radius;
	sp->orbit_tilt   = orbit_tilt;
	sp->orbit_speed  = orbit_speed;
	sp->obj_radius   = obj_radius;
	sp->obj_col      = obj_col;
	sp->obj_offset   = obj_offset;
}

void world_init( worldinfo *w, gpu_tracer *gt, int width, int height )
{
	gt->eye       = v4_set(  0.0f,  0.0f, -8.0f, 0.0f );
	gt->light_pos = v4_set( -4.0f, -6.0f, -9.5f, 0.0f );

	w->width  = width;
	w->height = height;

	gt->ax = 10.0f * w->width / w->height;
	gt->ay = 10.0f;

	gt->startpos = v4_set( gt->ax * -0.5f - gt->eye.x,
						   gt->ay * -0.5f - gt->eye.y,
						  -gt->eye.z,
						   0.0f );

	w->curcol = 0;

	w->largecnt = OBJNUM / 2;
	w->smallcnt = OBJNUM - w->largecnt - 1;

	float tilt = -PI;
	float tilt_add = (PI * 2.0f) / (w->largecnt / 2.0f);
	int moonsp_adj = 0;
	int planetsp_adj = 0;
	int planetoffset = 19;
	int moonoffset = 9;

	// sun in the center
	sphere_update( w, SUNPOS, -1, 0.0f, 0.0f, 360, RAD_SUN, getcol(), 0 );

	w->spheres[SUNPOS].obj_pos = v4_set( 0.0f, 0.0f, 0.0f, 0.0f );

	// planets orbiting the sun
	for( int i = 0; i < w->largecnt; i++ ) {
		float dist = i >= w->largecnt / 2 ? DIST_OUTER  : DIST_INNER;
		int  speed = i >= w->largecnt / 2 ? SPEED_OUTER : SPEED_INNER;
		sphere_update( w, i + 1, SUNPOS, dist, tilt, speed + planetsp_adj * i, RAD_PLANET, getcol(), i * planetoffset );
		tilt += tilt_add;
	}

	// moons orbiting the planets
	tilt = -PI;

	for( int i = 0; i < w->smallcnt; i++ ) {
		sphere_update( w, w->largecnt + i + 1, i + 1, DIST_MOON, tilt, SPEED_MOON + moonsp_adj * i, RAD_MOON, getcol(), i * moonoffset );
		tilt += tilt_add;
	}

	// shadow plane
	gt->plane_norm = v4_normalize3( v4_set( 0.0f, -1.0f, 0.0f, 6.5f ) );
	gt->plane_col = v4_set( 0.0f, 1.0f, 0.0f, 0.0f );

	w->frame  = 0;
	gt->frame = 0.0f;
	world_timer(w, gt);
	w->frame  = 0;
	gt->frame = 0.0f;
}

v4 rotate( float frac, float tilt, float radius )
{
	float yv = sinf( tilt );
	float xv = cosf( tilt );
	float sinxy = radius * sinf( frac * (PI * 2.0f) );

	return v4_set( sinxy * xv,
				   sinxy * yv,
				   -radius * cosf( frac * (PI * 2.0f) ),
				   0.0f );
}

static const v4 cols[7] = {
	{ 1.0f, 0.0f, 0.0f, 0.0f },
	{ 1.0f, 1.0f, 0.0f, 0.0f },
	{ 0.0f, 1.0f, 0.0f, 0.0f },
	{ 0.0f, 1.0f, 1.0f, 0.0f },
	{ 0.0f, 0.0f, 1.0f, 0.0f },
	{ 1.0f, 0.0f, 1.0f, 0.0f },
	{ 1.0f, 0.0f, 0.0f, 0.0f }
};

v4 h2rgb( float h )
{
	h = h - floorf( h );
	float Slice     = 6.0f * h;
	float SliceInt  = floorf( Slice );
	float SliceFrac = Slice - SliceInt;

	int i = (int)SliceInt;
	return v4_mix( cols[i], cols[i + 1], SliceFrac );
}

void world_timer( worldinfo *w, gpu_tracer *gt )
{
	for( int i = 0; i < OBJNUM; i++ ) {
		sphere *sp = &w->spheres[i];

		float frac = ((w->frame + sp->obj_offset) % sp->orbit_speed) / (float)(sp->orbit_speed);

		v4 rotv = rotate( frac, sp->orbit_tilt, sp->orbit_radius );

		v4 rot_pos;

		if( sp->orbit_center == -1 )
			rot_pos = v4_set( 0.0f, 0.0f, 0.0f, 0.0f );
		else
			rot_pos = w->spheres[sp->orbit_center].obj_pos;

		w->spheres[i].obj_pos = v4_add3( rot_pos, rotv );
		w->spheres[i].obj_pos.w = sp->obj_radius * sp->obj_radius;

		if( i == SUNPOS )
			w->spheres[i].obj_col = h2rgb( w->frame / 720.0f );
		else if (i < w->largecnt)
			w->spheres[i].obj_col = h2rgb( (w->frame / 300.f + (PI + sp->orbit_tilt) / 10.0f * PI) );

		// Large planet without moon always black
		w->spheres[w->largecnt].obj_col = v4_set( 0.0f, 0.0f, 0.0f, 0.0f );
	}

	float colf = 1.0f;
	gt->plane_col = v4_mul3( h2rgb( w->frame / 540.0f ), colf );

	// plane tilt
	float frac = (w->frame - 60 * 8) / (float)180;
	gt->plane_norm = v4_normalize3( v4_set( sinf( frac ) / 4.0f, -1.0f, 0.0f, 6.5f ) );

	// light movement
	frac = w->frame / (float)180;
	gt->light_pos = v4_set( -4.0f + sinf(frac) * 8.0f,
							-6.0f + cosf(frac) * 4.0f,
							-9.5f + sinf(frac  * 2.0f) * 3.0f, 0.0f );

	w->frame++;
	gt->frame += 1.0f;
}

typedef struct {
	v4 obj_pos;
	v4 obj_col;
} poscol;

static int sortfunc( const void *a, const void *b )
{
	poscol *va = (poscol *)a;
	poscol *vb = (poscol *)b;
	return va->obj_pos.z - vb->obj_pos.z > 0.0f ? 1 : -1;
}

void world_probe( worldinfo *w, gpu_tracer *gt )
{
	uint8_t r0[BLOCKSX + 1];
	uint8_t r1[BLOCKSX + 1];
	uint8_t *row0, *row1;

	// depth sort and copy to output table
	poscol pc[OBJNUM];

	for( int i = 0; i < OBJNUM; i++ ) {
		pc[i].obj_pos = w->spheres[i].obj_pos;
		pc[i].obj_col = w->spheres[i].obj_col;
	}

	qsort( &pc[1], OBJNUM - 1, sizeof(poscol), sortfunc );

	for( int i = 0; i < OBJNUM; i++ ) {
		gt->obj_pos[i] = pc[i].obj_pos;
		gt->obj_col[i] = pc[i].obj_col;
	}

	// check corners of 16x16 blocks
	row0 = r0;
	row1 = NULL;

	v3 ray_pos = v4_get3(gt->eye);

	int widthr  = (w->width  + BLOCKW - 1) & ~(BLOCKW - 1);
	int heightr = (w->height + BLOCKH - 1) & ~(BLOCKH - 1);

	for( int y = 0; y <= heightr; y += BLOCKH ) {

		float fy = y / (float)w->height;

		uint8_t *dst = row0;

		for( int x = 0; x <= widthr; x += BLOCKW ) {

			float fx = x / (float)w->width;

			v3 startpos = v3_normalize( v3_set( gt->startpos.x + gt->ax * fx,
												gt->startpos.y + gt->ay * fy,
												gt->startpos.z ) );

			int i;
			for( i = 0; i < OBJNUM; i += 4 ) {

				v3 vv0 = v3_sub( ray_pos, v4_get3( gt->obj_pos[i + 0] ) );
				v3 vv1 = v3_sub( ray_pos, v4_get3( gt->obj_pos[i + 1] ) );
				v3 vv2 = v3_sub( ray_pos, v4_get3( gt->obj_pos[i + 2] ) );
				v3 vv3 = v3_sub( ray_pos, v4_get3( gt->obj_pos[i + 3] ) );
				float rv0 = -v3_dot( vv0, vv0 ) + gt->obj_pos[i + 0].w;
				float rv1 = -v3_dot( vv1, vv1 ) + gt->obj_pos[i + 1].w;
				float rv2 = -v3_dot( vv2, vv2 ) + gt->obj_pos[i + 2].w;
				float rv3 = -v3_dot( vv3, vv3 ) + gt->obj_pos[i + 3].w;

				if( v3_dotsq( vv0, startpos ) + rv0 > 0.0f ||
					v3_dotsq( vv1, startpos ) + rv1 > 0.0f ||
					v3_dotsq( vv2, startpos ) + rv2 > 0.0f ||
					v3_dotsq( vv3, startpos ) + rv3 > 0.0f) break;

			}

			*dst++ = i == OBJNUM ? BLOCK_BG : BLOCK_SPH;
			// could also get BLOCK_SKY/BLOCK_PLN, but it's not paying off yet

		}

		if (row1 == NULL) {
			row0 = r1;
			row1 = r0;
			continue;
		}

		uint8_t *tmp;
		tmp = row0;
		row0 = row1;
		row1 = tmp;

		uint8_t *proberow = gt->probe + ((y - BLOCKH) / BLOCKH) * BLOCKSX;

		for( int x = 0; x < widthr / BLOCKW; x++ ) {
			*proberow++ = row0[x] & row0[x + 1] & row1[x] & row1[x + 1];
		}

	}

}
