/* === S Y N F I G ========================================================= */
/*!	\file sphere_distort.cpp
**	\brief Implementation of the "Spherize" layer
**
**	\legal
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**	Copyright (c) 2007, 2008 Chris Moore
**	Copyright (c) 2011 Carlos López
**
**	This file is part of Synfig.
**
**	Synfig 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.
**
**	Synfig 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 Synfig.  If not, see <https://www.gnu.org/licenses/>.
**	\endlegal
*/
/* ========================================================================= */

/* === H E A D E R S ======================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include "sphere_distort.h"

#include <synfig/localization.h>
#include <synfig/general.h>

#include <synfig/string.h>
#include <synfig/context.h>
#include <synfig/paramdesc.h>
#include <synfig/renddesc.h>
#include <synfig/surface.h>
#include <synfig/value.h>
#include <synfig/transform.h>

#include <synfig/curve_helper.h>

#endif

/* === U S I N G =========================================================== */

using namespace synfig;
using namespace modules;
using namespace lyr_std;

/* === M A C R O S ========================================================= */

#ifndef PI
const double PI = 3.14159265;
#endif

enum
{
	TYPE_NORMAL = 0,
	TYPE_DISTH = 1, //axe the horizontal axis
	TYPE_DISTV = 2, //axe the vertical axis
	N_TYPES
};

/* === G L O B A L S ======================================================= */

SYNFIG_LAYER_INIT(Layer_SphereDistort);
SYNFIG_LAYER_SET_NAME(Layer_SphereDistort,"spherize");
SYNFIG_LAYER_SET_LOCAL_NAME(Layer_SphereDistort,N_("Spherize"));
SYNFIG_LAYER_SET_CATEGORY(Layer_SphereDistort,N_("Distortions"));
SYNFIG_LAYER_SET_VERSION(Layer_SphereDistort,"0.2");

/* === P R O C E D U R E S ================================================= */

/* === M E T H O D S ======================================================= */

/* === E N T R Y P O I N T ================================================= */

Layer_SphereDistort::Layer_SphereDistort():
param_center(ValueBase(Vector(0,0))),
param_radius(ValueBase(double(1))),
param_amount(ValueBase(double(1))),
param_type(ValueBase(int(TYPE_NORMAL))),
param_clip(ValueBase(false))
{
	SET_INTERPOLATION_DEFAULTS();
	SET_STATIC_DEFAULTS();
}


bool
Layer_SphereDistort::set_param(const String & param, const ValueBase &value)
{
	IMPORT_VALUE_PLUS(param_center,sync());
	IMPORT_VALUE_PLUS(param_radius,sync());
	IMPORT_VALUE(param_type);
	IMPORT_VALUE(param_amount);
	IMPORT_VALUE(param_clip);

	if(param=="percent" && param_amount.get_type()==value.get_type())
		return set_param("amount", value);

	return Layer::set_param(param,value);
}

ValueBase
Layer_SphereDistort::get_param(const String &param)const
{
	EXPORT_VALUE(param_center);
	EXPORT_VALUE(param_radius);
	EXPORT_VALUE(param_type);
	EXPORT_VALUE(param_amount);
	EXPORT_VALUE(param_clip);
	if(param=="percent")
		return get_param("amount");

	EXPORT_NAME();
	EXPORT_VERSION();

	return Layer::get_param(param);
}

void
Layer_SphereDistort::sync()
{
// Remove me?
}

Layer::Vocab
Layer_SphereDistort::get_param_vocab()const
{
	Layer::Vocab ret;

	ret.push_back(ParamDesc("center")
		.set_local_name(_("Position"))
		.set_description(_("Center of the sphere distortion"))
		.set_is_distance()
	);

	ret.push_back(ParamDesc("radius")
		.set_local_name(_("Radius"))
		.set_origin("center")
		.set_is_distance()
		.set_description(_("Size of the sphere distortion"))
	);

	ret.push_back(ParamDesc("amount")
		.set_local_name(_("Amount"))
		.set_is_distance(false)
		.set_description(_("Distortion intensity (Negative values invert the effect)"))
	);

	ret.push_back(ParamDesc("clip")
		.set_local_name(_("Clip"))
		.set_description(_("When checked, the area outside the Radius is not distorted"))
	);

	ret.push_back(ParamDesc("type")
		.set_local_name(_("Distort Type"))
		.set_description(_("Direction of the distortion"))
		.set_hint("enum")
		.set_static(true)
		.add_enum_value(TYPE_NORMAL,"normal",_("Spherize"))
		.add_enum_value(TYPE_DISTH,"honly",_("Vertical Bar"))
		.add_enum_value(TYPE_DISTV,"vonly",_("Horizontal Bar"))
	);

	return ret;
}

/*
	Spherical Distortion: maps an image onto a ellipsoid of some sort

	so the image coordinate (i.e. distance away from the center)
	will determine how things get mapped

	so with the radius and position the mapping would go as follows

	r = (pos - center) / radius	clamped to [-1,1]

	if it's outside of that range then it's not distorted
	but if it's inside of that range then it goes as follows

	angle = r * pi/2 (-pi/2,pi/2)

	newr = cos(angle)*radius

	the inverse of this is (which is actually what we'd be transforming it from


*/

inline float spherify(float f)
{
	if(f > -1 && f < 1 && f!=0)
		return sinf(f*(PI/2));
	else return f;
}

inline float unspherify(float f)
{
	if(f > -1 && f < 1 && f!=0)
		return asin(f)/(PI/2);
	else return f;
}

Point sphtrans(const Point &p, const Point &center, const float &radius,
											const Real &percent, int type, bool& clipped)
{
	const Vector v = (p - center) / radius;

	Point newp = p;
	const float t = percent;

	clipped=false;

	if(type == TYPE_NORMAL)
	{
		const float m = v.mag();
		float lerp(0);

		if(m <= -1 || m >= 1)
		{
			clipped=true;
			return newp;
		}else
		if(m==0)
			return newp;
		else
		if(t > 0)
		{
			lerp = (t*unspherify(m) + (1-t)*m);
		}else if(t < 0)
		{
			lerp = ((1+t)*m - t*spherify(m));
		}else lerp = m;

		const float d = lerp*radius;
		newp = center + v*(d/m);
	}

	else if(type == TYPE_DISTH)
	{
		float lerp(0);
		if(v[0] <= -1 || v[0] >= 1)
		{
			clipped=true;
			return newp;
		}else
		if(v[0]==0)
			return newp;
		else
		if(t > 0)
		{
			lerp = (t*unspherify(v[0]) + (1-t)*v[0]);
		}else if(t < 0)
		{
			lerp = ((1+t)*v[0] - t*spherify(v[0]));
		}else lerp = v[0];

		newp[0] = center[0] + lerp*radius;
	}

	else if(type == TYPE_DISTV)
	{
		float lerp(0);
		if(v[1] <= -1 || v[1] >= 1)
		{
			clipped=true;
			return newp;
		}
		else
		if(v[1]==0)
			return newp;
		else
		if(t > 0)
		{
			lerp = (t*unspherify(v[1]) + (1-t)*v[1]);
		}else if(t < 0)
		{
			lerp = ((1+t)*v[1] - t*spherify(v[1]));
		}else lerp = v[1];

		newp[1] = center[1] + lerp*radius;
	}

	return newp;
}

inline Point sphtrans(const Point &p, const Point &center, const Real &radius,
											const Real &percent, int type)
{
	bool tmp;
	return sphtrans(p, center, radius, percent, type, tmp);
}

Layer::Handle
Layer_SphereDistort::hit_check(Context context, const Point &pos)const
{
	Vector center=param_center.get(Vector());
	double radius=param_radius.get(double());
	double percent=param_amount.get(double());
	int type=param_type.get(int());
	bool clip=param_clip.get(bool());
	
	bool clipped;
	Point point(sphtrans(pos,center,radius,percent,type,clipped));
	if(clip && clipped)
		return 0;
	return context.hit_check(point);
}

Color
Layer_SphereDistort::get_color(Context context, const Point &pos)const
{
	Vector center=param_center.get(Vector());
	double radius=param_radius.get(double());
	double percent=param_amount.get(double());
	int type=param_type.get(int());
	bool clip=param_clip.get(bool());

	bool clipped;
	Point point(sphtrans(pos,center,radius,percent,type,clipped));
	if(clip && clipped)
		return Color::alpha();
	return context.get_color(point);
}

RendDesc
Layer_SphereDistort::get_sub_renddesc_vfunc(const RendDesc &renddesc) const
{
	RendDesc desc(renddesc);
	Real pw = desc.get_pw();
	Real ph = desc.get_ph();
	desc.set_tl(Vector(-10.0, -10.0));
	desc.set_br(Vector( 10.0,  10.0));
	desc.set_wh(
		(int)approximate_ceil(fabs((desc.get_br()[0] - desc.get_tl()[0])/pw)),
		(int)approximate_ceil(fabs((desc.get_br()[1] - desc.get_tl()[1])/ph)) );
	return desc;
}

#if 1
bool
Layer_SphereDistort::accelerated_render(Context context,Surface *surface,int quality, const RendDesc &renddesc, ProgressCallback *cb)const
{
	RENDER_TRANSFORMED_IF_NEED(__FILE__, __LINE__)

	/*	Things to consider:
		1) Block expansion for distortion (ouch... quality level??)
		2) Bounding box clipping
		3) Super sampling for better visual quality (based on the quality level?)
		4) Interpolation type for sampling (based on quality level?)

		//things to defer until after
		super sampling, non-linear interpolation
	*/

	//bounding box reject
	Vector center=param_center.get(Vector());
	double radius=param_radius.get(double());
	double percent=param_amount.get(double());
	int type=param_type.get(int());
	bool clip=param_clip.get(bool());
	{
		Rect	sphr;

		sphr.set_point(center[0]-radius,center[1]-radius);
		sphr.expand(center[0]+radius,center[1]+radius);

		//get the bounding box of the transform
		Rect	windr;

		//and the bounding box of the rendering
		windr.set_point(renddesc.get_tl()[0],renddesc.get_tl()[1]);
		windr.expand(renddesc.get_br()[0],renddesc.get_br()[1]);

		//test bounding boxes for collision
		if( (type == TYPE_NORMAL && !rect_intersect(sphr,windr)) ||
			(type == TYPE_DISTH && (sphr.minx >= windr.maxx || windr.minx >= sphr.maxx)) ||
			(type == TYPE_DISTV && (sphr.miny >= windr.maxy || windr.miny >= sphr.maxy)) )
		{
			//warning("Spherize: Bounding box reject");
			if (clip)
			{
				surface->set_wh(renddesc.get_w(), renddesc.get_h());
				surface->clear();
				return true;
			}
			else
				return context.accelerated_render(surface,quality,renddesc,cb);
		}

		//warning("Spherize: Bounding box accept");
	}

	//Ok, so we overlap some... now expand the window for rendering
	RendDesc r = renddesc;
	Surface background;
	Real pw = renddesc.get_pw(),ph = renddesc.get_ph();

	int nl=0,nt=0,nr=0,nb=0, nw=0,nh=0;
	Point tl = renddesc.get_tl(), br = renddesc.get_br();

	{
		//must enlarge window by pixel coordinates so go!

		//need to figure out closest and farthest point and distort THOSE

		Point origin[4] = {tl,tl,br,br};
		Vector v[4] = {Vector(0,br[1]-tl[1]),
					   Vector(br[0]-tl[0],0),
					   Vector(0,tl[1]-br[1]),
					   Vector(tl[0]-br[0],0)};

		Point close(0,0);
		Real t = 0;
		Rect	expandr(tl,br);

		//expandr.set_point(tl[0],tl[1]);
		//expandr.expand(br[0],br[1]);

		//warning("Spherize: Loop through lines and stuff");
		for(int i=0; i<4; ++i)
		{
			//warning("Spherize: 	%d", i);
			Vector p_o = center-origin[i];

			//project onto left line
			t = (p_o*v[i])/v[i].mag_squared();

			//clamp
			if (t < 0) t = 0;
			if (t > 1) t = 1;

			close = origin[i] + v[i]*t;

			//now get transforms and expand the rectangle to accommodate
			Point p = sphtrans(close,center,radius,percent,type);
			expandr.expand(p[0],p[1]);
			p = sphtrans(origin[i],center,radius,percent,type);
			expandr.expand(p[0],p[1]);
			p = sphtrans(origin[i]+v[i],center,radius,percent,type);
			expandr.expand(p[0],p[1]);
		}

		/*warning("Spherize: Bounding box (%f,%f)-(%f,%f)",
							expandr.minx,expandr.miny,expandr.maxx,expandr.maxy);*/

		//now that we have the bounding rectangle of ALL the pixels (should be...)
		//order it so that it's in the same orientation as the tl,br pair

		//warning("Spherize: Organize like tl,br");
		Point ntl(0,0),nbr(0,0);

		//sort x
		if(tl[0] < br[0])
		{
			ntl[0] = expandr.minx;
			nbr[0] = expandr.maxx;
		}
		else
		{
			ntl[0] = expandr.maxx;
			nbr[0] = expandr.minx;
		}

		//sort y
		if(tl[1] < br[1])
		{
			ntl[1] = expandr.miny;
			nbr[1] = expandr.maxy;
		}
		else
		{
			ntl[1] = expandr.maxy;
			nbr[1] = expandr.miny;
		}

		//now expand the window as needed
		Vector temp = ntl-tl;

		//pixel offset
		nl = (int)(temp[0]/pw)-1;
		nt = (int)(temp[1]/ph)-1;

		temp = nbr - br;
		nr = (int)(temp[0]/pw)+1;
		nb = (int)(temp[1]/ph)+1;

		nw = renddesc.get_w() + nr - nl;
		nh = renddesc.get_h() + nb - nt;

		//warning("Spherize: Setting subwindow (%d,%d) (%d,%d) (%d,%d)",nl,nt,nr,nb,nw,nh);
		r.set_subwindow(nl,nt,nw,nh);

		/*r = renddesc;
		nw = r.get_w(), nh = r.get_h();
		nl = 0, nt = 0;*/
	}

	//warning("Spherize: render background");
	if(!context.accelerated_render(&background,quality,r,cb))
	{
		warning("SphereDistort: Layer below failed");
		return false;
	}

	//now distort and check to make sure we aren't overshooting our bounds here
	int w = renddesc.get_w(), h = renddesc.get_h();
	surface->set_wh(w,h);

	Point sample = tl, sub = tl, trans(0,0);
	float xs = 0,ys = 0;
	int y=0,x=0;
	Real invpw = 1/pw, invph = 1/ph;
	Surface::pen	p = surface->begin();

	Point rtl = r.get_tl();

	//warning("Spherize: About to transform");

	for(y = 0; y < h; ++y, sample[1] += ph, p.inc_y())
	{
		sub = sample;
		for(x = 0; x < w; ++x, sub[0] += pw, p.inc_x())
		{
			bool clipped;
			trans=sphtrans(sub,center,radius,percent,type,clipped);
			if(clip && clipped)
			{
				p.put_value(Color::alpha());
				continue;
			}

			xs = (trans[0]-rtl[0])*invpw;
			ys = (trans[1]-rtl[1])*invph;

			if(!(xs >= 0 && xs < nw && ys >= 0 && ys < nh))
			{
				//warning("Spherize: we failed to account for %f,%f",xs,ys);
				p.put_value(context.get_color(trans));//Color::alpha());
				continue;
			}

			//sample at that pixel location based on the quality
			if(quality <= 4)	// cubic
				p.put_value(background.cubic_sample(xs,ys));
			else if(quality <= 5) // cosine
				p.put_value(background.cosine_sample(xs,ys));
			else if(quality <= 6) // linear
				p.put_value(background.linear_sample(xs,ys));
			else				// nearest
				p.put_value(background[round_to_int(ys)][round_to_int(xs)]);
		}
		p.dec_x(w);
	}

	return true;
}



#endif

class lyr_std::Spherize_Trans : public Transform
{
	etl::handle<const Layer_SphereDistort> layer;
public:
	Spherize_Trans(const Layer_SphereDistort* x):Transform(x->get_guid()),layer(x) { }

	Vector perform(const Vector& x)const
	{
		return sphtrans(x,layer->param_center.get(Vector()),layer->param_radius.get(double()),-layer->param_amount.get(double()),layer->param_type.get(int()));
	}

	Vector unperform(const Vector& x)const
	{
		return sphtrans(x,layer->param_center.get(Vector()),layer->param_radius.get(double()),-layer->param_amount.get(double()),layer->param_type.get(int()));
	}

	String get_string()const
	{
		return "spheredistort";
	}
};

etl::handle<Transform>
Layer_SphereDistort::get_transform()const
{
	return new Spherize_Trans(this);
}

Rect
Layer_SphereDistort::get_bounding_rect()const
{
	Vector center=param_center.get(Vector());
	double radius=param_radius.get(double());
	int type=param_type.get(int());
	bool clip=param_clip.get(bool());

	Rect bounds(Rect::full_plane());

	if (clip)
		return bounds;

	switch(type)
	{
		case TYPE_NORMAL:
			bounds=Rect(center[0]+radius, center[1]+radius,
						center[0]-radius, center[1]-radius);
			break;
		case TYPE_DISTH:
			bounds = Rect::vertical_strip(center[0]-radius, center[0]+radius);
			break;
		case TYPE_DISTV:
			bounds = Rect::horizontal_strip(center[1]-radius, center[1]+radius);
			break;
		default:
			break;
	}

	return bounds;
}
