//
// FSpot.Widgets.Curve.cs
//
// Author(s):
//	Stephane Delcroix  <stephane@delcroix.org>
//
// Copyright (c) 2009 Novell, Inc.
//
// This is open source software. See COPYING for details.
//
// Ported from Gtk+, where this widget and is no longer really supported.
//

using System;
using System.Collections.Generic;
using Gtk;
using Gdk;

namespace FSpot.Widgets
{
	public class Curve : DrawingArea
	{
#region public API
		public Curve () : base ()
		{
			Events |= EventMask.ExposureMask
				| EventMask.PointerMotionMask
				| EventMask.PointerMotionHintMask
				| EventMask.EnterNotifyMask
				| EventMask.ButtonPressMask
				| EventMask.ButtonReleaseMask
				| EventMask.Button1MotionMask;
			ResetVector ();
		}

		public void Reset ()
		{
			CurveType old_type = CurveType;
			CurveType = CurveType.Spline;
			ResetVector ();
			EventHandler eh;
			if (old_type != CurveType.Spline && (eh = CurveTypeChanged) != null) 
				eh (this, EventArgs.Empty);
		}

		float min_x = 0f;
		public float MinX {
			get { return min_x; }
			set { SetRange (value, max_x, min_y, max_y); }
		}

		float max_x = 1.0f;
		public float MaxX {
			get { return max_x; }
			set { SetRange (min_x, value, min_y, max_y); }
		}

		float min_y = 0f;
		public float MinY {
			get { return min_y; }
			set { SetRange (min_x, max_x, value, max_y); }
		}

		float max_y = 1.0f;
		public float MaxY {
			get { return max_y; }
			set { SetRange (min_x, max_x, min_y, value); }
		}

		public void SetRange (float min_x, float max_x, float min_y, float max_y)
		{
			this.min_x = min_x;
			this.max_x = max_x;
			this.min_y = min_y;
			this.max_y = max_y;

			ResetVector ();
			QueueDraw ();
		}

		CurveType curve_type = CurveType.Spline;
		public CurveType CurveType {
			get { return curve_type; }
			set {
				curve_type = value;
				QueueDraw ();
			}
		}

		public float [] GetVector (int len)
		{
			if (len <= 0)
				return null;

			var vector = new float [len];

			var xv = new float [points.Count];
			var yv = new float [points.Count];
			int i = 0;
			foreach (var keyval in points) {
				xv[i] = keyval.Key;
				yv[i] = keyval.Value;
				i++;
			}
			float rx = MinX;
			float dx = (MaxX - MinX) / (len - 1.0f);

			switch (CurveType) {
			case CurveType.Spline:	
				var y2v = SplineSolve (xv, yv);

				for (int x = 0; x < len; x++, rx += dx) {
					float ry = SplineEval (xv, yv, y2v, rx);
					if (ry < MinY)
						ry = MinY;
					if (ry > MaxY)
						ry = MaxY;
					vector[x] = ry;
				}
				break;;
			case CurveType.Linear:
				for (int x = 0; x < len; x++, rx += dx) {
					float ry = LinearEval (xv, yv, rx);
					if (ry < MinY)
						ry = MinY;
					if (ry > MaxY)
						ry = MaxY;
					vector[x] = ry;
				}
				break;
			case CurveType.Free:
				throw new NotImplementedException ();
			}

			return vector;
		}

		public void SetVector (float[] vector)
		{
		}

		public void AddPoint (float x, float y)
		{
			points.Add (x, y);
			EventHandler eh = CurveChanged;
			if (eh != null)
				eh (this, EventArgs.Empty);
		}

		public event EventHandler CurveTypeChanged;
		public event EventHandler CurveChanged;
#endregion

#region vector handling
		SortedDictionary<float, float> points;
		void ResetVector ()
		{
			points = new SortedDictionary<float, float> ();
			points.Add (min_x, min_y);
			points.Add (max_x, max_y);
			points.Add (.2f, .1f);
			points.Add (.5f, .5f);
			points.Add (.8f, .9f);
		}
#endregion

#region math helpers
		/* Solve the tridiagonal equation system that determines the second
		   derivatives for the interpolation points. (Based on Numerical
		   Recipies 2nd Edition) */
		static float [] SplineSolve (float[] x, float[] y)
		{
			var y2 = new float [x.Length];
			var u = new float [x.Length - 1];

			y2[0] = u[0] = 0.0f;	//Set lower boundary condition to "natural"

			for (int i = 1; i < x.Length - 1; ++i) {
				float sig = (x[i] - x[i - 1]) / (x[i + 1] - x[i - 1]);
				float p = sig * y2[i - 1] + 2.0f;
				y2[i] = (sig - 1.0f) / p;
				u[i] = ((y[i + 1] - y[i]) / (x[i + 1] - x[i]) - (y[i] - y[i - 1]) / (x[i] - x[i - 1]));
				u[i] = (6.0f * u[i] / (x[i + 1] - x[i - 1]) - sig * u[i - 1]) / p;
			}

			y2[x.Length - 1] = 0.0f;
			for (int k = x.Length - 2; k >= 0; --k)
				y2[k] = y2[k] * y2[k + 1] + u[k];

			return y2;
		}

		/* Returns a y value for val, given x[], y[] and y2[] */
		static float SplineEval (float[] x, float[] y, float[] y2, float val)
		{
			//binary search for the right interval
			int k_lo = 0;
			int k_hi = x.Length - 1;
			while (k_hi - k_lo > 1) {
				int k = (k_hi + k_lo) / 2;
				if (x[k] > val)
					k_hi = k;
				else
					k_lo = k;
			}
			float h = x[k_hi] - x[k_lo];
			float a = (x[k_hi] - val) / h;
			float b = (val - x[k_lo]) / h;
			return a * y[k_lo] + b * y[k_hi] + ((a*a*a - a) * y2[k_lo] + (b*b*b - b) * y2[k_hi]) * (h*h)/6.0f;
		}

		static float LinearEval (float[] x, float[] y, float val)
		{
			//binary search for the right interval
			int k_lo = 0;
			int k_hi = x.Length - 1;
			while (k_hi - k_lo > 1) {
				int k = (k_hi + k_lo) / 2;
				if (x[k] > val)
					k_hi = k;
				else
					k_lo = k;
			}
			float dx = x[k_hi] - x[k_lo];
			float dy = y[k_hi] - y[k_lo];
			return val*dy/dx + y[k_lo] - dy/dx*x[k_lo];
		}

		static int Project (float val, float min, float max, int norm)
		{
			return (int)((norm - 1) * ((val - min) / (max - min)) + .5f);
		}

		static float Unproject (int val, float min, float max, int norm)
		{
			return val / (float) (norm - 1) * (max - min) + min;
		}
#endregion

#region Gtk widgetry
		const int radius = 3;		//radius of the control points
		const int min_distance = 8;	//min distance between control points
		int x_offset = radius;
		int y_offset = radius;
		int width, height;		//the real graph

		Pixmap pixmap = null;

		protected override bool OnConfigureEvent (EventConfigure evnt)
		{
			pixmap = null;
			return base.OnConfigureEvent (evnt);
		}

		protected override bool OnExposeEvent (EventExpose evnt)
		{
			pixmap = new Pixmap (GdkWindow, Allocation.Width, Allocation.Height);
			Draw ();
			return base.OnExposeEvent (evnt);
		}

		Gdk.Point [] Interpolate (int width, int height)
		{
			var vector = GetVector (width);
			var retval = new Gdk.Point [width];
			for (int i = 0; i < width; i++) {
				retval[i].X = x_offset + i;
				retval[i].Y = y_offset + height - Project (vector[i], MinY, MaxY, height);
			}
			return retval;
		}

		void Draw ()
		{
			if (pixmap == null)
				return;

			Style style = Style;
			StateType state = Sensitive ? StateType.Normal : StateType.Insensitive;

			if (width <= 0 || height <= 0)
				return;

			//clear the pixmap
			GtkBeans.Style.PaintFlatBox (style, pixmap, StateType.Normal, ShadowType.None, null, this, "curve_bg", 0, 0, Allocation.Width, Allocation.Height);

			//draw the grid lines
			for (int i = 0; i < 5; i++) {
				pixmap.DrawLine (style.DarkGC (state),
						 x_offset,
						 i * (int)(height / 4.0) + y_offset,
						 width + x_offset,
						 i * (int)(height / 4.0) + y_offset);
				pixmap.DrawLine (style.DarkGC (state),
						 i * (int)(width / 4.0) + x_offset,
						 y_offset,
						 i * (int)(width / 4.0) + x_offset,
						 height + y_offset);
			}

			//draw the curve
			pixmap.DrawPoints (style.ForegroundGC (state), Interpolate (width, height));

			//draw the bullets
			if (CurveType != CurveType.Free)
				foreach (var keyval in points) {
					if (keyval.Key < MinX)
						continue;
					int x = Project (keyval.Key, MinX, MaxX, width);
					int y = height - Project (keyval.Value, MinY, MaxY, height);
					pixmap.DrawArc (style.ForegroundGC (state), true, x, y, radius * 2, radius * 2, 0, 360*64);
				}
			GdkWindow.DrawDrawable (style.ForegroundGC (state), pixmap, 0, 0, 0, 0, Allocation.Width, Allocation.Height);
		}

		protected override void OnSizeAllocated (Rectangle allocation)
		{
			width = allocation.Width - 2 * radius;
			height = allocation.Height - 2 * radius;	
			base.OnSizeAllocated (allocation);
		}

		protected override void OnSizeRequested (ref Requisition requisition)
		{
			requisition.Width = 128 + 2 * x_offset;
			requisition.Height = 128 + 2 * y_offset;
		}

		float? grab_point = null;
		protected override bool OnButtonPressEvent (EventButton evnt)
		{
			int px = (int)evnt.X - x_offset;
			int py = (int)evnt.Y - y_offset;
			if (px < 0) px = 0;
			if (px > width - 1) px = width - 1;
			if (py < 0) py = 0;
			if (py > height - 1) py = height - 1;
			
			//find the closest point
			float closest_x = MinX - 1;
			var distance = Int32.MaxValue;
			foreach (var point in points) {
				int cx = Project (point.Key, MinX, MaxX, width);
				if (Math.Abs (px - cx) < distance) {
					distance = Math.Abs (px - cx);
					closest_x = point.Key;
				}
			}

			Grab.Add (this);
			CursorType = CursorType.Tcross;
			switch (CurveType) {
			case CurveType.Linear:
			case CurveType.Spline:
				if (distance > min_distance) {
					//insert a new control point
					AddPoint ((closest_x = Unproject (px, MinX, MaxX, width)), MaxY - Unproject (py, MinY, MaxY, height));
					QueueDraw ();
				}
				grab_point = closest_x;
				break;
			case CurveType.Free:
				throw new NotImplementedException ();
			}

			return true;
		}

		protected override bool OnButtonReleaseEvent (EventButton evnt)
		{
			Grab.Remove (this);
			//FIXME: remove inactive points

			CursorType = CursorType.Fleur;
			grab_point = null;
			return true;
		}

		protected override bool OnMotionNotifyEvent (EventMotion evnt)
		{
			int px = (int)evnt.X - x_offset;
			int py = (int)evnt.Y - y_offset;
			if (px < 0) px = 0;
			if (px > width - 1) px = width - 1;
			if (py < 0) py = 0;
			if (py > height - 1) py = height - 1;
			
			//find the closest point
			float closest_x = MinX - 1;
			var distance = Int32.MaxValue;
			foreach (var point in points) {
				int cx = Project (point.Key, MinX, MaxX, width);
				if (Math.Abs (px - cx) < distance) {
					distance = Math.Abs (px - cx);
					closest_x = point.Key;
				}
			}

			switch (CurveType) {
			case CurveType.Spline:
			case CurveType.Linear:
				if (grab_point == null) {		//No grabbed point
					if (distance <= min_distance)
						CursorType = CursorType.Fleur;
					else
						CursorType = CursorType.Tcross;
					return true;
				}

				CursorType = CursorType.Tcross;
				points.Remove (grab_point.Value);
				AddPoint ((closest_x = Unproject (px, MinX, MaxX, width)), MaxY - Unproject (py, MinY, MaxY, height));
				QueueDraw ();
				grab_point = closest_x;

				break;
			case CurveType.Free:
				throw new NotImplementedException ();
			}
			return true;
		}

		Gdk.CursorType cursor_type = Gdk.CursorType.TopLeftArrow;
		CursorType CursorType {
			get { return cursor_type; }
			set {
				if (value == cursor_type)
					return;
				cursor_type = value;
				GdkWindow.Cursor = new Cursor (CursorType);	
			}
		}
#endregion
	}
}
