/******************************************************************************
 * Copyright 1986-2002 by mental images GmbH & Co.KG, Fasanenstr. 81, D-10623
 * Berlin, Germany. All rights reserved.
 ******************************************************************************
 * Author:	thomas
 * Created:	1.11.96
 * Module:	shader
 * Purpose:	out_depth_of_field shader for raylib
 *
 * Exports: 
 *	out_depth_of_field		outpus shader for depth of field
 *
 *
 * Description:
 * This is a direct port from 1.9, ANSIfied plus minor portability fixes.
 *****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>
#include <shader.h>
#include <mi_softshade.h>

/*
 *			Depth of field output shader
 */


/*
 * This is only a preprocessing shader.
 * It operates on the rgba and the depth image
 * and rewrites the according to the depth values filtered rgba image.
 */

#ifdef DEBUG
#define register
#define DOF_DEBUG
#endif

#ifdef WIN_NT
#undef rad1
#undef near
#undef far
#define near really_near
#define far really_far
#endif

#define MAX_SIZE	64

static int		debug_x = 100;	/* pixel to debug */
static int		debug_y = -100;	/* pixel to debug */
static int		n;		/* max. filter radius */
static int		x_res;		/* image x resolution */
static int		y_res;		/* image y resolution */
static miImg_image	*frame_rgba;	/* rgba frame buffer */
static miImg_image	*frame_z;	/* depth frame buffer */
static double		near_plane;	/* near focus plane */
static double		far_plane;	/* far focus plane */
static double		size_factor;	/* filter radius factor */

static double	dist_tab[MAX_SIZE][MAX_SIZE];/* table of pixel distances */

					/* temporary buffers for MAX_SIZE
					 * scanlines: */
static miColor	*src_buf[MAX_SIZE];	/* source image */
static miColor	*res_buf[MAX_SIZE];	/* result image */
static float	*z_buf[MAX_SIZE];	/* depth values */
static float	*rad_buf[MAX_SIZE];	/* blur radius at pixel depth */
static float	*w_buf[MAX_SIZE];	/* summed weight of result image */
static int	buf_line[MAX_SIZE];

static miBoolean init_dof(miState *, struct dof *);
static void free_dof(void);
static void read_line(int);
static void write_line(int);
static void process_buffer(int);
static void filter_pair(int, int, double,double,int,int,double,double,double);
static void filter_one(int, int, double, double);
static double factor(double, double);
static void add_value(miColor *, float *, miColor *, double);


int out_depth_of_field_version(void) {return(1);}

miBoolean out_depth_of_field(
	void		*result,
	miState		*state,
	struct dof	*paras)
{
	int		y0;

	result;
	if (!init_dof(state, paras)) {
		mi_warning("out_depth_of_field shader not applied");
		return miTRUE;
	}
	for (y0 = -n; y0 < y_res + n; y0++) {
		if (mi_par_aborted())
			break;
		if (y0 + n - 1 >= 0 && y0 + n - 1 < y_res)
			read_line(y0 + n - 1);
		process_buffer(y0);
		if (y0 >= 0 && y0 < y_res)
			write_line(y0);
	}
	free_dof();
	return miTRUE;
}


static miBoolean init_dof(
	miState		*state,
	struct dof	*paras)
{
	int		x, y;

	x_res = state->camera->x_resolution;
	y_res = state->camera->y_resolution;
	if (paras->max <= 0)
		n = 4;
	else if (paras->max > MAX_SIZE)
		n = MAX_SIZE;
	else
		for (n = 2; 2 * n <= paras->max; n *= 2) ;
	if (n != paras->max)
		mi_error("adjusted max filter size %d to %d",
			paras->max, n);
	frame_rgba = state->options->image[miRC_IMAGE_RGBA].p;
	if (!frame_rgba) {
		mi_error("no rgba frame buffer computed");
		return miFALSE;
	}
	frame_z = state->options->image[miRC_IMAGE_Z].p;
	if (!frame_z) {
		mi_error("no depth frame buffer computed");
		return miFALSE;
	}
	near_plane = fabs(paras->nearplane);
	far_plane = fabs(paras->farplane);
	if (far_plane < near_plane) {
		mi_error("far focus plane %g changed to near plane %g",
			far_plane, near_plane);
		far_plane = near_plane;
	}
	size_factor = paras->radius *
			(state->camera->focal / state->camera->aperture) *
			(state->camera->x_resolution / 2.0);
	if (size_factor <= 0.0) {
		mi_error("lens radius %g too small",
			size_factor);
		return miFALSE;
	}
	for (x = 0; x < n; x++)
		for (y = 0; y < n; y++)
			dist_tab[x][y] = sqrt(x * x + y * y);
	for (y = 0; y < n; y++) {
		src_buf[y] = mi_mem_allocate(x_res * sizeof(miColor));
		res_buf[y] = mi_mem_allocate(x_res * sizeof(miColor));
		z_buf[y] = mi_mem_allocate(x_res * sizeof(float));
		rad_buf[y] = mi_mem_allocate(x_res * sizeof(float));
		w_buf[y] = mi_mem_allocate(x_res * sizeof(float));
		buf_line[y] = -1;
	}
	return miTRUE;
}

static void free_dof(void)
{
	int		y;

	for (y = 0; y < n; y++) {
		mi_mem_release(src_buf[y]);
		mi_mem_release(res_buf[y]);
		mi_mem_release(z_buf[y]);
		mi_mem_release(rad_buf[y]);
		mi_mem_release(w_buf[y]);
	}
}

static void read_line(
	int		y)
{
	int		x;
	int		idx = y & (n - 1);
	miColor		*src = src_buf[idx];
	miColor		*res = res_buf[idx];
	float		*z = z_buf[idx];
	float		*rad = rad_buf[idx];
	float		*w = w_buf[idx];
	double		depth;

	buf_line[idx] = y;
	for (x = 0; x < x_res; x++) {
		mi_img_get_color(frame_rgba, src + x, x, y);
		mi_img_get_depth(frame_z, z + x, x, y);
		res[x].r =
		res[x].g =
		res[x].b =
		res[x].a = 0;
		w[x] = 0;
		depth = z[x];
		if (depth <= 0.0) {
			z[x] = 1e30;
			rad[x] = size_factor * (1.0 / far_plane);
		} else if (depth < near_plane)
			rad[x] = size_factor * (1.0 / depth - 1.0 / near_plane);
		else if (depth <= far_plane)
			rad[x] = 0.0;
		else
			rad[x] = size_factor * (1.0 / far_plane - 1.0 / depth);
	}
}

static void write_line(
	int		y)
{
	int		idx = y & (n - 1);
	miColor		*res = res_buf[idx];
	float		*w = w_buf[idx];
	int		x;

	assert(buf_line[idx] == y);
	for (x = 0; x < x_res; x++) {
#ifdef DOF_DEBUG
		if (y == debug_y)
			mi_debug("%f %f %f w %f",
				res[x].r, res[x].g, res[x].b, w[x]);
#endif
		if (w[x] <= 0.0)
			mi_debug("pixel weight %f low", w[x]);
		mi_img_put_color(frame_rgba, res + x, x, y);
	}
}

static void process_buffer(
	int		y0)
{
	int		x0;
	int		idx0;
	double		z0;
	double		rad0;
	int		x1;
	int		y1;
	int		idx1;
	float		*z1_line;
	float		*rad1_line;
	double		z1;
	double		rad1;
	double		*dist_line;
	double		dist;
	miBoolean	same;

	if (y0 % 50 == 0)
		mi_progress("depth of field: line %d", y0);
	if (y0 < 0 || y0 >= y_res)
		return;
	idx0 = y0 & (n - 1);
	assert(buf_line[idx0] == y0);
	for (y1 = y0; y1 < y0 + n; y1++) {
		if (y1 >= y_res)
			break;
		idx1 = y1 & (n - 1);
		assert(buf_line[idx1] == y1);
		dist_line = dist_tab[y1 - y0];
		z1_line = z_buf[idx1];
		rad1_line = rad_buf[idx1];
		same = (y0 == y1);
		for (x0 = 0; x0 < x_res; x0++) {
			z0 = z_buf[idx0][x0];
			rad0 = rad_buf[idx0][x0];
			if (!same) {
				x1 = x0 - n + 1;
				if (x1 < 0)
					x1 = 0;
				for ( ; x1 < x0; x1++) {
					dist = dist_line[x0 - x1];
					z1 = z1_line[x1];
					rad1 = rad1_line[x1];
					if (z0 <= z1)
						filter_pair(
							x0, y0, z0, rad0,
							x1, y1, z1, rad1,
							dist);
					else
						filter_pair(
							x1, y1, z1, rad1,
							x0, y0, z0, rad0,
							dist);
				}
			}
			if (same)
				filter_one(x0, y0, z0, rad0);
			else {
				z1 = z1_line[x0];
				rad1 = rad1_line[x0];
				if (z0 <= z1)
					filter_pair(
						x0, y0, z0, rad0,
						x0, y1, z1, rad1,
						(double)(y1 - y0));
				else
					filter_pair(
						x0, y1, z1, rad1,
						x0, y0, z0, rad0,
						(double)(y1 - y0));
			}
			for (x1 = x0 + 1; x1 < x0 + n && x1 < x_res; x1++) {
				dist = dist_line[x1 - x0];
				rad1 = rad1_line[x1];
				/*
				if (dist > rad0 + 2 &&
				    dist > rad1 + 2)
					continue;
				*/
				z1 = z1_line[x1];
				if (z1 == 0.0 || z0 <= z1)
					filter_pair(
						x0, y0, z0, rad0,
						x1, y1, z1, rad1,
						dist);
				else
					filter_pair(
						x1, y1, z1, rad1,
						x0, y0, z0, rad0,
						dist);
			}
		}
	}
}

static void filter_pair(
	int		x0,
	int		y0,
	double		z0,
	double		rad0,
	int		x1,
	int		y1,
	double		z1,
	double		rad1,
	double		dist)
{
	double		f;
	int		idx0;
	int		idx1;

	assert(z0 <= z1);
	if ((z1 - z0) / dist > 10.0) {
		f = factor(dist, rad0);
		if (f <= 0.0)
			return;
		idx0 = y0 & (n - 1);
		idx1 = y1 & (n - 1);
		assert(buf_line[idx0] == y0);
		assert(buf_line[idx1] == y1);
		if (rad1 < rad0) {
			if (dist > rad1)
				add_value(&res_buf[idx1][x1], &w_buf[idx1][x1],
					  &src_buf[idx0][x0], 2.0 * f);
			else
			/* f *= (0.5 - 0.5 * (rad0 - rad1)) / rad1; */
				add_value(&res_buf[idx1][x1], &w_buf[idx1][x1],
					  &src_buf[idx0][x0], f);
		} else
			add_value(&res_buf[idx1][x1], &w_buf[idx1][x1],
				  &src_buf[idx0][x0], f);
#ifdef DOF_DEBUG
		if (x1 == debug_x && y1 == debug_y)
			mi_debug("%f * %f %f %f", f,
				src_buf[idx0][x0].r,
				src_buf[idx0][x0].g,
				src_buf[idx0][x0].b);
		if (x0 == debug_x && y0 == debug_y)
			mi_debug("%f * %f %f %f", f,
				src_buf[idx1][x1].r,
				src_buf[idx1][x1].g,
				src_buf[idx1][x1].b);
#endif
		/*
		add_value(&res_buf[idx0][x0], &w_buf[idx0][x0],
			  &src_buf[idx1][x1], f);
		*/
	} else {
		f = factor(dist, rad0);
		if (f <= 0.0)
			return;
		idx0 = y0 & (n - 1);
		idx1 = y1 & (n - 1);
		assert(buf_line[idx0] == y0);
		assert(buf_line[idx1] == y1);
		add_value(&res_buf[idx1][x1], &w_buf[idx1][x1],
			  &src_buf[idx0][x0], f);
		add_value(&res_buf[idx0][x0], &w_buf[idx0][x0],
			  &src_buf[idx1][x1], f);
#ifdef DOF_DEBUG
		if (x1 == debug_x && y1 == debug_y)
			mi_debug("%f * %f %f %f", f,
				src_buf[idx0][x0].r,
				src_buf[idx0][x0].g,
				src_buf[idx0][x0].b);
		if (x0 == debug_x && y0 == debug_y)
			mi_debug("%f * %f %f %f", f,
				src_buf[idx1][x1].r,
				src_buf[idx1][x1].g,
				src_buf[idx1][x1].b);
#endif
	}
}

static void filter_one(
	int		x0,
	int		y0,
	double		z0,
	double		rad0)
{
	double		f;
	int		idx0;

	z0;
	f = factor(0.0, rad0);
	if (f <= 0.0)
		return;
	idx0 = y0 & (n - 1);
	assert(buf_line[idx0] == y0);
	add_value(&res_buf[idx0][x0], &w_buf[idx0][x0],
		&src_buf[idx0][x0], f);
#ifdef DOF_DEBUG
	if (x0 == debug_x && y0 == debug_y)
		mi_debug("%f * %f %f %f", f,
			src_buf[idx0][x0].r,
			src_buf[idx0][x0].g,
			src_buf[idx0][x0].b);
#endif
}

static double factor(
	double		dist,
	register double	size)
{
	register double	f;

	if (dist < 0.5 && size < 0.5)
		return 1.0;
	if (dist < size - 0.5)
		f = 1.0;
	else if (dist < size + 0.5)
		f = size + 0.5 - dist;
	else
		return 0.0;
	if ((size * size * M_PI) < 1)
		return f;
	if (size < 0.5)
		return f;
	else if (size < n)
		return f / (size * size * M_PI);
	else if (size < 1.4 * n)
		return f / (size * n * 0.5 * (M_PI + 4.0));
	else
		return f / (4 * n * n);
}

static void add_value(
	register miColor	*res,
	float			*res_w,
	register miColor	*src,
	double			src_w)
{
	register float		w = *res_w;
	register float		w2 = src_w;
	register float		sum = w + w2;

	*res_w = sum;
	if (sum == 0.0) {
		mi_debug("pixel has weight %f", sum);
		return;
	}
	sum = 1.0 / sum;
	w *= sum;
	w2 *= sum;
	res->r = w * res->r + w2 * src->r;
	res->g = w * res->g + w2 * src->g;
	res->b = w * res->b + w2 * src->b;
	res->a = w * res->a + w2 * src->a;
}
