/*
 * 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 <SDL2/SDL.h>
#include <SDL2/SDL_image.h>

#define ENET_IMPLEMENTATION
#include "thirdparty/enet/include/enet.h"

#include "os.h"

#include "mouse.h"
#include "vesa.h"
#include "surface.h"
#include "comport.h"


#define TIMER_INTERVAL    (1000 / TICKS_PER_SECOND)

#define COM_BUFFER_SIZE       2048
#define MODEM_RESULT_OK       1
#define MODEM_RESULT_ERROR    2
#define MODEM_RESULT_NOTHING  3


static MouseT        _mouse                    = { 0 };
static SDL_Window   *_window                   = NULL;
static SDL_Renderer *_renderer                 = NULL;
static SDL_Surface  *_surface                  = NULL;
static SDL_Texture  *_texture                  = NULL;
static uint16_t      _width                    = 0;
static uint16_t      _height                   = 0;
static uint8_t       _windowScale              = 1;
static uint8_t       _alt                      = 0;
static uint8_t       _control                  = 0;
static uint8_t       _shift                    = 0;
static uint8_t       _ASCII                    = 0;
static uint8_t       _scanCode                 = 0;
static uint8_t       _extended                 = 0;
static uint8_t       _ASCIIKeep                = 0;
static uint8_t       _scanCodeKeep             = 0;
static uint8_t       _keyPressed               = 0;
static uint8_t       _debounce                 = 0;
static uint32_t      _timerTicks               = 0;
static SDL_TimerID   _timerID                  = { 0 };
static ENetHost     *_host                     = NULL;
static ENetPeer     *_peer                     = NULL;
static ENetAddress   _address                  = { 0 };
static uint8_t       _comPortOpen              = 0;
static uint8_t       _modemCommandMode         = 1;
static char          _buffer[COM_BUFFER_SIZE]  = { 0 };
static uint16_t      _bufferHead               = 0;
static uint16_t      _bufferTail               = 0;
static uint8_t       _connected                = 0;
static char          _command[COM_BUFFER_SIZE] = { 0 };
static uint32_t      _pixelFormat              = SDL_PIXELFORMAT_RGBA8888;
static uint8_t       _takeScreenshot           = 0;


static void     comAddToBuffer(char *data, uint16_t len);
//static void     comBufferShow(void);
static void     comModem(uint8_t c);
static void     processEvent(void);
static void     processNetworkEvent(void);
static uint32_t timerCallback(uint32_t interval, void *param);
static void     vbeScreenshot(void);


uint32_t rawclock(void) {
	return _timerTicks;
}


static void comAddToBuffer(char *data, uint16_t len) {
	uint16_t x;

	for (x=0; x<len; x++) {
		_buffer[_bufferHead++] = data[x];
		if (_bufferHead >= COM_BUFFER_SIZE) _bufferHead = 0;
	}
}


/*
static void comBufferShow(void) {
	uint16_t x = _bufferTail;

	logWrite("={");
	while (x != _bufferHead) {
		if (_buffer[x] != '\r' && _buffer[x] != '\n') logWrite("%c ", _buffer[x]);
		logWrite("%02x ", (uint8_t)_buffer[x++]);
		if (x >= COM_BUFFER_SIZE) x = 0;
	}
	logWrite("}\n");
}
*/


int comClose(int com) {
	// We only pretend to support one COM port.
	if (com != 0) return SER_ERR_UNKNOWN;
	if (!_comPortOpen) return SER_ERR_NOT_OPEN;

	if (_peer) {
		enet_peer_reset(_peer);
		_peer = NULL;
	}
	if (_host) {
		enet_host_destroy(_host);
		_host = NULL;
	}

	_comPortOpen = 0;

	return SER_SUCCESS;
}


static void comModem(uint8_t c) {
	uint16_t    x        = 0;
	uint8_t     result   = 0;
	char       *p        = NULL;

	if (c != 13) {

		x = strlen(_command);
		// Room in buffer?
		if (x > COM_BUFFER_SIZE - 1) {
			// No.  Clear buffer.
			_command[0] = 0;
		} else {
			// Add to command buffer.
			_command[x]     = toupper(c);
			_command[x + 1] = 0;
		}

	} else {

		// AT?
		if (_command[0] == 'A' || _command[1] == 'T') {
			// Process command string.
			x = 2;
			while (!result && _command[x]) {
				switch (_command[x]) {

					// Vendor Commands
					case '+':
						x++;
						// Select Socket Mode
						if (strncmp(&_command[x], "SOCK", 4) == 0) {
							result = MODEM_RESULT_OK;
						} else {
							result = MODEM_RESULT_ERROR;
						}
						break;


					// Dial
					case 'D':
						x++;
						if (_command[x] == 'T' || _command[x] == 'P') x++;
						// Find port, if any.
						_address.port = 23;
						if ((p = strstr(&_command[x], ":"))) {
							*p = 0;
							p++;
							_address.port = atoi(p);
						}
						if (enet_address_set_host(&_address, &_command[x]) < 0) {
							result = MODEM_RESULT_ERROR;
						} else {
							_host = enet_host_create(NULL, 1, 1, 0, 0);
							if (!_host) {
								result = MODEM_RESULT_ERROR;
							} else {
								_peer = enet_host_connect(_host, &_address, 1, 0);
								if (!_peer) {
									enet_host_destroy(_host);
									_host = NULL;
									result = MODEM_RESULT_ERROR;
								} else {
									result = MODEM_RESULT_OK;
								}
							}
						}
						_modemCommandMode = 0;
						break;

					// Dunno.
					default:
						result = MODEM_RESULT_ERROR;
						break;
				}
			}
		} else { // AT
			// Not an AT command.  Just gibberish sent to the modem.
			result = MODEM_RESULT_NOTHING;
		}

		// Send result.
		switch (result) {
			case MODEM_RESULT_OK:
				snprintf(_command, COM_BUFFER_SIZE, "OK%c", 13);
				comAddToBuffer(_command, strlen(_command));
				break;

			case MODEM_RESULT_ERROR:
				snprintf(_command, COM_BUFFER_SIZE, "ERROR%c", 13);
				comAddToBuffer(_command, strlen(_command));
				break;

			case MODEM_RESULT_NOTHING:
				// Nothing.
				break;
		}

		// Clear buffer.
		_command[0] = 0;
	}
}


int comOpen(int com, long bps, int dataBits, char parity, int stopBits, int handshaking) {
	(void)bps;
	(void)dataBits;
	(void)parity;
	(void)stopBits;
	(void)handshaking;

	// We only pretend to support one COM port.
	if (com != 0) return SER_ERR_UNKNOWN;
	if (_comPortOpen) return SER_ERR_ALREADY_OPEN;

	_comPortOpen = 1;
	_bufferHead  = 0;
	_bufferTail  = 0;

	// We really shouldn't do this, but enet is being stupid about not sending a DISCONNECT_TIMEOUT when failing to connect.
	_modemCommandMode = 1;

	return SER_SUCCESS;
}


int comRead(int com, char *data, int len) {
	uint16_t x = 0;

	// We only pretend to support one COM port.
	if (com != 0) return SER_ERR_UNKNOWN;
	if (!_comPortOpen) return SER_ERR_NOT_OPEN;

	// Is the buffer empty?
	if (_bufferHead == _bufferTail) {
		return 0;
	}

	// Fill buffer.
	while (x < len && _bufferHead != _bufferTail) {
		data[x++] = _buffer[_bufferTail++];
		if (_bufferTail >= COM_BUFFER_SIZE) _bufferTail = 0;
	}

	return x;
}


int comWrite(int com, const char *data, int len) {
	uint16_t    x      = 0;
	ENetPacket *packet = NULL;

	// We only pretend to support one COM port.
	if (com != 0) return SER_ERR_UNKNOWN;
	if (!_comPortOpen) return SER_ERR_NOT_OPEN;

	while (_modemCommandMode && x < len) {
		// Echo to terminal.
		comAddToBuffer((char *)&data[x], 1);
		// Send to modem command mode.
		comModem(data[x++]);
	}

	if (!_modemCommandMode && _connected && len - x > 0) {
		packet = enet_packet_create(&data[x], len - x, ENET_PACKET_FLAG_RELIABLE);
		if (!packet) return SER_ERR_UNKNOWN;
		enet_peer_send(_peer, 0, packet);
	}

	return SER_SUCCESS;
}


uint8_t keyAltGet(void) {
	return _alt;
}


uint8_t keyASCIIGet(void) {
	return _ASCIIKeep;
}


uint8_t keyControlGet(void) {
	return _control;
}


uint8_t keyExtendedGet(void) {
	return _extended;
}


uint8_t keyHit(void) {
	uint8_t result = 0;

	processEvent();

	result = _keyPressed;
	if (result) {
		// Default mapping.
		_extended     = 0;
		_ASCIIKeep    = _ASCII;
		_scanCodeKeep = _scanCode;
		_keyPressed   = 0;
		// Fix mappings to match DOS bioskey() call.
		if (_scanCodeKeep == SDL_SCANCODE_ESCAPE)    _ASCIIKeep = 27;
		if (_scanCodeKeep == SDL_SCANCODE_BACKSPACE) _ASCIIKeep = 8;
		if (_scanCodeKeep == SDL_SCANCODE_HOME)      { _extended = 1;  _ASCIIKeep = 71; }
		if (_scanCodeKeep == SDL_SCANCODE_UP)        { _extended = 1;  _ASCIIKeep = 72; }
		if (_scanCodeKeep == SDL_SCANCODE_PAGEUP)    { _extended = 1;  _ASCIIKeep = 73; }
		if (_scanCodeKeep == SDL_SCANCODE_LEFT)      { _extended = 1;  _ASCIIKeep = 75; }
		if (_scanCodeKeep == SDL_SCANCODE_RIGHT)     { _extended = 1;  _ASCIIKeep = 77; }
		if (_scanCodeKeep == SDL_SCANCODE_END)       { _extended = 1;  _ASCIIKeep = 79; }
		if (_scanCodeKeep == SDL_SCANCODE_DOWN)      { _extended = 1;  _ASCIIKeep = 80; }
		if (_scanCodeKeep == SDL_SCANCODE_PAGEDOWN)  { _extended = 1;  _ASCIIKeep = 81; }
		if (_scanCodeKeep == SDL_SCANCODE_DELETE)    { _extended = 1;  _ASCIIKeep = 83; }
	}

	return result;
}


uint8_t keyScanCodeGet(void) {
	return _scanCode;
}


uint8_t keyShiftGet(void) {
	return _shift;
}


void keyShutdown(void) {
	SDL_StopTextInput();
}


void keyStartup(void) {
	SDL_StartTextInput();
}


void linuxOsShutdown(void) {
	// Stop timer.
	SDL_RemoveTimer(_timerID);

	// Stop network.
	enet_deinitialize();

	SDL_Quit();
}


void linuxOsStartup(void) {
	SDL_Init(SDL_INIT_EVERYTHING);

	// And networking/serial startup.
	enet_initialize();

	// We do timer startup here, too.
	_timerID = SDL_AddTimer(TIMER_INTERVAL, timerCallback, NULL);
}


void mouseShutdown(void) {
	// Eh, don't care.
}


int16_t mouseStartup(void) {
	_mouse.active              = 1;
	_mouse.buttonCount         = 3;
	_mouse.buttonLeft          = 0;
	_mouse.buttonRight         = 0;
	_mouse.buttonMiddle        = 0;
	_mouse.buttonLeftWasDown   = 0;
	_mouse.buttonRightWasDown  = 0;
	_mouse.buttonMiddleWasDown = 0;
	_mouse.x                   = 0;
	_mouse.y                   = 0;
	_mouse.w                   = vbeDisplayWidthGet();
	_mouse.h                   = vbeDisplayHeightGet();

	return _mouse.active;
}


MouseT *mouseRead(void) {
	_mouse.buttonLeftWasDown   = _mouse.buttonLeft;
	_mouse.buttonRightWasDown  = _mouse.buttonRight;
	_mouse.buttonMiddleWasDown = _mouse.buttonMiddle;

	processEvent();

	return &_mouse;
}


static void processEvent(void) {
	SDL_Event e;

	while (SDL_PollEvent(&e)) {
		switch (e.type) {
			case SDL_MOUSEMOTION:
				_mouse.x = e.motion.x;
				_mouse.y = e.motion.y;
				break;

			case SDL_MOUSEBUTTONUP:
				if (e.button.button == SDL_BUTTON_LEFT)   _mouse.buttonLeft   = 0;
				if (e.button.button == SDL_BUTTON_RIGHT)  _mouse.buttonRight  = 0;
				if (e.button.button == SDL_BUTTON_MIDDLE) _mouse.buttonMiddle = 0;
				break;

			case SDL_MOUSEBUTTONDOWN:
				if (e.button.button == SDL_BUTTON_LEFT)   _mouse.buttonLeft   = 1;
				if (e.button.button == SDL_BUTTON_RIGHT)  _mouse.buttonRight  = 1;
				if (e.button.button == SDL_BUTTON_MIDDLE) _mouse.buttonMiddle = 1;
				break;

			case SDL_TEXTINPUT:
				_ASCII = e.text.text[0];  //***TODO*** This is horrifically wrong.
				break;

			case SDL_KEYUP:
				_scanCode   = 0;
				_ASCII      = 0;
				_keyPressed = 0;
				_debounce   = 0;
				break;

			case SDL_KEYDOWN:
				if (_debounce == 0) {
					_scanCode   = e.key.keysym.scancode;
					_shift      = ((SDL_GetModState() & KMOD_SHIFT) != 0);
					_alt        = ((SDL_GetModState() & KMOD_ALT)   != 0);
					_control    = ((SDL_GetModState() & KMOD_CTRL)  != 0);
					if (e.key.keysym.scancode != 0) {
						// Not a meta key
						_keyPressed = 1;
						_debounce   = 1;
					}
					// Screenshot?
					if (e.key.keysym.scancode == SDL_SCANCODE_F10) _takeScreenshot = 1;
				}
				break;
		}
	}
}


static void processNetworkEvent(void) {
	ENetEvent event = { 0 };

	if (_host) {
		while (enet_host_service(_host, &event, 0) > 0) {
			switch (event.type) {
				case ENET_EVENT_TYPE_CONNECT:
					_connected = 1;
					break;

				case ENET_EVENT_TYPE_RECEIVE:
					comAddToBuffer((char *)event.packet->data, event.packet->dataLength);
					break;

				case ENET_EVENT_TYPE_DISCONNECT:
				case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT:
					_connected = 0;
					_modemCommandMode = 1;
					comAddToBuffer("\13NO CARRIER\13", 12);
					break;

				default:
					break;
			}
		}
	}
}


static uint32_t timerCallback(uint32_t interval, void *param) {
	(void)param;

	processNetworkEvent();

	_timerTicks++;
	if (_timerTicks > TICKS_PER_DAY) {
		_timerTicks = 0;
	}

	return interval;
}


uint8_t vbeDisplayDepthBitsGet(void) {
	return 32;
}


uint8_t vbeDisplayDepthBytesGet(void) {
	return 4;
}


uint16_t vbeDisplayHeightGet(void) {
	return _height;
}


uint16_t vbeDisplayWidthGet(void) {
	return _width;
}


ColorT vbeColorMake(uint8_t red, uint8_t green, uint8_t blue) {
	// Pixels are 8:8:8:8 RGBA - SDL_PIXELFORMAT_RGBA8888
	return
		(red   << 24) |
		(green << 16) |
		(blue  << 8)  |
		255;
}


void vbePresent(void) {
	void      *pixels;
	int        temp;
	SurfaceT  *s      = surfaceOffScreenGet();

	SDL_LockTexture(_texture, NULL, &pixels, &temp);
	memcpy(pixels, s->buffer.bits8, s->bytes);
	SDL_UnlockTexture(_texture);
	SDL_RenderCopy(_renderer, _texture, NULL, NULL);
	SDL_RenderPresent(_renderer);

	// Throttle this to some sane frame rate.
	SDL_Delay(32);

	if (_takeScreenshot) {
		_takeScreenshot = 0;
		vbeScreenshot();
	}
}


int16_t vbeInfoShow(void) {
	// Eh, don't care.
	return 0;
}


static void vbeScreenshot(void) {
	int32_t      x             = 0;
	char         filename[16]  = { 0 };
	void        *pixels        = NULL;
	SDL_Surface *surface       = NULL;
	SDL_Surface *save          = NULL;
	FILE        *check         = NULL;

	while (x <= 999) {
		snprintf(filename, 16, "shot%03d.png", x);
		check = fopen(filename, "rb");
		if (check) {
			x++;
			fclose(check);
		} else {
			break;
		}
	}
	if (x > 999) utilDie("Seriously?  You have 1000 screenshots in this folder?  Remove some.");

	surface = SDL_GetWindowSurface(_window);
	pixels = (uint8_t *)malloc(surface->w * surface->h * surface->format->BytesPerPixel);
	SDL_RenderReadPixels(_renderer, &surface->clip_rect, surface->format->format, pixels, surface->w * surface->format->BytesPerPixel);
	save = SDL_CreateRGBSurfaceFrom(pixels, surface->w, surface->h, surface->format->BitsPerPixel, surface->w * surface->format->BytesPerPixel, surface->format->Rmask, surface->format->Gmask, surface->format->Bmask, surface->format->Amask);
	IMG_SavePNG(save, filename);
	SDL_FreeSurface(save);
	SDL_FreeSurface(surface);
	free(pixels);
}


int16_t vbeShutdown(void) {
	if (_texture) {
		SDL_DestroyTexture(_texture);
		_texture = NULL;
	}

	if (_renderer) {
		SDL_DestroyRenderer(_renderer);
		_renderer = NULL;
	}

	if (_window) {
		SDL_DestroyWindow(_window);
		_window = NULL;
	}

	return 0;
}


uint8_t vbeStartup(uint16_t xRes, uint16_t yRes, uint8_t bpp) {

	(void)bpp;

	_windowScale = 3;

	_window   = SDL_CreateWindow("GUI Test", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, xRes, yRes, SDL_WINDOW_ALLOW_HIGHDPI);
	_surface  = SDL_GetWindowSurface(_window);
	_renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED);
	_texture  = SDL_CreateTexture(_renderer, _pixelFormat, SDL_TEXTUREACCESS_STREAMING, xRes, yRes);

	SDL_RenderSetLogicalSize(_renderer, xRes, yRes);
	SDL_SetWindowSize(_window, xRes * _windowScale, yRes * _windowScale);

	_width  = xRes;
	_height = yRes;

	return 0;
}


void vbeVBlankWait(void) {
	// Eh, don't care.
}

