/*
 * Kangaroo Punch MultiPlayer Game Server Mark II
 * Copyright (C) 2020-2021  Scott Duensing
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 *
 */


#include "updown.h"
#include "timer.h"


// 32 bit ranges from -2,147,483,648 to 2,147,483,647.
// Maximum character cells needed is 11 (10 digits + negative sign).
#define UPDOWN_MAX_DIGITS  11


// These are used in several places, so we made them global and just update as needed.  See updownSizesRecalculate.
static uint16_t _labelWidth = 0;
static uint16_t _valueWidth = 0;
static uint16_t _arrowWidth = 0;
static uint16_t _arrowStart = 0;
static uint16_t _halfFont   = 0;


static void updownDel(WidgetT **widget);
static void updownFocusEvent(WidgetT *widget, uint8_t focused);
static void updownMouseEvent(WidgetT *widget, MouseT *mouse, uint16_t x, uint16_t y, uint8_t event);
static void updownKeyboardEvent(WidgetT *widget, uint8_t ascii, uint8_t extended, uint8_t scancode, uint8_t shift, uint8_t control, uint8_t alt);
static void updownPaint(WidgetT *widget, uint8_t enabled, RectT pos);
static void updownSizesRecalculate(UpdownT *updown);
static void updownVisibleSet(UpdownT *updown);


static void updownDel(WidgetT **widget) {
	UpdownT *u = (UpdownT *)*widget;

	if (u->title) free(u->title);
}


static void updownFocusEvent(WidgetT *widget, uint8_t focused) {
	// Make sure cursor disappears when we lose focus.
	if (!focused) {
		GUI_SET_FLAG(widget, WIDGET_FLAG_DIRTY);
	}
}


WidgetT *updownInit(WidgetT *widget, int32_t min, int32_t max, int32_t step, char *title) {
	UpdownT *u = (UpdownT *)widget;

	u->base.delMethod           = updownDel;
	u->base.focusMethod         = updownFocusEvent;
	u->base.paintMethod         = updownPaint;
	u->base.keyboardEventMethod = updownKeyboardEvent;
	u->base.mouseEventMethod    = updownMouseEvent;
	u->maximum                  = INT32_MAX;
	u->minimum                  = INT32_MIN;
	u->title                    = NULL;
	u->value                    = 0;

	updownTitleSet(u, title);  // Needs to be set first so updownSizesRecalculate works.
	updownMinimumSet(u, min);
	updownMaximumSet(u, max);
	updownStepSet(u, step);

	// We need to blink a cursor.
	GUI_SET_FLAG(widget, WIDGET_FLAG_ALWAYS_PAINT);

	return widget;
}


static void updownKeyboardEvent(WidgetT *widget, uint8_t ascii, uint8_t extended, uint8_t scancode, uint8_t shift, uint8_t control, uint8_t alt) {

	UpdownT  *u = (UpdownT *)widget;
	int32_t   temp;
	static uint8_t willBeNegative = 0;

	(void)scancode;
	(void)shift;
	(void)control;
	(void)alt;

	if (extended) {

		switch (ascii) {
			case 71:  // HOME
				u->value = u->minimum;
				willBeNegative = 0;
				break;

			case 79:  // END
				u->value = u->maximum;
				willBeNegative = 0;
				break;

			case 73:  // PAGEUP
				u->value += u->step;
				if (u->value > u->maximum) u->value = u->maximum;
				willBeNegative = 0;
				break;

			case 81:  // PAGEDOWN
				u->value -= u->step;
				if (u->value < u->minimum) u->value = u->minimum;
				willBeNegative = 0;
				break;

			case 72:  // UP
				if (u->value < u->maximum) u->value++;
				willBeNegative = 0;
				break;

			case 80:  // DOWN
				if (u->value > u->minimum) u->value--;
				willBeNegative = 0;
				break;

			case 83:  // DELETE
				u->value = (int)(u->value * 0.1);
				willBeNegative = 0;
				break;

		} // switch

	} else { // extended

		switch (ascii) {
			case 8:  // BACKSPACE
				u->value = (int)(u->value * 0.1);
				willBeNegative = 0;
				break;

			case '-':  // Leading Minus
				if (u->value == 0) willBeNegative = 1;
				break;

			default:  // Number keys
				if (ascii >= '0'  && ascii <= '9') {
					temp = u->value * 10 + (ascii - '0');
					if (willBeNegative) u->value = -u->value;
					if (temp <= u->maximum && temp >= u->minimum) u->value = temp;
				}
				willBeNegative = 0;
				break;

		} // switch

	} // extended
}


void updownMaximumSet(UpdownT *updown, int32_t maximum) {
	if (maximum > updown->minimum) {
		updown->maximum = maximum;
		updownVisibleSet(updown);
	}
}


void updownMinimumSet(UpdownT *updown, int32_t minimum) {
	if (minimum < updown->maximum) {
		updown->minimum = minimum;
		updownVisibleSet(updown);
	}
}


static void updownMouseEvent(WidgetT *widget, MouseT *mouse, uint16_t x, uint16_t y, uint8_t event) {
	UpdownT  *u = (UpdownT *)widget;
	uint16_t  o;

	(void)x;
	(void)y;
	(void)mouse;

	if (event == MOUSE_EVENT_LEFT_UP) {
		updownSizesRecalculate(u);

		_arrowStart += 1 + _guiMetric[METRIC_UPDOWN_HORIZONTAL_PADDING]; // Left of up arrow
		o = 1 + _guiMetric[METRIC_UPDOWN_VERTICAL_PADDING];  // Top of arrows

		// Did they click the up arrow?
		if (x >= _arrowStart && x <= _arrowStart + fontHeightGet(_guiFont) && y >= o && y <= o + fontHeightGet(_guiFont)) {
			// Can we increment?
			if (u->value < u->maximum) {
				// Clamp.
				if (u->maximum - u->value < u->step) {
					u->value = u->maximum;
				} else {
					u->value += u->step;
				}
				GUI_SET_FLAG(widget, WIDGET_FLAG_DIRTY);
			}
		}

		// Did they click the down arrow?
		_arrowStart += fontHeightGet(_guiFont) + 1;  // Left of down arrow
		if (x >= _arrowStart && x <= _arrowStart + fontHeightGet(_guiFont) && y >= o && y <= o + fontHeightGet(_guiFont)) {
			// Can we decrement?
			if (u->value > u->minimum) {
				// Clamp.
				if (u->value - u->minimum < u->step) {
					u->value = u->minimum;
				} else {
					u->value -= u->step;
				}
				GUI_SET_FLAG(widget, WIDGET_FLAG_DIRTY);
			}
		}

	}
}


UpdownT *updownNew(uint16_t x, uint16_t y, int32_t min, int32_t max, int32_t step, char *title) {
	UpdownT  *updown = (UpdownT *)malloc(sizeof(UpdownT));
	WidgetT  *widget  = NULL;
	uint16_t  h       = fontHeightGet(_guiFont) + 4 + (_guiMetric[METRIC_UPDOWN_VERTICAL_PADDING] * 2);

	if (!updown) return NULL;

	// Width is set in SetVisible.
	widget = widgetInit(W(updown), MAGIC_UPDOWN, x, y, min, h, 0, 0, 0, 0);
	if (!widget) {
		free(updown);
		return NULL;
	}

	updown = (UpdownT *)updownInit((WidgetT *)updown, min, max, step, title);

	return updown;
}


static void updownPaint(WidgetT *widget, uint8_t enabled, RectT pos) {
	UpdownT  *u          = (UpdownT *)widget;
	uint8_t   i;
	uint16_t  textX;
	uint16_t  textY;
	uint16_t  o;
	ColorT    color;
	char      draw[UPDOWN_MAX_DIGITS + 1];
	char      cursor[2] = { 0xb1, 0 };

	if (GUI_GET_FLAG(widget, WIDGET_FLAG_DIRTY) || guiFocusGet() == widget) {
		updownSizesRecalculate(u);

		_arrowStart += pos.x; // This method expects the start in screen space, not widget space.

		// Draw title.
		fontRender(_guiFont, u->title, _guiColor[COLOR_UPDOWN_TEXT],  _guiColor[COLOR_WINDOW_BACKGROUND], pos.x, pos.y + 2 + _guiMetric[METRIC_UPDOWN_VERTICAL_PADDING]);

		// Draw outline of text.
		surfaceHighlightFrameDraw( pos.x + _labelWidth,     pos.y,     pos.x + _labelWidth + _valueWidth + 2, pos.y + pos.h,     _guiColor[COLOR_UPDOWN_SHADOW], _guiColor[COLOR_UPDOWN_HIGHLIGHT]);
		surfaceRectangleDraw(      pos.x + _labelWidth + 1, pos.y + 1, pos.x + _labelWidth + _valueWidth + 1, pos.y + pos.h - 1, _guiColor[COLOR_WINDOW_BACKGROUND]);

		// Draw text background.
		surfaceRectangleFilledDraw(pos.x + _labelWidth + 2, pos.y + 2, pos.x + _labelWidth + _valueWidth,     pos.y + pos.h - 2, _guiColor[COLOR_UPDOWN_BACKGROUND]);

		// Draw arrows outline
		surfaceHighlightFrameDraw(_arrowStart, pos.y, _arrowStart + _arrowWidth + 1, pos.y + pos.h, _guiColor[COLOR_UPDOWN_SHADOW], _guiColor[COLOR_UPDOWN_HIGHLIGHT]);

		// Draw arrows background
		surfaceRectangleFilledDraw(_arrowStart + 1, pos.y + 1, _arrowStart + _arrowWidth, pos.y + pos.h - 1, _guiColor[COLOR_UPDOWN_ARROWS_BACKGROUND]);

		// Draw up arrow
		_arrowStart += _halfFont + 1 + _guiMetric[METRIC_UPDOWN_HORIZONTAL_PADDING];  // Center of up arrow
		o = pos.y + 1 + _guiMetric[METRIC_UPDOWN_VERTICAL_PADDING];  // Top of up arrow
		color = u->value < u->maximum ? _guiColor[COLOR_UPDOWN_ARROWS_ACTIVE] : _guiColor[COLOR_UPDOWN_ARROWS_INACTIVE];
		for (i=0; i<=fontHeightGet(_guiFont); i++) {
			surfaceLineDraw(_arrowStart - i * 0.5, o + i, _arrowStart + i * 0.5, o + i, color);
		}
		surfaceLineDraw(_arrowStart, o, _arrowStart + _halfFont, o + fontHeightGet(_guiFont), _guiColor[COLOR_UPDOWN_SHADOW]);
		surfaceLineDraw(_arrowStart - _halfFont, o + fontHeightGet(_guiFont), _arrowStart + _halfFont, o + fontHeightGet(_guiFont), _guiColor[COLOR_UPDOWN_SHADOW]);
		surfaceLineDraw(_arrowStart, o, _arrowStart - _halfFont, o + fontHeightGet(_guiFont), _guiColor[COLOR_UPDOWN_HIGHLIGHT]);

		// Draw down arrow
		_arrowStart += fontHeightGet(_guiFont) + 1;  // Center of down arrow
		o += fontHeightGet(_guiFont);  // Bottom of down arrow
		color = u->value > u->minimum ? _guiColor[COLOR_UPDOWN_ARROWS_ACTIVE] : _guiColor[COLOR_UPDOWN_ARROWS_INACTIVE];
		for (i=0; i<=fontHeightGet(_guiFont); i++) {
			surfaceLineDraw(_arrowStart - i * 0.5, o - i, _arrowStart + i * 0.5, o - i, color);
		}
		surfaceLineDraw(_arrowStart, o, _arrowStart + _halfFont, o - fontHeightGet(_guiFont), _guiColor[COLOR_UPDOWN_SHADOW]);
		surfaceLineDraw(_arrowStart - _halfFont, o - fontHeightGet(_guiFont), _arrowStart + _halfFont, o - fontHeightGet(_guiFont), _guiColor[COLOR_UPDOWN_HIGHLIGHT]);
		surfaceLineDraw(_arrowStart, o, _arrowStart - _halfFont, o - fontHeightGet(_guiFont), _guiColor[COLOR_UPDOWN_HIGHLIGHT]);

		// Where's the text display start?
		textX = pos.x + _labelWidth + 2 + _guiMetric[METRIC_UPDOWN_HORIZONTAL_PADDING];
		textY = pos.y + 2 + _guiMetric[METRIC_UPDOWN_VERTICAL_PADDING];

		// Draw value.
		snprintf(draw, UPDOWN_MAX_DIGITS, "%ld", (long)u->value);  // Weird typecasting prevents warnings in both DOS and Linux (32 and 64 bit)
		textX += (u->visible - strlen(draw)) * fontWidthGet(_guiFont);
		fontRender(_guiFont, draw, _guiColor[COLOR_UPDOWN_TEXT], _guiColor[COLOR_UPDOWN_BACKGROUND], textX, textY);

		// Draw cursor.
		if (guiFocusGet() == widget && guiTimerQuarterSecondOn()) {
			textX += (strlen(draw) - 1) * fontWidthGet(_guiFont);
			fontRender(_guiFont, cursor, _guiColor[COLOR_TEXTBOX_TEXT], _guiColor[COLOR_TEXTBOX_BACKGROUND], textX, textY);
		}

		GUI_CLEAR_FLAG(widget, WIDGET_FLAG_DIRTY);
	}
}


static void updownSizesRecalculate(UpdownT *updown) {

	// Label [  0] ^v
	// ====== - labelWidth, including space between label and text box.
	//        === - valueWidth (digits, padding, does not include border).
	//            === - arrowWidth (arrows, padding, 1px between arrows, and border).
	//             = - arrowStart (start of box surrounding arrows, including border).

	// Set global stuff that isn't actually part of the widget state.
	_halfFont   = fontHeightGet(_guiFont) * 0.5;
	_labelWidth = (strlen(updown->title) * fontWidthGet(_guiFont)) + _guiMetric[METRIC_UPDOWN_PADDING];
	_valueWidth = (updown->visible * fontWidthGet(_guiFont)) + (_guiMetric[METRIC_UPDOWN_HORIZONTAL_PADDING] * 2);
	_arrowWidth = (fontHeightGet(_guiFont) * 2) + 1 + 2 + (_guiMetric[METRIC_UPDOWN_HORIZONTAL_PADDING] * 2);  // Arrow width = font height
	_arrowStart = _labelWidth + _valueWidth + 2 + _guiMetric[METRIC_UPDOWN_ARROW_PADDING];
}


void updownStepSet(UpdownT *updown, int32_t step) {
	updown->step = step;
}


void updownTitleSet(UpdownT *updown, char *title) {
	if (updown->title) free(updown->title);
	updown->title = strdup(title);
	updownVisibleSet(updown);
	GUI_SET_FLAG((WidgetT *)updown, WIDGET_FLAG_DIRTY);
}


int32_t updownValueGet(UpdownT *updown) {
	return updown->value;
}


void updownValueSet(UpdownT *updown, int32_t value) {
	if (value >= updown->minimum && value <= updown->maximum) {
		updown->value = value;
		GUI_SET_FLAG((WidgetT *)updown, WIDGET_FLAG_DIRTY);
	}
}


static void updownVisibleSet(UpdownT *updown) {
	char   digits[UPDOWN_MAX_DIGITS + 1];
	int8_t maxWidth;
	int8_t minWidth;

	// Calculate how many characters we need to be able to display.
	maxWidth = snprintf(digits, UPDOWN_MAX_DIGITS, "%ld", (long)updown->maximum);  // Weird typecasting prevents warnings in both DOS and Linux (32 and 64 bit)
	minWidth = snprintf(digits, UPDOWN_MAX_DIGITS, "%ld", (long)updown->minimum);  // Weird typecasting prevents warnings in both DOS and Linux (32 and 64 bit)

	updown->visible = maxWidth > minWidth ? maxWidth : minWidth;

	// Calculate width of the widget.
	updownSizesRecalculate(updown);
	updown->base.pos.w = _arrowStart + _arrowWidth + 1;
}
