/*
 * Copyright (C) 2009, 2010 Hermann Meyer, James Warden, Andreas Degert
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "GxKnob.h"
#include <cmath>


#ifndef min
#define min(x, y) ((x) < (y) ? (x) : (y))
#endif
#ifndef max
#define max(x, y) ((x) < (y) ? (y) : (x))
#endif

#define P_(s) (s)   // FIXME -> gettext

//#define GX_KNOB_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), GX_TYPE_KNOB, GxKnobPrivate))



struct _GxKnobPrivate
{
	gint last_quadrant;
};

static gboolean gx_knob_pointer_motion(GtkWidget *widget, GdkEventMotion *event);
static gboolean gx_knob_enter_in(GtkWidget *widget, GdkEventCrossing *event);
static gboolean gx_knob_leave_out(GtkWidget *widget, GdkEventCrossing *event);
static gboolean gx_knob_draw(GtkWidget *widget, cairo_t *cr);
static void gx_knob_get_preferred_width(GtkWidget *widget, gint *min_width, gint *natural_width);
static void gx_knob_get_preferred_height(GtkWidget *widget, gint *min_height, gint *natural_height);
static void gx_knob_size_request(GtkWidget *widget, gint *width, gint *height);
static gboolean gx_knob_button_press(GtkWidget *widget, GdkEventButton *event);


G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE(GxKnob, gx_knob, GX_TYPE_REGLER)

#define get_stock_id(widget) (GX_KNOB_CLASS(GTK_WIDGET_GET_CLASS(widget))->stock_id)

static void gx_knob_class_init(GxKnobClass *klass)
{
	GtkWidgetClass *widget_class = (GtkWidgetClass*) klass;
	widget_class->motion_notify_event = gx_knob_pointer_motion;
	widget_class->enter_notify_event = gx_knob_enter_in;
	widget_class->leave_notify_event = gx_knob_leave_out;
	widget_class->draw = gx_knob_draw;
	widget_class->get_preferred_width = gx_knob_get_preferred_width;
	widget_class->get_preferred_height = gx_knob_get_preferred_height;
	widget_class->button_press_event = gx_knob_button_press;

	gtk_widget_class_set_css_name(widget_class, "gx-knob");

	gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("arc-inset",P_("inset of arch"),
				 P_("Inset of the arc around the knob"),
		                 0, 100, 2, GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
		                    
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("framecount",
		                    P_("Framecount"),
		                    P_("Number of frames in the animation specified by the gtkrc"),
		                    -1, 250, 0,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
                            
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("x-center",
		                    P_("X-Center"),
		                    P_("Horizontal position of the center, -1 for auto."),
		                    -1, 250, -1,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("y-center",
		                    P_("Y-Center"),
		                    P_("Vertical position of the center, -1 for auto."),
		                    -1, 250, -1,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("ring-radius",
		                    P_("Ring-Radius"),
		                    P_("Radius of the outer edge of the ring, -1 for max possible."),
                            -1, 250, -1,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("ring-width",
		                    P_("Ring-Width"),
		                    P_("Width of the outer ring"),
		                    0, 250, 2,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("ring-led-size",
		                    P_("Ring-LED-Size"),
		                    P_("Length of one LED in the ring, 0 for seamless ring."),
		                    0, 250, 3,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("ring-led-distance",
		                    P_("Ring-LED-Distance"),
		                    P_("Distance between two LED in the ring"),
		                    0, 250, 2,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("indicator-radius",
		                    P_("Indicator-Radius"),
		                    P_("Radius of the outer edge of the indicator, -1 for half max."),
		                    -1, 250, -1,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("indicator-width",
		                    P_("Indicator-Width"),
		                    P_("Width of the indicator"),
		                    0, 250, 2,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
    gtk_widget_class_install_style_property(
		widget_class,
		g_param_spec_int("indicator-length",
		                    P_("Indicator-Length"),
		                    P_("Length of the indicator"),
		                    0, 250, 5,
		                    GParamFlags(G_PARAM_READABLE|G_PARAM_STATIC_STRINGS)));
                            
}

static void gx_knob_init(GxKnob *knob)
{
	knob->priv = (GxKnobPrivate*)gx_knob_get_instance_private(knob);
}

static const double scale_zero = 40 * (M_PI/180); // defines "dead zone" for knobs

void _gx_knob_draw_shtuff(GtkWidget *widget, cairo_t *cr, GdkRectangle *image_rect, gdouble knobstate)
{
	// add a value Indicator around the knob
    gint x0 = 0; //image_rect->x;
    gint y0 = 0; //image_rect->y;
    
    // style definitions
    gint ind_radius, ind_width, ind_length, ring_radius, ring_width;
    gint ring_led_size, ring_led_distance, x_center, y_center;
    gtk_widget_style_get(widget, "indicator_radius", &ind_radius, "indicator_width", &ind_width,
                                 "indicator_length", &ind_length, "ring_radius", &ring_radius,
                                 "ring_led_distance", &ring_led_distance,
                                 "ring_width", &ring_width, "ring_led_size", &ring_led_size,
                                 "x_center", &x_center, "y_center", &y_center, NULL);

    GtkStyleContext *style = gtk_widget_get_style_context(widget);
    // foreground and background color
    GdkRGBA color;
    gtk_style_context_get_color(style, gtk_widget_get_state_flags(widget), &color);
    GdkRGBA color_;
    gtk_style_context_get_color(style, GTK_STATE_FLAG_INSENSITIVE, &color_);

    // auto values in style?
    gint minrad = min(image_rect->width, image_rect->height) / 2;
    if(x_center < 0)
        x_center = image_rect->width / 2;
    if(y_center < 0)
        y_center = image_rect->height / 2;
    if(ring_radius < 0)
        ring_radius = minrad - ring_radius / 2;
    if(ind_radius < 0)
        ind_radius = minrad / 2;
    
    // precalcs
	double angle = scale_zero + knobstate * 2 * (M_PI - scale_zero);
	double add_angle = 90 * (M_PI / 180.);
    
    double x1 = sin(angle) * -ind_radius;
    double y1 = cos(angle) * ind_radius;
    double x2 = sin(angle) * -(ind_radius - ind_length);
    double y2 = cos(angle) * (ind_radius - ind_length);
    
    // dashed ring
	if (ring_led_size) {
        double dashes[] = {double(ring_led_size), double(ring_led_distance)};
        cairo_set_dash(cr, dashes, sizeof(dashes)/sizeof(dashes[0]), 0);
    }
    
    // draw background
    if (scale_zero < 320 * (M_PI/180)) {
        gdk_cairo_set_source_rgba(cr, &color_);
        cairo_set_line_width(cr, ring_width);
        cairo_arc (cr, x_center + x0, y_center + y0, ring_radius,
            add_angle + scale_zero, add_angle + 320 * (M_PI/180));
        cairo_stroke(cr);
    }
    
    // draw foreground
    if (scale_zero < angle) {
        gdk_cairo_set_source_rgba(cr, &color);
        cairo_arc (cr, x_center + x0, y_center + y0, ring_radius,
            add_angle + scale_zero, add_angle + angle);
        if (ring_width >= 3) {
            cairo_stroke_preserve(cr);
            
            cairo_pattern_t * pat1 = cairo_pattern_create_radial(
                x0 + x_center, y0 + y_center, ring_radius - ring_width / 2., 
                x0 + x_center, y0 + y_center, ring_radius + ring_width / 2.);
            cairo_pattern_add_color_stop_rgba(pat1, 0.0, 0., 0., 0., 0.6);
            cairo_pattern_add_color_stop_rgba(pat1, 0.5, 1., 1., 1., 0.4);
            cairo_pattern_add_color_stop_rgba(pat1, 1.0, 0., 0., 0., 0.6);
            cairo_set_source(cr, pat1);
            cairo_set_operator(cr, CAIRO_OPERATOR_SOFT_LIGHT);
            cairo_stroke(cr);
            cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
            cairo_pattern_destroy(pat1);
        } else {
            cairo_stroke(cr);
        }
    }    
    cairo_set_dash(cr, NULL, 0, 0);
    
    // draw indicator
    gdk_cairo_set_source_rgba(cr, &color);
    cairo_set_line_width(cr, ind_width);
    cairo_move_to(cr, x0 + x1 + x_center, y0 + y1 + y_center);
    cairo_line_to(cr, x0 + x2 + x_center, y0 + y2 + y_center);
    if (ring_width >= 3) {
        cairo_stroke_preserve(cr);
        
        cairo_pattern_t * pat2 = cairo_pattern_create_radial(
            x0 + x_center, y0 + y_center, ind_radius - ind_length, 
            x0 + x_center, y0 + y_center, ind_radius);
        cairo_pattern_add_color_stop_rgba(pat2, 0.0, 0., 0., 0., 0.6);
        cairo_pattern_add_color_stop_rgba(pat2, 0.5, 1., 1., 1., 0.4);
        cairo_pattern_add_color_stop_rgba(pat2, 1.0, 0., 0., 0., 0.6);
        cairo_set_source(cr, pat2);
        cairo_set_operator(cr, CAIRO_OPERATOR_SOFT_LIGHT);
        cairo_stroke(cr);
        cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
        cairo_pattern_destroy(pat2);
    } else {
        cairo_stroke(cr);
    }
}

void _gx_knob_expose(GtkWidget *widget, cairo_t *cr, GdkRectangle *image_rect, gdouble knobstate,
                     GdkPixbuf *knob_image, gint framecount, int has_focus)
{
	if (framecount > 1) {
        int findex;
		framecount--; // zero based index
		findex = (int)(framecount * knobstate);
		gdk_cairo_set_source_pixbuf(cr, knob_image, image_rect->x - (image_rect->width * findex), image_rect->y);
		cairo_rectangle(cr, image_rect->x, image_rect->y, image_rect->width, image_rect->height);
		cairo_fill(cr);
	}
	else {
		if (gtk_widget_has_focus(widget)) {
			gtk_render_focus(gtk_widget_get_style_context(widget), cr,
							 image_rect->x, image_rect->y, image_rect->width, image_rect->height);
		}
		cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 
                               image_rect->width, image_rect->height);
        cairo_t *cr_s = cairo_create(surface);
		gdk_cairo_set_source_pixbuf(cr_s, knob_image, 0, 0);
		cairo_rectangle(cr_s, 0, 0, image_rect->width, image_rect->height);
		cairo_fill(cr_s);
        // shtuff
		_gx_knob_draw_shtuff(widget, cr_s, image_rect, knobstate);
		cairo_set_source_surface(cr, surface, image_rect->x, image_rect->y);
        cairo_paint(cr);
        cairo_surface_destroy (surface);
		cairo_destroy(cr_s);
	}

	
}

static void get_image_dimensions(GtkWidget *widget, GdkPixbuf *pb,
				 GdkRectangle *rect, gint *frame_count)
{
	gtk_widget_style_get(widget, "framecount", frame_count, NULL);

	rect->width  = gdk_pixbuf_get_width(pb);
	rect->height = gdk_pixbuf_get_height(pb);
	
	if (*frame_count >1) {
		rect->width = (rect->width / *frame_count);
	}
	if (*frame_count == 0) {// rc directs to assume square frames
		*frame_count = rect->width / rect->height;
		if (rect->width > (2*rect->height)) {
			rect->width = rect->height;
		}
	}
}

gboolean _gx_knob_pointer_event(GtkWidget *widget, gdouble x, gdouble y, const gchar *icon,
                                gboolean drag, int state, int button, GdkEventButton *event)
{
	int fcount;
	bool finemode = ((state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK)) != 0);
	GdkRectangle image_rect, value_rect;
	
	GxKnob *knob = GX_KNOB(widget);
	GdkPixbuf *pb = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
											 icon, -1,
											 GTK_ICON_LOOKUP_GENERIC_FALLBACK, nullptr);
	GxKnobPrivate *priv = knob->priv;
	
	get_image_dimensions (widget, pb, &image_rect, &fcount);
	
	g_clear_object(&pb);
	_gx_regler_get_positions(GX_REGLER(widget), &image_rect, &value_rect, false);
	if (!drag) {
		if (_gx_regler_check_display_popup(GX_REGLER(widget), &image_rect, &value_rect, event)) {
			return FALSE;
		}
	}
	static double last_y = 2e20;
	GtkAdjustment *adj = gtk_range_get_adjustment(GTK_RANGE(widget));
	gdouble lower = gtk_adjustment_get_lower(adj);
	gdouble upper = gtk_adjustment_get_upper(adj);
	double radius =	 min(image_rect.width, image_rect.height) / 2;
	double posx = radius - x + image_rect.x; // x axis right -> left
	double posy = radius - y + image_rect.y; // y axis top -> bottom
	double value;
	if (!drag) {
		if (event && event->type == GDK_2BUTTON_PRESS) {
			last_y = 2e20;
		} else {
			last_y = posy;
			return TRUE;
		}
	}
	if (last_y < 1e20) { // in drag started in linear mode
		const double scaling = 0.005;
		double scal = (finemode ? scaling*0.1 : scaling);
		value = (posy - last_y) * scal;
		last_y = posy;
		gdouble adj_value = gtk_adjustment_get_value(adj);
		gtk_range_set_value(GTK_RANGE(widget), adj_value + value * (upper - lower));
		return TRUE;
	}

	double angle = atan2(-posx, posy) + M_PI; // clockwise, zero at 6 o'clock, 0 .. 2*M_PI
	if (drag) {
		// block "forbidden zone" and direct moves between quadrant 1 and 4
		int quadrant = 1 + (int)(angle/M_PI_2);
		if (priv->last_quadrant == 1 && (quadrant == 3 || quadrant == 4)) {
			angle = scale_zero;
		} else if (priv->last_quadrant == 4 && (quadrant == 1 || quadrant == 2)) {
			angle = 2*M_PI - scale_zero;
		} else {
			if (angle < scale_zero) {
				angle = scale_zero;
			} else if (angle > 2*M_PI - scale_zero) {
				angle = 2*M_PI - scale_zero;
			}
			priv->last_quadrant = quadrant;
		}
	} else {
		if (angle < scale_zero) {
			angle = scale_zero;
		} else if (angle > 2*M_PI - scale_zero) {
			angle = 2*M_PI - scale_zero;
		}
		priv->last_quadrant = 0;
	}
	angle = (angle - scale_zero) / (2 * (M_PI-scale_zero)); // normalize to 0..1
	gtk_range_set_value(GTK_RANGE(widget), lower + angle * (upper - lower));
	return TRUE;
}

static gboolean gx_knob_pointer_motion(GtkWidget *widget, GdkEventMotion *event)
{
	g_assert(GX_IS_KNOB(widget));
	if (!gtk_widget_has_grab(widget)) {
		return FALSE;
	}
	gdk_event_request_motions (event);
	_gx_knob_pointer_event(widget, event->x, event->y, get_stock_id(widget), TRUE, event->state, 0, NULL);
	return FALSE;
}

static gboolean gx_knob_enter_in (GtkWidget *widget, GdkEventCrossing *event)
{
	g_assert(GX_IS_KNOB(widget));
	if (gtk_widget_has_grab(widget) || gtk_widget_has_focus(widget)== TRUE) {
		return TRUE;
	}
	gint fcount;
	gtk_widget_style_get(widget, "framecount", &fcount, NULL);
	if (fcount == -1) {
		gtk_widget_queue_draw(widget);
	}
	return TRUE;
}

static gboolean gx_knob_leave_out (GtkWidget *widget, GdkEventCrossing *event)
{
	g_assert(GX_IS_KNOB(widget));
	if (gtk_widget_has_grab(widget) || gtk_widget_has_focus(widget)== TRUE) {
		return TRUE;
	}
	gint fcount;
	gtk_widget_style_get(widget, "framecount", &fcount, NULL);
	if (fcount == -1) {
		gtk_widget_queue_draw(widget);
	}
	return TRUE;
}

static void gx_knob_get_preferred_width(GtkWidget *widget, gint *min_width, gint *natural_width)
{
	gint width, height;
	gx_knob_size_request(widget, &width, &height);

	if (min_width) {
		*min_width = width;
	}
	if (natural_width) {
		*natural_width = width;
	}
}

static void gx_knob_get_preferred_height(GtkWidget *widget, gint *min_height, gint *natural_height)
{
	gint width, height;
	gx_knob_size_request(widget, &width, &height);

	if (min_height) {
		*min_height = height;
	}
	if (natural_height) {
		*natural_height = height;
	}
}

static void gx_knob_size_request (GtkWidget *widget, gint *width, gint *height)
{
	g_assert(GX_IS_KNOB(widget));
	GdkPixbuf *pb = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
											 get_stock_id(widget), -1,
											 GTK_ICON_LOOKUP_GENERIC_FALLBACK, nullptr);
	if (GDK_IS_PIXBUF (pb)) {
		gint fcount;
		GdkRectangle rect;
		get_image_dimensions (widget, pb, &rect, &fcount);
		*width = rect.width;
		*height = rect.height;
		_gx_regler_calc_size_request(GX_REGLER(widget), width, height, TRUE);
		g_clear_object(&pb);
	}
}

static gboolean gx_knob_draw(GtkWidget *widget, cairo_t *cr)
{
	g_assert(GX_IS_KNOB(widget));
	GdkRectangle image_rect, value_rect;
	GdkPixbuf *pb = gtk_icon_theme_load_icon(gtk_icon_theme_get_default(),
											 get_stock_id(widget), -1,
											 GTK_ICON_LOOKUP_GENERIC_FALLBACK, nullptr);
	if (GDK_IS_PIXBUF (pb)) {
		gint fcount;
		get_image_dimensions (widget, pb, &image_rect, &fcount);
		gdouble knobstate = _gx_regler_get_step_pos(GX_REGLER(widget), 1);
		_gx_regler_get_positions(GX_REGLER(widget), &image_rect, &value_rect, false);
		_gx_knob_expose(widget, cr, &image_rect, knobstate, pb, fcount, gtk_widget_has_focus(widget));
		_gx_regler_display_value(GX_REGLER(widget), cr, &value_rect);
		g_clear_object(&pb);
	}
	return FALSE;
}

static gboolean gx_knob_button_press (GtkWidget *widget, GdkEventButton *event)
{
	g_assert(GX_IS_KNOB(widget));
	if (event->button != 1 && event->button != 3) {
		return FALSE;
	}
	gtk_widget_grab_focus(widget);
	if (_gx_knob_pointer_event(widget, event->x, event->y, get_stock_id(widget), FALSE, event->state, event->button, event)) {
		gtk_grab_add(widget);
	}
	return FALSE;
}
