
#include "WINGsP.h"

#undef STRICT_NEXT_BEHAVIOUR

typedef struct W_Slider {
	W_Class widgetClass;
	WMView *view;

	int minValue;
	int maxValue;

	int value;

	Pixmap knobPixmap;
	WMPixmap *backPixmap;

	WMAction *action;
	void *clientData;

	int knobThickness;

	struct {
		unsigned int continuous:1;

		unsigned int vertical:1;
		unsigned int dragging:1;
	} flags;

} Slider;

static void didResizeSlider();

W_ViewDelegate _SliderViewDelegate = {
	NULL,
	NULL,
	didResizeSlider,
	NULL,
	NULL
};

static void destroySlider(Slider * sPtr);
static void paintSlider(Slider * sPtr);

static void handleEvents(XEvent * event, void *data);
static void handleActionEvents(XEvent * event, void *data);

static void makeKnobPixmap(Slider * sPtr);

static void realizeObserver(void *self, WMNotification * not)
{
	makeKnobPixmap(self);
}

WMSlider *WMCreateSlider(WMWidget * parent)
{
	Slider *sPtr;

	sPtr = wmalloc(sizeof(Slider));
	memset(sPtr, 0, sizeof(Slider));

	sPtr->widgetClass = WC_Slider;

	sPtr->view = W_CreateView(W_VIEW(parent));
	if (!sPtr->view) {
		wfree(sPtr);
		return NULL;
	}
	sPtr->view->self = sPtr;

	sPtr->view->delegate = &_SliderViewDelegate;

	WMCreateEventHandler(sPtr->view, ExposureMask | StructureNotifyMask, handleEvents, sPtr);

	WMCreateEventHandler(sPtr->view, ButtonPressMask | ButtonReleaseMask
			     | EnterWindowMask | LeaveWindowMask | ButtonMotionMask, handleActionEvents, sPtr);

	W_ResizeView(sPtr->view, 100, 16);
	sPtr->flags.vertical = 0;
	sPtr->minValue = 0;
	sPtr->maxValue = 100;
	sPtr->value = 50;

	sPtr->knobThickness = 20;

	sPtr->flags.continuous = 1;

	WMAddNotificationObserver(realizeObserver, sPtr, WMViewRealizedNotification, sPtr->view);

	return sPtr;
}

void WMSetSliderImage(WMSlider * sPtr, WMPixmap * pixmap)
{
	if (sPtr->backPixmap)
		WMReleasePixmap(sPtr->backPixmap);

	sPtr->backPixmap = WMRetainPixmap(pixmap);

	if (sPtr->view->flags.mapped) {
		paintSlider(sPtr);
	}
}

void WMSetSliderKnobThickness(WMSlider * sPtr, int thickness)
{
	assert(thickness > 0);

	sPtr->knobThickness = thickness;

	if (sPtr->knobPixmap) {
		makeKnobPixmap(sPtr);
	}

	if (sPtr->view->flags.mapped) {
		paintSlider(sPtr);
	}
}

int WMGetSliderMinValue(WMSlider * slider)
{
	CHECK_CLASS(slider, WC_Slider);

	return slider->minValue;
}

int WMGetSliderMaxValue(WMSlider * slider)
{
	CHECK_CLASS(slider, WC_Slider);

	return slider->maxValue;
}

int WMGetSliderValue(WMSlider * slider)
{
	CHECK_CLASS(slider, WC_Slider);

	return slider->value;
}

void WMSetSliderMinValue(WMSlider * slider, int value)
{
	CHECK_CLASS(slider, WC_Slider);

	slider->minValue = value;
	if (slider->value < value) {
		slider->value = value;
		if (slider->view->flags.mapped)
			paintSlider(slider);
	}
}

void WMSetSliderMaxValue(WMSlider * slider, int value)
{
	CHECK_CLASS(slider, WC_Slider);

	slider->maxValue = value;
	if (slider->value > value) {
		slider->value = value;
		if (slider->view->flags.mapped)
			paintSlider(slider);
	}
}

void WMSetSliderValue(WMSlider * slider, int value)
{
	CHECK_CLASS(slider, WC_Slider);

	if (value < slider->minValue)
		slider->value = slider->minValue;
	else if (value > slider->maxValue)
		slider->value = slider->maxValue;
	else
		slider->value = value;

	if (slider->view->flags.mapped)
		paintSlider(slider);
}

void WMSetSliderContinuous(WMSlider * slider, Bool flag)
{
	CHECK_CLASS(slider, WC_Slider);

	slider->flags.continuous = ((flag == 0) ? 0 : 1);
}

void WMSetSliderAction(WMSlider * slider, WMAction * action, void *data)
{
	CHECK_CLASS(slider, WC_Slider);

	slider->action = action;
	slider->clientData = data;
}

static void makeKnobPixmap(Slider * sPtr)
{
	Pixmap pix;
	WMScreen *scr = sPtr->view->screen;
	int w, h;

	if (sPtr->flags.vertical) {
		w = sPtr->view->size.width - 2;
		h = sPtr->knobThickness;
	} else {
		w = sPtr->knobThickness;
		h = sPtr->view->size.height - 2;
	}

	pix = XCreatePixmap(scr->display, sPtr->view->window, w, h, scr->depth);
	XFillRectangle(scr->display, pix, WMColorGC(scr->gray), 0, 0, w, h);

	if (sPtr->knobThickness < 10) {
		W_DrawRelief(scr, pix, 0, 0, w, h, WRRaised);
	} else if (sPtr->flags.vertical) {
		XDrawLine(scr->display, pix, WMColorGC(scr->white), 0, 0, 0, h - 3);
		XDrawLine(scr->display, pix, WMColorGC(scr->white), 1, 0, 1, h - 3);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), w - 2, 1, w - 2, h / 2 - 2);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), w - 2, h / 2, w - 2, h - 2);

		XDrawLine(scr->display, pix, WMColorGC(scr->white), 0, 0, w - 2, 0);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), 1, h / 2 - 2, w - 3, h / 2 - 2);
		XDrawLine(scr->display, pix, WMColorGC(scr->white), 0, h / 2 - 1, w - 3, h / 2 - 1);

		XDrawLine(scr->display, pix, WMColorGC(scr->black), w - 1, 0, w - 1, h - 2);

		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), 0, h - 3, w - 2, h - 3);
		XDrawLine(scr->display, pix, WMColorGC(scr->black), 0, h - 2, w - 1, h - 2);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), 0, h - 1, w - 1, h - 1);
	} else {
		XDrawLine(scr->display, pix, WMColorGC(scr->white), 0, 0, w - 3, 0);

		XDrawLine(scr->display, pix, WMColorGC(scr->white), 0, 0, 0, h - 2);

		XDrawLine(scr->display, pix, WMColorGC(scr->white), 1, 0, 1, h - 3);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), w / 2 - 2, 1, w / 2 - 2, h - 3);
		XDrawLine(scr->display, pix, WMColorGC(scr->white), w / 2 - 1, 0, w / 2 - 1, h - 3);

		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), w - 3, 0, w - 3, h - 2);
		XDrawLine(scr->display, pix, WMColorGC(scr->black), w - 2, 0, w - 2, h - 2);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), w - 1, 0, w - 1, h - 1);

		XDrawLine(scr->display, pix, WMColorGC(scr->black), 1, h - 1, w / 2 + 1, h - 1);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), 1, h - 2, w / 2 - 2, h - 2);
		XDrawLine(scr->display, pix, WMColorGC(scr->darkGray), w / 2, h - 2, w - 3, h - 2);

		XDrawLine(scr->display, pix, WMColorGC(scr->black), 0, h - 1, w - 2, h - 1);
	}

	if (sPtr->knobPixmap)
		XFreePixmap(scr->display, sPtr->knobPixmap);
	sPtr->knobPixmap = pix;
}

static void didResizeSlider(W_ViewDelegate * self, WMView * view)
{
	Slider *sPtr = (Slider *) view->self;
	int width = sPtr->view->size.width;
	int height = sPtr->view->size.height;

	assert(width > 0);
	assert(height > 0);

	if (width > height) {
		if (sPtr->flags.vertical) {
			sPtr->flags.vertical = 0;
			if (sPtr->view->flags.realized)
				makeKnobPixmap(sPtr);
		}
	} else {
		if (!sPtr->flags.vertical) {
			sPtr->flags.vertical = 1;
			if (sPtr->view->flags.realized)
				makeKnobPixmap(sPtr);
		}
	}
}

static void paintSlider(Slider * sPtr)
{
	W_Screen *scr = sPtr->view->screen;
	GC bgc;
	GC wgc;
	GC lgc;
	WMSize size = sPtr->view->size;
	int pos;
	Pixmap buffer;

#define MINV sPtr->minValue
#define MAXV sPtr->maxValue
#define POSV sPtr->value

	bgc = WMColorGC(scr->black);
	wgc = WMColorGC(scr->white);
	lgc = WMColorGC(scr->gray);

	buffer = XCreatePixmap(scr->display, sPtr->view->window, size.width, size.height, scr->depth);

	if (sPtr->backPixmap) {
		WMSize size = WMGetPixmapSize(sPtr->backPixmap);

		XCopyArea(scr->display, WMGetPixmapXID(sPtr->backPixmap),
			  buffer, scr->copyGC, 0, 0, size.width, size.height, 1, 1);
	} else {
		XFillRectangle(scr->display, buffer, lgc, 0, 0, size.width, size.height);
		XFillRectangle(scr->display, buffer, scr->stippleGC, 0, 0, size.width, size.height);
	}

	if (sPtr->flags.vertical) {
		pos = (size.height - 2 - sPtr->knobThickness) * (POSV - MINV) / (MAXV - MINV) + 1;
		/* draw knob */
		XCopyArea(scr->display, sPtr->knobPixmap, buffer,
			  scr->copyGC, 0, 0, size.width - 2, sPtr->knobThickness, 1, pos);
	} else {
		pos = (size.width - 2 - sPtr->knobThickness) * (POSV - MINV) / (MAXV - MINV) + 1;
		/* draw knob */
		XCopyArea(scr->display, sPtr->knobPixmap, buffer,
			  scr->copyGC, 0, 0, sPtr->knobThickness, size.height, pos, 1);
	}

	XDrawLine(scr->display, buffer, bgc, 0, 0, 0, size.height - 1);
	XDrawLine(scr->display, buffer, bgc, 0, 0, size.width, 0);

	XDrawLine(scr->display, buffer, wgc, size.width - 1, 0, size.width - 1, size.height - 1);
	XDrawLine(scr->display, buffer, wgc, 0, size.height - 1, size.width - 1, size.height - 1);

	XCopyArea(scr->display, buffer, sPtr->view->window, scr->copyGC, 0, 0, size.width, size.height, 0, 0);
	XFreePixmap(scr->display, buffer);
}

static void handleEvents(XEvent * event, void *data)
{
	Slider *sPtr = (Slider *) data;

	CHECK_CLASS(data, WC_Slider);

	switch (event->type) {
	case Expose:
		if (event->xexpose.count != 0)
			break;
		paintSlider(sPtr);
		break;

	case DestroyNotify:
		destroySlider(sPtr);
		break;

	}
}

#define DECR_PART	1
#define KNOB_PART	2
#define INCR_PART	3

static int getSliderPart(Slider * sPtr, int x, int y)
{
	int p;
	int pos;
	WMSize size = sPtr->view->size;

	if (sPtr->flags.vertical) {
		p = y;
		pos = (size.height - 2 - sPtr->knobThickness) * (POSV - MINV) / (MAXV - MINV);
		if (p < pos)
			return INCR_PART;
		if (p > pos + sPtr->knobThickness)
			return DECR_PART;
		return KNOB_PART;
	} else {
		p = x;
		pos = (size.width - 2 - sPtr->knobThickness) * (POSV - MINV) / (MAXV - MINV);
		if (p < pos)
			return DECR_PART;
		if (p > pos + sPtr->knobThickness)
			return INCR_PART;
		return KNOB_PART;
	}
}

static int valueForMousePoint(Slider * sPtr, int x, int y)
{
	WMSize size = sPtr->view->size;
	int f;

	if (sPtr->flags.vertical) {
		f = (y - sPtr->knobThickness / 2) * (MAXV - MINV)
		    / ((int)size.height - 2 - sPtr->knobThickness);
	} else {
		f = (x - sPtr->knobThickness / 2) * (MAXV - MINV)
		    / ((int)size.width - 2 - sPtr->knobThickness);
	}

	f += sPtr->minValue;
	if (f < sPtr->minValue)
		f = sPtr->minValue;
	else if (f > sPtr->maxValue)
		f = sPtr->maxValue;

	return f;
}

static void handleActionEvents(XEvent * event, void *data)
{
	WMSlider *sPtr = (Slider *) data;

	CHECK_CLASS(data, WC_Slider);

	switch (event->type) {
	case ButtonPress:
		if (event->xbutton.button == WINGsConfiguration.mouseWheelUp && !sPtr->flags.dragging) {
			/* Wheel up */
			if (sPtr->value + 1 <= sPtr->maxValue) {
				WMSetSliderValue(sPtr, sPtr->value + 1);
				if (sPtr->flags.continuous && sPtr->action) {
					(*sPtr->action) (sPtr, sPtr->clientData);
				}
			}
		} else if (event->xbutton.button == WINGsConfiguration.mouseWheelDown && !sPtr->flags.dragging) {
			/* Wheel down */
			if (sPtr->value - 1 >= sPtr->minValue) {
				WMSetSliderValue(sPtr, sPtr->value - 1);
				if (sPtr->flags.continuous && sPtr->action) {
					(*sPtr->action) (sPtr, sPtr->clientData);
				}
			}
		} else if (getSliderPart(sPtr, event->xbutton.x, event->xbutton.y)
			   == KNOB_PART)
			sPtr->flags.dragging = 1;
		else {
#ifdef STRICT_NEXT_BEHAVIOUR
			sPtr->flags.dragging = 1;

			sPtr->value = valueForMousePoint(sPtr, event->xmotion.x, event->xmotion.y);
			paintSlider(sPtr);
#else
			int tmp;

			if (event->xbutton.button == Button2) {
				sPtr->flags.dragging = 1;

				sPtr->value = valueForMousePoint(sPtr, event->xmotion.x, event->xmotion.y);
				paintSlider(sPtr);
			} else {
				tmp = valueForMousePoint(sPtr, event->xmotion.x, event->xmotion.y);
				if (tmp < sPtr->value)
					tmp = sPtr->value - 1;
				else
					tmp = sPtr->value + 1;
				WMSetSliderValue(sPtr, tmp);
			}
#endif

			if (sPtr->flags.continuous && sPtr->action) {
				(*sPtr->action) (sPtr, sPtr->clientData);
			}
		}
		break;

	case ButtonRelease:
		if (!sPtr->flags.continuous && sPtr->action) {
			(*sPtr->action) (sPtr, sPtr->clientData);
		}
		sPtr->flags.dragging = 0;
		break;

	case MotionNotify:
		if (sPtr->flags.dragging) {
			sPtr->value = valueForMousePoint(sPtr, event->xmotion.x, event->xmotion.y);
			paintSlider(sPtr);

			if (sPtr->flags.continuous && sPtr->action) {
				(*sPtr->action) (sPtr, sPtr->clientData);
			}
		}
		break;
	}
}

static void destroySlider(Slider * sPtr)
{
	if (sPtr->knobPixmap)
		XFreePixmap(sPtr->view->screen->display, sPtr->knobPixmap);

	if (sPtr->backPixmap)
		WMReleasePixmap(sPtr->backPixmap);

	WMRemoveNotificationObserver(sPtr);

	wfree(sPtr);
}
