// Copyright 2005 David Hilvert <dhilvert@auricle.dyndns.org>,
//                              <dhilvert@ugcs.caltech.edu>

/*  This file is part of the Anti-Lamenessing Engine.

    The Anti-Lamenessing Engine is free software; you can redistribute it
    and/or modify it under the terms of the GNU General Public License as
    published by the Free Software Foundation; either version 3 of the License,
    or (at your option) any later version.

    The Anti-Lamenessing Engine is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with the Anti-Lamenessing Engine; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

/*
 * d3/et.h: Represent 3D->2D projective transformations.
 */

#ifndef __pt_h__
#define __pt_h__

#include "space.h"
#include "et.h"

/*
 * Structure to describe a 3D->2D projective transformation.  3D information is
 * preserved by adding a depth element to the result.
 *
 * The following coordinate systems are considered:
 *
 * 	P: projective
 * 	C: local cartesian
 * 	W: world
 */

struct pt {
private:
	d2::transformation t;
	et euclidean;
	ale_pos _view_angle;
	ale_pos scale_factor;
	mutable ale_pos diag_per_depth;

public:	

	/*
	 * Constructor
	 */

	pt() : t(d2::transformation::eu_identity()) {
		_view_angle = M_PI / 4;
		scale_factor = 1;
		diag_per_depth = 0;
	}

	pt(d2::transformation t, et e, ale_pos va, ale_pos sf = 1) : t(t) {
		this->t = t;
		euclidean = e;
		_view_angle = va;
		scale_factor = sf;
		diag_per_depth = 0;
	}

	/*
	 * Output function
	 */

	void debug_output() {
		t.debug_output();
		euclidean.debug_output();
		fprintf(stderr, "[pt.do va=%f sf=%f dpd=%f\n]",
				(double) _view_angle, (double) scale_factor, (double) diag_per_depth);
	}

	/*
	 * Get euclidean transformation reference.
	 */

	et &e() {
		return euclidean;
	}

	/*
	 * Modify scale factor
	 */
	void scale(ale_pos sf) {
		scale_factor = sf;
	}

	/*
	 * Modify or get view angle
	 */
	void view_angle(ale_pos va) {
		diag_per_depth = 0;
		_view_angle = va;
	}

	ale_pos view_angle() {
		return _view_angle;
	}

	/*
	 * Get the 2D scale factor
	 */
	ale_pos scale_2d() const {
		return t.scale();
	}

	/*
	 * Transform W to C.
	 */
	point wc(point p) const {
		return euclidean(p);
	}

	/*
	 * Transform C to P for given width and height.
	 */
	point cp_generic(point p, ale_pos w, ale_pos h) const {
		/*
		 * Divide x and y by negative z
		 */

		p[0] /= -p[2];
		p[1] /= -p[2];

		/*
		 * Scale x and y
		 */

		ale_pos scaling_factor = (ale_pos) sqrt(w*w + h*h) 
		                       / (ale_pos) (2 * tan(_view_angle / 2));
		p[0] *= scaling_factor;
		p[1] *= scaling_factor;

		/*
		 * Add an offset so that the upper-left corner is the origin.
		 */

		p[0] += h / 2;
		p[1] += w / 2;

		return p;
	}

	/*
	 * Transform point p.
	 */
	struct point wp_generic(struct point p, ale_pos w, ale_pos h) const {
		return cp_generic(wc(p), w, h);
	}

	/*
	 * Width and height
	 */

	ale_pos scaled_width() const {
		return t.scaled_width() * scale_factor;
	}

	ale_pos scaled_height() const {
		return t.scaled_height() * scale_factor;
	}

	int scaled_in_bounds(point p) const {
		return (p[0] >= 0 && p[0] <= scaled_height() - 1
		     && p[1] >= 0 && p[1] <= scaled_width() - 1);
	}

	int unscaled_in_bounds(point p) const {
		return (p[0] >= 0 && p[0] <= unscaled_height() - 1
		     && p[1] >= 0 && p[1] <= unscaled_width() - 1);
	}

	ale_pos unscaled_width() const {
		return t.unscaled_width() * scale_factor;
	}

	ale_pos unscaled_height() const {
		return t.unscaled_height() * scale_factor;
	}

	/*
	 * Scaled transforms
	 */

	point cp_scaled(point p) const {
		return cp_generic(p, scaled_width(), scaled_height());
	}

	point wp_scaled(point p) const {
		return wp_generic(p, scaled_width(), scaled_height());
	}

	/*
	 * Unscaled transforms
	 */

	point cp_unscaled(point p) const {
		return cp_generic(p, unscaled_width(), unscaled_height());
	}

	point wp_unscaled(point p) const {
		return wp_generic(p, unscaled_width(), unscaled_height());
	}

	/*
	 * Transform P to C.
	 */
	point pc_generic(point p, ale_pos w, ale_pos h) const {
		/*
		 * Subtract offset
		 */

		p[0] -= h / 2;
		p[1] -= w / 2;

		/*
		 * Scale x and y
		 */

		ale_pos scaling_factor = (ale_pos) sqrt(w*w + h*h) 
		                       / (ale_pos) (2 * tan(_view_angle / 2));
		p[0] /= scaling_factor;
		p[1] /= scaling_factor;

		/*
		 * Multiply x and y by negative z
		 */

		p[0] *= -p[2];
		p[1] *= -p[2];

		return p;
	}

	/*
	 * Transform C to W
	 */
	point cw(point p) const {
		return euclidean.inverse_transform(p);
	}

	/*
	 * Transform P to W
	 */
	point pw_generic(point p, ale_pos w, ale_pos h) const {
		return cw(pc_generic(p, w, h));
	}

	/*
	 * Inverse transforms for scaled points.
	 */

	point pc_scaled(point p) const {
		return pc_generic(p, scaled_width(), scaled_height());
	}

	point pw_scaled(point p) const {
		return pw_generic(p, scaled_width(), scaled_height());
	}

	/*
	 * Inverse transforms for unscaled points.
	 */

	point pc_unscaled(point p) const {
		return pc_generic(p, unscaled_width(), unscaled_height());
	}

	point pw_unscaled(point p) const {
		return pw_generic(p, unscaled_width(), unscaled_height());
	}

	/*
	 * Density calculation
	 */

	ale_pos c_density_scaled(point p) const {
		ale_pos one_density = 1 / (pc_scaled(point(0, 0, -1)).lengthto(pc_scaled(point(0, 1, -1)))
				         * pc_scaled(point(0, 0, -1)).lengthto(pc_scaled(point(1, 0, -1))));

		return one_density / (p[2] * p[2]);
	}

	ale_pos w_density_scaled(point p) const {
		return c_density_scaled(wc(p));
	}

	ale_pos w_density_scaled_max(point w0, point w1, point w2) {
		point c0 = wc(w0);
		point c1 = wc(w1);
		point c2 = wc(w2);

		/*
		 * Select the point closest to the camera.
		 */

		if (c0[2] > c1[2] && c0[2] > c2[2])
			return c_density_scaled(c0);
		else if (c1[2] > c2[2])
			return c_density_scaled(c1);
		else
			return c_density_scaled(c2);
	}

private:
	void calculate_diag_per_depth() const {
		if (diag_per_depth != 0)
			return;
		ale_pos w = unscaled_width();
		ale_pos h = unscaled_height();

		diag_per_depth = sqrt(2) * (2 * tan(_view_angle / 2)) / sqrt(w*w + h*h);
	}

public:

	/*
	 * Get a trilinear coordinate for a given depth.
	 */
	ale_pos trilinear_coordinate(ale_pos depth, ale_pos diagonal) {
		calculate_diag_per_depth();

		return log(diagonal / (fabs(depth) * diag_per_depth)) / log(2);

	}

	/*
	 * Get a trilinear coordinate for a given position in the world and
	 * a given 2D diagonal distance.
	 */
	ale_pos trilinear_coordinate(point w, ale_pos diagonal) {
		return trilinear_coordinate(wc(w)[2], diagonal);
	}

	/*
	 * Get a trilinear coordinate for a given subspace.
	 */
	ale_pos trilinear_coordinate(const space::traverse &st) {
		point min = st.get_min();
		point max = st.get_max();
		point avg = (min + max) / (ale_pos) 2;

		ale_pos diagonal = min.lengthto(max) * (ale_pos) (sqrt(2) / sqrt(3));

		return trilinear_coordinate(avg, diagonal);
	}

	/*
	 * Get a diagonal distance for a given position in the world
	 * and a given trilinear coordinate.
	 */
	ale_pos diagonal_distance(point w, ale_pos coordinate) const {
		calculate_diag_per_depth();

		ale_pos depth = fabs(wc(w)[2]);
		ale_pos diagonal = pow(2, coordinate) * depth * diag_per_depth;

		return diagonal;
	}

	/*
	 * Get the 3D diagonal for a given depth and trilinear coordinate.
	 */
	ale_pos diagonal_distance_3d(ale_pos depth, ale_pos coordinate) const {
		calculate_diag_per_depth();
		return pow(2, coordinate) * fabs(depth) * diag_per_depth * (ale_pos) (sqrt(3) / sqrt(2));
	}

	/*
	 * Get the 1D distance for a given depth and trilinear coordinate.
	 */
	ale_pos distance_1d(ale_pos depth, ale_pos coordinate) const {
		calculate_diag_per_depth();
		return pow(2, coordinate) * fabs(depth) * diag_per_depth / (ale_pos) (sqrt(2));
	}

	ale_pos distance_1d(point iw, ale_pos coordinate) const {
		if (wc(iw)[2] >= 0)
			return point::undefined()[0];
		return distance_1d(-wc(iw)[2], coordinate);
	}

	/*
	 * Check for inclusion of a point in the bounding box of projected
	 * vertices.  This function returns non-zero when a point is included,
	 * when one of the vertices is infinite or undefined, or when a vertex
	 * is behind the point of projection.
	 * 
	 * WBB is assumed to contain {volume_min, volume_max}.
	 */

	int check_inclusion(const point *wbb, const d2::point &pc_min, const d2::point &pc_max, int scaled) const {

		assert(pc_min[0] <= pc_max[0]);
		assert(pc_min[1] <= pc_max[1]);

		int test[2][2] = {{0, 0}, {0, 0}};

		for (int x = 0; x < 2; x++)
		for (int y = 0; y < 2; y++)
		for (int z = 0; z < 2; z++) {

			point p = scaled ? wp_scaled(point(wbb[x][0], wbb[y][1], wbb[z][2]))
				         : wp_unscaled(point(wbb[x][0], wbb[y][1], wbb[z][2]));

			if (!p.finite())
				return 1;

			if (p[2] > 0)
				return 1;

			for (int d = 0; d < 2; d++) {
				if (p[d] <= pc_max[d])
					test[d][0] = 1;
				if (p[d] >= pc_min[d])
					test[d][1] = 1;
			}
		}

		for (int d = 0; d < 2; d++)
		for (int c = 0; c < 2; c++)
			if (test[d][c] == 0)
				return 0;

		return 1;
	}

	int check_inclusion_scaled(const point *wbb, d2::point pc_min, d2::point pc_max) const {
		return check_inclusion(wbb, pc_min, pc_max, 1);
	}

	int check_inclusion_scaled(const space::traverse &st, d2::point pc_min, d2::point pc_max) const {
		return check_inclusion_scaled(st.get_bounds(), pc_min, pc_max);
	}

	int check_inclusion_scaled(const space::traverse &st, d2::point pc) {
		return check_inclusion_scaled(st, pc, pc);
	}

	/*
	 * Get bounding box for projection of a subspace.
	 */

	void get_view_local_bb(point volume_min, point volume_max, point bb[2], int scaled) const {

		point min = point::posinf();
		point max = point::neginf();

		point wbb[2] = { volume_min, volume_max };

		for (int x = 0; x < 2; x++)
		for (int y = 0; y < 2; y++)
		for (int z = 0; z < 2; z++) {
			point p = scaled ? wp_scaled(point(wbb[x][0], wbb[y][1], wbb[z][2]))
				         : wp_unscaled(point(wbb[x][0], wbb[y][1], wbb[z][2]));
			
			for (int d = 0; d < 3; d++) {
				if (p[d] < min[d])
					min[d] = p[d];
				if (p[d] > max[d])
					max[d] = p[d];
			}
		}

		/*
		 * Clip bounding box to image extents.
		 */

		if (min[0] < 0)
			min[0] = 0;
		if (min[1] < 0)
			min[1] = 0;
		if (max[0] > scaled_height() - 1)
			max[0] = scaled_height() - 1;
		if (max[1] > scaled_width() - 1)
			max[1] = scaled_width() - 1;

		bb[0] = min;
		bb[1] = max;
	}

	void get_view_local_bb_unscaled(point volume_min, point volume_max, point bb[2]) const {
		get_view_local_bb(volume_min, volume_max, bb, 0);
	}

	void get_view_local_bb_scaled(point volume_min, point volume_max, point bb[2]) const {
		get_view_local_bb(volume_min, volume_max, bb, 1);
	}

	void get_view_local_bb_scaled(const space::traverse &st, point bb[2]) const {
		get_view_local_bb_scaled(st.get_min(), st.get_max(), bb);
	}

	void get_view_local_bb_unscaled(const space::traverse &t, point bb[2]) const {
		get_view_local_bb_unscaled(t.get_min(), t.get_max(), bb);
	}

	/*
	 * Get the in-bounds centroid for a subspace, if one exists.
	 */

	point centroid(point volume_min, point volume_max) const {

		point bb[2];

		get_view_local_bb_unscaled(volume_min, volume_max, bb);

		point min = bb[0];
		point max = bb[1];

		for (int d = 0; d < 2; d++)
		if (min[d] > max[d])
			return point::undefined();

		if (min[2] >= 0)
			return point::undefined();

		if (max[2] > 0)
			max[2] = 0;

		return (max + min) / 2;
	}

	point centroid(const space::traverse &t) {
		return centroid(t.get_min(), t.get_max());
	}

	/*
	 * Get the local space origin in world space.
	 */

	point origin() {
		return cw(point(0, 0, 0));
	}
};

#endif
