
/*
 * CINELERRA
 * Copyright (C) 2017 Adam Williams <broadcast at earthling dot net>
 * 
 * This program 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 2 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 */

#include "affine.h"
#include "bcdisplayinfo.h"
#include "bchash.h"
#include "bcsignals.h"
#include "clip.h"
#include "filexml.h"
#include "language.h"
#include "spherecam.h"
#include "theme.h"

#include <string.h>

// largely based on equations from http://paulbourke.net/dome/fish2/

REGISTER_PLUGIN(SphereCamMain)


SphereCamConfig::SphereCamConfig()
{
	feather = 0;
	
	for( int i = 0; i < EYES; i++ ) {
		enabled[i] = 1;
		fov[i] = 180;
		radius[i] = 100.0;
		center_y[i] = 50.0;
		rotate_y[i] = 50.0;
		rotate_z[i] = 0;
	}
	
	center_x[0] = 25.0;
	center_x[1] = 75.0;
	rotate_x[0] = 50.0;
	rotate_x[1] = 100.0;
	draw_guides = 1;
	mode = SphereCamConfig::DO_NOTHING;
}

int SphereCamConfig::equivalent(SphereCamConfig &that)
{
	for( int i = 0; i < EYES; i++ ) {
		if( enabled[i] != that.enabled[i] ||
			!EQUIV(fov[i], that.fov[i]) ||
			!EQUIV(radius[i], that.radius[i]) ||
			!EQUIV(center_x[i], that.center_x[i]) ||
			!EQUIV(center_y[i], that.center_y[i]) ||
			!EQUIV(rotate_x[i], that.rotate_x[i]) ||
			!EQUIV(rotate_y[i], that.rotate_y[i]) ||
			!EQUIV(rotate_z[i], that.rotate_z[i]) ) {
			return 0;
		}
	}

	if( feather != that.feather || mode != that.mode ||
	    draw_guides != that.draw_guides ) {
		return 0;
	}


	return 1;
}

void SphereCamConfig::copy_from(SphereCamConfig &that)
{
	*this = that;
}

void SphereCamConfig::interpolate(SphereCamConfig &prev, 
	SphereCamConfig &next, 
	int64_t prev_frame, 
	int64_t next_frame, 
	int64_t current_frame)
{
	double next_scale = (double)(current_frame - prev_frame) / (next_frame - prev_frame);
	double prev_scale = (double)(next_frame - current_frame) / (next_frame - prev_frame);

	for( int i = 0; i < EYES; i++ ) {
		enabled[i] = prev.enabled[i];
		fov[i] = prev.fov[i] * prev_scale + next.fov[i] * next_scale;
		radius[i] = prev.radius[i] * prev_scale + next.radius[i] * next_scale;
		center_x[i] = prev.center_x[i] * prev_scale + next.center_x[i] * next_scale;
		center_y[i] = prev.center_y[i] * prev_scale + next.center_y[i] * next_scale;
		rotate_x[i] = prev.rotate_x[i] * prev_scale + next.rotate_x[i] * next_scale;
		rotate_y[i] = prev.rotate_y[i] * prev_scale + next.rotate_y[i] * next_scale;
		rotate_z[i] = prev.rotate_z[i] * prev_scale + next.rotate_z[i] * next_scale;
	}

	feather = prev.feather * prev_scale + next.feather * next_scale;
	draw_guides = prev.draw_guides;
	mode = prev.mode;

	boundaries();
}

void SphereCamConfig::boundaries()
{
	for( int i = 0; i < EYES; i++ ) {
		CLAMP(fov[i], 1.0, 359.0);
		CLAMP(radius[i], 1.0, 150.0);
		CLAMP(center_x[i], 0.0, 100.0);
		CLAMP(center_y[i], 0.0, 100.0);
		CLAMP(rotate_x[i], 0.0, 100.0);
		CLAMP(rotate_y[i], 0.0, 100.0);
		CLAMP(rotate_z[i], -180.0, 180.0);
	}
	
	CLAMP(feather, 0, 50);
}


SphereCamSlider::SphereCamSlider(SphereCamMain *client, 
	SphereCamGUI *gui,
	SphereCamText *text,
	float *output, 
	int x, 
	int y, 
	float min,
	float max)
 : BC_FSlider(x, 
 	y, 
	0, 
	gui->get_w() / 2 - client->get_theme()->widget_border * xS(3) - xS(100),
	gui->get_w() / 2 - client->get_theme()->widget_border * xS(3) - xS(100),
	min, 
	max, 
	*output)
{
	this->gui = gui;
	this->client = client;
	this->output = output;
	this->text = text;
	set_precision(0.01);
}

int SphereCamSlider::handle_event()
{
	*output = get_value();
	text->update(*output);
	client->send_configure_change();
	return 1;
}


SphereCamText::SphereCamText(SphereCamMain *client, 
	SphereCamGUI *gui,
	SphereCamSlider *slider,
	float *output, 
	int x, 
	int y)
 : BC_TextBox(x, y, xS(100), 1, *output)
{
	this->gui = gui;
	this->client = client;
	this->output = output;
	this->slider = slider;
}

int SphereCamText::handle_event()
{
	*output = atof(get_text());
	slider->update(*output);
	client->send_configure_change();
	return 1;
}


SphereCamToggle::SphereCamToggle(SphereCamMain *client, 
	int *output, int x, int y, const char *text)
 : BC_CheckBox(x, y, *output, text)
{
	this->output = output;
	this->client = client;
}

int SphereCamToggle::handle_event()
{
	*output = get_value();
	client->send_configure_change();
	return 1;
}


SphereCamMode::SphereCamMode(SphereCamMain *plugin,  
	SphereCamGUI *gui, int x, int y)
 : BC_PopupMenu(x, y, calculate_w(gui), "", 1)
{
	this->plugin = plugin;
	this->gui = gui;
}

int SphereCamMode::handle_event()
{
	plugin->config.mode = from_text(get_text());
	plugin->send_configure_change();
	return 1;

}

void SphereCamMode::create_objects()
{
	add_item(new BC_MenuItem(to_text(SphereCamConfig::DO_NOTHING)));
	add_item(new BC_MenuItem(to_text(SphereCamConfig::EQUIRECT)));
	add_item(new BC_MenuItem(to_text(SphereCamConfig::ALIGN)));
	update(plugin->config.mode);
}

void SphereCamMode::update(int mode)
{
	char string[BCTEXTLEN];
	sprintf(string, "%s", to_text(mode));
	set_text(string);
}

int SphereCamMode::calculate_w(SphereCamGUI *gui)
{
	int result = 0;
	result = MAX(result, gui->get_text_width(MEDIUMFONT, to_text(SphereCamConfig::EQUIRECT)));
	result = MAX(result, gui->get_text_width(MEDIUMFONT, to_text(SphereCamConfig::DO_NOTHING)));
	result = MAX(result, gui->get_text_width(MEDIUMFONT, to_text(SphereCamConfig::ALIGN)));
	return result + xS(50);
}

int SphereCamMode::from_text(char *text)
{
	for( int i = 0; i < 3; i++ ) {
		if( !strcmp(text, to_text(i)) ) {
			return i;
		}
	}

	return SphereCamConfig::EQUIRECT;
}

const char* SphereCamMode::to_text(int mode)
{
	switch( mode ) {
	case SphereCamConfig::DO_NOTHING:
		return "Do nothing";
		break;
	case SphereCamConfig::EQUIRECT:
		return "Equirectangular";
		break;
	case SphereCamConfig::ALIGN:
		return "Align only";
		break;
	}
	return "Equirectangular";
}


SphereCamGUI::SphereCamGUI(SphereCamMain *client)
 : PluginClientWindow(client, xS(640), yS(600), xS(640), yS(600), 0)
{
	this->client = client;
}

SphereCamGUI::~SphereCamGUI()
{
}


void SphereCamGUI::create_objects()
{
	int margin = client->get_theme()->widget_border;
	int margin2 = margin;
	int y = margin;
	int x[EYES];

	x[0] = margin;
	x[1] = get_w() / 2 + margin / 2;

	BC_Title *title;

	for( int i = 0; i < EYES; i++ ) {
		int x3 = x[i];
		y = margin;

		add_tool(title = new BC_Title(x[i], y,
			i == 0 ? _("Left Eye") : _("Right Eye"), LARGEFONT));
		y += title->get_h() + margin2;
		
		add_tool(enabled[i] = new SphereCamToggle(client, 
			&client->config.enabled[i], x3, y, _("Enabled")));
		y += enabled[i]->get_h() + margin2;
		
		add_tool(title = new BC_Title(x3, y, _("FOV:")));
		y += title->get_h() + margin2;
		add_tool(fov_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.fov[i], x3, y, 1, 359));
		fov_slider[i]->set_precision(0.1);
		x3 += fov_slider[i]->get_w() + margin;
		add_tool(fov_text[i] = new SphereCamText(client, this,
			fov_slider[i], &client->config.fov[i], x3, y));
		fov_slider[i]->text = fov_text[i];
		y += fov_text[i]->get_h() + margin2;

		x3 = x[i];
		add_tool(title = new BC_Title(x3, y, _("Radius:")));
		y += title->get_h() + margin2;
		add_tool(radius_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.radius[i], x3, y, 1, 150));
		radius_slider[i]->set_precision(0.1);
		x3 += radius_slider[i]->get_w() + margin;
		add_tool(radius_text[i] = new SphereCamText(client, this,
			radius_slider[i], &client->config.radius[i], x3, y));
		radius_slider[i]->text = radius_text[i];
		y += radius_text[i]->get_h() + margin2;

		x3 = x[i];
		add_tool(title = new BC_Title(x3, y, _("Input X:")));
		y += title->get_h() + margin2;
		add_tool(centerx_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.center_x[i], x3, y, 0, 100));
		centerx_slider[i]->set_precision(0.1);
		x3 += centerx_slider[i]->get_w() + margin;
		add_tool(centerx_text[i] = new SphereCamText(client, this,
			centerx_slider[i], &client->config.center_x[i], x3, y));
		centerx_slider[i]->text = centerx_text[i];
		y += centerx_text[i]->get_h() + margin2;

		x3 = x[i];
		add_tool(title = new BC_Title(x3, y, _("Input Y:")));
		y += title->get_h() + margin2;
		add_tool(centery_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.center_y[i], x3, y, 0, 100));
		centery_slider[i]->set_precision(0.1);
		x3 += centery_slider[i]->get_w() + margin;
		add_tool(centery_text[i] = new SphereCamText(client, this,
			centery_slider[i], &client->config.center_y[i], x3, y));
		centery_slider[i]->text = centery_text[i];
		y += centery_text[i]->get_h() + margin2;

		x3 = x[i];
		add_tool(title = new BC_Title(x3, y, _("Output X:")));
		y += title->get_h() + margin2;
		add_tool(rotatex_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.rotate_x[i], x3, y, 0, 100));
		rotatex_slider[i]->set_precision(0.1);
		x3 += rotatex_slider[i]->get_w() + margin;
		add_tool(rotatex_text[i] = new SphereCamText(client, this,
			rotatex_slider[i], &client->config.rotate_x[i], x3, y));
		rotatex_slider[i]->text = rotatex_text[i];
		y += rotatex_text[i]->get_h() + margin2;

		x3 = x[i];
		add_tool(title = new BC_Title(x3, y, _("Output Y:")));
		y += title->get_h() + margin2;
		add_tool(rotatey_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.rotate_y[i], x3, y, 0, 100));
		rotatey_slider[i]->set_precision(0.1);
		x3 += rotatey_slider[i]->get_w() + margin;
		add_tool(rotatey_text[i] = new SphereCamText(client, this,
			rotatey_slider[i], &client->config.rotate_y[i], x3, y));
		rotatey_slider[i]->text = rotatey_text[i];
		y += rotatey_text[i]->get_h() + margin2;

		x3 = x[i];
		add_tool(title = new BC_Title(x3, y, _("Rotate:")));
		y += title->get_h() + margin2;
		add_tool(rotatez_slider[i] = new SphereCamSlider(client, this,
			0, &client->config.rotate_z[i], x3, y, -180, 180));
		rotatez_slider[i]->set_precision(0.1);
		x3 += rotatez_slider[i]->get_w() + margin;
		add_tool(rotatez_text[i] = new SphereCamText(client, this,
			rotatez_slider[i], &client->config.rotate_z[i], x3, y));
		rotatez_slider[i]->text = rotatez_text[i];
		y += rotatez_text[i]->get_h() + margin2;
	}

	int x3 = x[0];
	add_tool(title = new BC_Title(x3, y, _("Feather:")));
	y += title->get_h() + margin2;
	add_tool(feather_slider = new SphereCamSlider(client, this,
		0, &client->config.feather, x3, y, 0, 100));
	feather_slider->set_precision(0.1);
	x3 += feather_slider->get_w() + margin;
	add_tool(feather_text = new SphereCamText(client, this,
		feather_slider, &client->config.feather, x3, y));
	feather_slider->text = feather_text;
	y += feather_text->get_h() + margin2;

//printf("SphereCamGUI::create_objects %d %f\n", __LINE__, client->config.distance);

	x3 = x[0];
	add_tool(draw_guides = new SphereCamToggle(client, 
		&client->config.draw_guides, x3, y, _("Draw guides")));
	y += draw_guides->get_h() + margin2;
	
	add_tool(title = new BC_Title(x3, y, _("Mode:")));
	add_tool(mode = new SphereCamMode(client, 
		this, 
		x3 + title->get_w() + margin, 
		y));
	mode->create_objects();
	y += mode->get_h() + margin2;

	show_window();
	flush();
}


SphereCamMain::SphereCamMain(PluginServer *server)
 : PluginVClient(server)
{
	engine = 0;
	affine = 0;
}

SphereCamMain::~SphereCamMain()
{
	delete engine;
	delete affine;
}

NEW_WINDOW_MACRO(SphereCamMain, SphereCamGUI)
LOAD_CONFIGURATION_MACRO(SphereCamMain, SphereCamConfig)
int SphereCamMain::is_realtime() { return 1; }
const char* SphereCamMain::plugin_title() { return N_("Sphere Cam"); }

void SphereCamMain::update_gui()
{
	if( !thread ) return;
	if( !load_configuration() ) return;
	((SphereCamGUI*)thread->window)->lock_window("SphereCamMain::update_gui");
	SphereCamGUI *window = (SphereCamGUI*)thread->window;

	for( int i = 0; i < EYES; i++ ) {
		window->enabled[i]->update(config.enabled[i]);

		window->fov_slider[i]->update(config.fov[i]);
		window->fov_text[i]->update(config.fov[i]);

		window->radius_slider[i]->update(config.radius[i]);
		window->radius_text[i]->update(config.radius[i]);

		window->centerx_slider[i]->update(config.center_x[i]);
		window->centerx_text[i]->update(config.center_x[i]);

		window->centery_slider[i]->update(config.center_y[i]);
		window->centery_text[i]->update(config.center_y[i]);

		window->rotatex_slider[i]->update(config.rotate_x[i]);
		window->rotatex_text[i]->update(config.rotate_x[i]);

		window->rotatey_slider[i]->update(config.rotate_y[i]);
		window->rotatey_text[i]->update(config.rotate_y[i]);

		window->rotatez_slider[i]->update(config.rotate_z[i]);
		window->rotatez_text[i]->update(config.rotate_z[i]);
	}

	window->feather_slider->update(config.feather);
	window->feather_text->update(config.feather);

	window->mode->update(config.mode);
	window->draw_guides->update(config.draw_guides);
	window->unlock_window();
}

void SphereCamMain::save_data(KeyFrame *keyframe)
{
	FileXML output;
	char string[BCTEXTLEN];

// cause data to be stored directly in text
	output.set_shared_output(keyframe->xbuf);
	output.tag.set_title("SPHERECAM");
	
	for( int i = 0; i < EYES; i++ ) {
		sprintf(string, "ENABLED_%d", i);
		output.tag.set_property(string, config.enabled[i]);
		sprintf(string, "FOV_%d", i);
		output.tag.set_property(string, config.fov[i]);
		sprintf(string, "RADIUS_%d", i);
		output.tag.set_property(string, config.radius[i]);
		sprintf(string, "CENTER_X_%d", i);
		output.tag.set_property(string, config.center_x[i]);
		sprintf(string, "CENTER_Y_%d", i);
		output.tag.set_property(string, config.center_y[i]);
		sprintf(string, "ROTATE_X_%d", i);
		output.tag.set_property(string, config.rotate_x[i]);
		sprintf(string, "ROTATE_Y_%d", i);
		output.tag.set_property(string, config.rotate_y[i]);
		sprintf(string, "ROTATE_Z_%d", i);
		output.tag.set_property(string, config.rotate_z[i]);
	}
	
	output.tag.set_property("FEATHER", config.feather);
	output.tag.set_property("DRAW_GUIDES", config.draw_guides);
	output.tag.set_property("MODE", config.mode);
	output.append_tag();
	output.terminate_string();
}


void SphereCamMain::read_data(KeyFrame *keyframe)
{
	FileXML input;
	char string[BCTEXTLEN];


	input.set_shared_input(keyframe->xbuf);

	int result = 0;

	while(!result)
	{
		result = input.read_tag();

		if( !result ) {
			if( input.tag.title_is("SPHERECAM") ) {
				for( int i = 0; i < EYES; i++ ) {
					sprintf(string, "ENABLED_%d", i);
					config.enabled[i] = input.tag.get_property(string, config.enabled[i]);
					sprintf(string, "FOV_%d", i);
					config.fov[i] = input.tag.get_property(string, config.fov[i]);
					sprintf(string, "RADIUS_%d", i);
					config.radius[i] = input.tag.get_property(string, config.radius[i]);
					sprintf(string, "CENTER_X_%d", i);
					config.center_x[i] = input.tag.get_property(string, config.center_x[i]);
					sprintf(string, "CENTER_Y_%d", i);
					config.center_y[i] = input.tag.get_property(string, config.center_y[i]);
					sprintf(string, "ROTATE_X_%d", i);
					config.rotate_x[i] = input.tag.get_property(string, config.rotate_x[i]);
					sprintf(string, "ROTATE_Y_%d", i);
					config.rotate_y[i] = input.tag.get_property(string, config.rotate_y[i]);
					sprintf(string, "ROTATE_Z_%d", i);
					config.rotate_z[i] = input.tag.get_property(string, config.rotate_z[i]);
				}

				config.feather = input.tag.get_property("FEATHER", config.feather);
				config.draw_guides = input.tag.get_property("DRAW_GUIDES", config.draw_guides);
				config.mode = input.tag.get_property("MODE", config.mode);
			}
		}
	}
}



int SphereCamMain::process_buffer(VFrame *frame,
	int64_t start_position,
	double frame_rate)
{
	load_configuration();
	
	VFrame *input = new_temp(frame->get_w(), frame->get_h(), frame->get_color_model());
	
	read_frame(input, 0, start_position, frame_rate, 0); // use opengl

//	find_lenses();
	calculate_extents();

	if( config.mode == SphereCamConfig::DO_NOTHING ) {
		get_output()->copy_from(input);
	}
	else {
		get_output()->clear_frame();
		if( !engine ) engine = new SphereCamEngine(this);
		engine->process_packages();
	}

	if( config.draw_guides ) {
// input regions
// printf("SphereCamMain::process_buffer %d %d %d\n", __LINE__, out_x1[0], out_x4[0]);
		for( int eye = 0; eye < EYES; eye++ ) {
			if( config.enabled[eye] ) {
// input regions
				get_output()->draw_oval(input_x[eye] - radius[eye], 
					input_y[eye] - radius[eye], 
					input_x[eye] + radius[eye], 
					input_y[eye] + radius[eye]);


// output regions.  If they overlap, don't xor out the line
				if( eye == 0 || (out_x1[eye] != out_x1[0] && out_x1[eye] != out_x4[0]) ) {
					get_output()->draw_line(out_x1[eye], 0, out_x1[eye], h);
				}
				
				if( eye == 0 || (out_x4[eye] != out_x4[0] && out_x4[eye] != out_x1[0]) ) {
					get_output()->draw_line(out_x4[eye], 0, out_x4[eye], h);
				}

				if( feather != 0 && eye == 1 ) {
// crosshatch feather of right eye only, since only the right eye feathers
					int feather_gap = h / 20;
					for( int j = 0; j < h + feather; j += feather_gap ) {
						draw_feather(out_x1[eye], out_x1[eye] + feather, eye, j);
						draw_feather(out_x4[eye] - feather, out_x4[eye], eye, j);
					}
				}
			}
		}

	}

	return 0;
}

void SphereCamMain::draw_feather(int left, int right, int eye, int y)
{
	int slope = 1;
	if( eye == 1 ) {
		slope = -1;
	}

// wrap
	if( left < 0 ) {
		int left2 = w + left;
		get_output()->draw_line(left2, y, left2 + right - left, y + feather * slope);
	}

	if( right > w ) {
		int right2 = right - w;
		get_output()->draw_line(0, y, right2, y + feather * slope);

	}

// proper
	get_output()->draw_line(left, y, right, y + feather * slope);

}


// void SphereCamMain::find_lenses()
// {
// }


void SphereCamMain::calculate_extents()
{
	w = get_output()->get_w();
	h = get_output()->get_h();

	feather = (int)(config.feather * w / 100);
	
	for( int i = 0; i < EYES; i++ ) {
// input regions
		input_x[i] = (int)(config.center_x[i] * w / 100);
		input_y[i] = (int)(config.center_y[i] * h / 100);
		radius[i] = (int)(h / 2 * config.radius[i] / 100);

// output regions
		output_x[i] = (int)(config.rotate_x[i] * w / 100);
		output_y[i] = (int)(config.rotate_y[i] * h / 100);


// Assume each lens fills 1/2 the width
		out_x1[i] = output_x[i] - w / 4 - feather / 2;
		out_x2[i] = output_x[i];
		out_x3[i] = output_x[i];
		out_x4[i] = output_x[i] + w / 4 + feather / 2;
	}

// If the output isn't 50% apart, we have to expand the left eye to fill the feathering region
//printf("SphereCamMain::calculate_extents %d %f\n", __LINE__, config.rotate_x[0] - config.rotate_x[1]);
	float x_separation = config.rotate_x[0] - config.rotate_x[1];
	if( !EQUIV(fabs(x_separation), 50) ) {
		if( x_separation < -50 ) {
			out_x4[0] += (-49.5 - x_separation) * w / 100;
		}
		else
		if( x_separation < 0 ) {
			out_x1[0] -= (x_separation + 50) * w / 100;
		}
		else
		if( x_separation < 50 ) {
			out_x4[0] += (50.5 - x_separation) * w / 100;
		}
		else {
			out_x1[0] -= (x_separation - 49.5) * w / 100;
		}
	}


// wrap around
	for( int i = 0; i < EYES; i++ ) {
		if( out_x1[i] < 0 ) {
			out_x1[i] = w + out_x1[i];
			out_x2[i] = w;
			out_x3[i] = 0;
		}

		if( out_x4[i] > w ) {
			out_x2[i] = w;
			out_x3[i] = 0;
			out_x4[i] -= w;
		}

// printf("SphereCamMain::calculate_extents %d i=%d x=%d y=%d radius=%d\n", 
// __LINE__, 
// i, 
// center_x[i],
// center_y[i],
// radius[i]);
	}
}

SphereCamPackage::SphereCamPackage()
 : LoadPackage() {}

SphereCamUnit::SphereCamUnit(SphereCamEngine *engine, SphereCamMain *plugin)
 : LoadClient(engine)
{
	this->plugin = plugin;
}


SphereCamUnit::~SphereCamUnit()
{
}



// interpolate & accumulate a pixel in the output
#define BLEND_PIXEL(type, components) \
 	if( x_in < 0.0 || x_in >= w - 1 || \
	    y_in < 0.0 || y_in >= h - 1 ) { \
		out_row += components; \
	} \
	else { \
		float y1_fraction = y_in - floor(y_in); \
		float y2_fraction = 1.0 - y1_fraction; \
		float x1_fraction = x_in - floor(x_in); \
		float x2_fraction = 1.0 - x1_fraction; \
		type *in_pixel1 = in_rows[(int)y_in] + (int)x_in * components; \
		type *in_pixel2 = in_rows[(int)y_in + 1] + (int)x_in * components; \
		for( int i = 0; i < components; i++ ) { \
			float value = in_pixel1[i] * x2_fraction * y2_fraction + \
				in_pixel2[i] * x2_fraction * y1_fraction + \
				in_pixel1[i + components] * x1_fraction * y2_fraction + \
				in_pixel2[i + components] * x1_fraction * y1_fraction; \
			value = *out_row * inv_a + value * a; \
			*out_row++ = (type)value; \
		} \
	}


// nearest neighbor & accumulate a pixel in the output
#define BLEND_NEAREST(type, components) \
 	if( x_in < 0.0 || x_in >= w - 1 || \
	    y_in < 0.0 || y_in >= h - 1 ) { \
		out_row += components; \
	} \
	else { \
		type *in_pixel = in_rows[(int)y_in] + (int)x_in * components; \
		for( int i = 0; i < components; i++ ) { \
			float value = in_pixel[i]; \
			value = *out_row * inv_a + value * a; \
			*out_row++ = (type)value; \
		} \
	}


#define COLORSPACE_SWITCH(function) \
	switch( plugin->get_input()->get_color_model() ) { \
	case BC_RGB888:     function(unsigned char, 3, 0x0); break; \
	case BC_RGBA8888:   function(unsigned char, 4, 0x0); break; \
	case BC_RGB_FLOAT:  function(float, 3, 0.0); break; \
	case BC_RGBA_FLOAT: function(float, 4, 0.0); break; \
	case BC_YUV888:     function(unsigned char, 3, 0x80); break; \
	case BC_YUVA8888:   function(unsigned char, 4, 0x80); break; \
	}

void SphereCamUnit::process_equirect(SphereCamPackage *pkg)
{
	VFrame *input = plugin->get_temp();
	VFrame *output = plugin->get_output();


// overlay a single eye
#define PROCESS_EQUIRECT(type, components, chroma) \
{ \
	type **in_rows = (type**)input->get_rows(); \
	type **out_rows = (type**)output->get_rows(); \
 \
	for( int out_y = row1; out_y < row2; out_y++ ) { \
		type *out_row = out_rows[out_y]; \
/* polar angle - -M_PI/2 to M_PI/2 */ \
		float y_diff = out_y - output_y; \
		float phi = M_PI / 2 * (y_diff / radius); \
 \
 		for( int out_x = 0; out_x < w; out_x++ ) { \
/* alpha */ \
 			float a = alpha[out_x]; \
 			float inv_a = 1.0f - a; \
			if( a > 0 ) { \
/* polar angle */ \
				float x_diff = out_x - output_x; \
/* -M_PI/2 to M_PI/2 */ \
				float theta = M_PI / 2 * (x_diff / radius); \
/* vector in 3D space */ \
				float vect_x = cos(phi) * sin(theta); \
				float vect_y = cos(phi) * cos(theta); \
				float vect_z = sin(phi); \
/* fisheye angle & radius */ \
				float theta2 = atan2(vect_z, vect_x) - rotate_z; \
				float phi2 = atan2(hypot(vect_x, vect_z), vect_y); \
				float r = radius * 2 * phi2 / fov; \
/* pixel in fisheye space */ \
				float x_in = input_x + r * cos(theta2); \
				float y_in = input_y + r * sin(theta2); \
 \
 				BLEND_PIXEL(type, components) \
			} \
			else { \
				out_row += components; \
			} \
		} \
	} \
}

	int row1 = pkg->row1;
	int row2 = pkg->row2;
	for( int eye = 0; eye < EYES; eye++ ) {
		if( plugin->config.enabled[eye] ) {
			int w = plugin->w;
			int h = plugin->h;
			float fov = plugin->config.fov[eye] * M_PI * 2 / 360;
//			float radius = plugin->radius[eye];
			float radius = h / 2;
			float input_x = plugin->input_x[eye];
			float input_y = plugin->input_y[eye];
			float output_x = plugin->output_x[eye];
			float output_y = plugin->output_y[eye];
			float rotate_z = plugin->config.rotate_z[eye] * M_PI * 2 / 360;
			int feather = plugin->feather;
			int out_x1 = plugin->out_x1[eye];
			int out_x2 = plugin->out_x2[eye];
			int out_x3 = plugin->out_x3[eye];
			int out_x4 = plugin->out_x4[eye];

// compute the alpha for every X
			float alpha[w];
			for( int i = 0; i < w; i++ ) {
				alpha[i] = 0;
			}

			for( int i = out_x1; i < out_x2; i++ ) {
				alpha[i] = 1.0f;
			}

			for( int i = out_x3; i < out_x4; i++ ) {
				alpha[i] = 1.0f;
			}
			
			if( eye == 1 ) {
				for( int i = out_x1; i < out_x1 + feather; i++ ) {
					float value = (float)(i - out_x1) / feather;
					if( i >= w ) {
						alpha[i - w] = value;
					}
					else {
						alpha[i] = value;
					}
				}

				for( int i = out_x4 - feather; i < out_x4; i++ ) {
					float value = (float)(out_x4 - i) / feather;
					if( i < 0 ) {
						alpha[w + i] = value;
					}
					else {
						alpha[i] = value;
					}
				}
			}
			

//printf("SphereCamUnit::process_equirect %d rotate_x=%f rotate_y=%f rotate_z=%f\n", 
// __LINE__, rotate_x, rotate_y, rotate_z);

			COLORSPACE_SWITCH(PROCESS_EQUIRECT)
		}
	}
}



void SphereCamUnit::process_align(SphereCamPackage *pkg)
{
	VFrame *input = plugin->get_temp();
	VFrame *output = plugin->get_output();


// overlay a single eye
#define PROCESS_ALIGN(type, components, chroma) \
{ \
	type **in_rows = (type**)input->get_rows(); \
	type **out_rows = (type**)output->get_rows(); \
 \
	for( int out_y = row1; out_y < row2; out_y++ ) { \
		type *out_row = out_rows[out_y]; \
/* polar angle */ \
/* -M_PI/2 to M_PI/2 */ \
		float y_diff = out_y - output_y; \
		float phi = M_PI / 2 * (y_diff / radius); \
 \
 		for( int out_x = 0; out_x < w; out_x++ ) { \
/* alpha */ \
 			float a = alpha[out_x]; \
 			float inv_a = 1.0f - a; \
			if( a > 0 ) { \
/* polar angle */ \
				float x_diff = out_x - output_x; \
/* -M_PI/2 to M_PI/2 */ \
				float theta = M_PI / 2 * (x_diff / radius); \
	/* vector in 3D space */ \
				float vect_x = cos(phi) * sin(theta); \
				float vect_y = cos(phi) * cos(theta); \
				float vect_z = sin(phi); \
	/* fisheye angle & radius */ \
				float theta2 = atan2(vect_z, vect_x) - rotate_z; \
				float phi2 = atan2(hypot(vect_x, vect_z), vect_y); \
				float r = radius * 2 * phi2 / fov; \
	/* pixel in fisheye space */ \
				float x_in = input_x + r * cos(theta2); \
				float y_in = input_y + r * sin(theta2); \
	\
 				BLEND_NEAREST(type, components) \
			} \
			else { \
				out_row += components; \
			} \
		} \
	} \
}

	int row1 = pkg->row1;
	int row2 = pkg->row2;
	for( int eye = 0; eye < EYES; eye++ ) {
		if( plugin->config.enabled[eye] ) {
			int w = plugin->w;
			int h = plugin->h;
			float fov = plugin->config.fov[eye] * M_PI * 2 / 360;
//			float radius = plugin->radius[eye];
			float radius = h / 2;
			float input_x = plugin->input_x[eye];
			float input_y = plugin->input_y[eye];
			float output_x = plugin->output_x[eye];
			float output_y = plugin->output_y[eye];
			float rotate_z = plugin->config.rotate_z[eye] * M_PI * 2 / 360;
			int out_x1 = plugin->out_x1[eye];
			int out_x2 = plugin->out_x2[eye];
			int out_x3 = plugin->out_x3[eye];
			int out_x4 = plugin->out_x4[eye];
// left eye is opaque.  right eye overlaps
// compute the alpha for every X
			float alpha[w];
			for( int i = 0; i < w; i++ ) alpha[i] = 0;
			float v = eye == 0 || !plugin->config.enabled[0] ? 1.0f : 0.5f;
			for( int i = out_x1; i < out_x2; i++ ) alpha[i] = v;
			for( int i = out_x3; i < out_x4; i++ ) alpha[i] = v;

			COLORSPACE_SWITCH(PROCESS_ALIGN)
		}
	}
}

void SphereCamUnit::process_package(LoadPackage *package)
{
	SphereCamPackage *pkg = (SphereCamPackage*)package;

	switch( plugin->config.mode ) {
	case SphereCamConfig::EQUIRECT:
		process_equirect(pkg);
		break;
	case SphereCamConfig::ALIGN:
		process_align(pkg);
		break;
	}
}


SphereCamEngine::SphereCamEngine(SphereCamMain *plugin)
 : LoadServer(plugin->PluginClient::smp + 1, plugin->PluginClient::smp + 1)
// : LoadServer(1, 1)
{
	this->plugin = plugin;
}

SphereCamEngine::~SphereCamEngine()
{
}

void SphereCamEngine::init_packages()
{
	for( int i = 0; i < LoadServer::get_total_packages(); i++ ) {
		SphereCamPackage *package = (SphereCamPackage*)LoadServer::get_package(i);
		package->row1 = plugin->get_input()->get_h() * i / LoadServer::get_total_packages();
		package->row2 = plugin->get_input()->get_h() * (i + 1) / LoadServer::get_total_packages();
	}
}

LoadClient* SphereCamEngine::new_client()
{
	return new SphereCamUnit(this, plugin);
}

LoadPackage* SphereCamEngine::new_package()
{
	return new SphereCamPackage;
}


