/* Copyright (c) 2013-2017 Jeffrey Pfau
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <mgba/internal/gba/sio/dolphin.h>

#include <mgba/internal/gba/gba.h>
#include <mgba/internal/gba/io.h>

#define BITS_PER_SECOND 115200 // This is wrong, but we need to maintain compat for the time being
#define CYCLES_PER_BIT (GBA_ARM7TDMI_FREQUENCY / BITS_PER_SECOND)
#define CLOCK_GRAIN (CYCLES_PER_BIT * 8)
#define CLOCK_WAIT 500

const uint16_t DOLPHIN_CLOCK_PORT = 49420;
const uint16_t DOLPHIN_DATA_PORT = 54970;

enum {
	WAIT_FOR_FIRST_CLOCK = 0,
	WAIT_FOR_CLOCK,
	WAIT_FOR_COMMAND,
};

static bool GBASIODolphinInit(struct GBASIODriver* driver);
static void GBASIODolphinReset(struct GBASIODriver* driver);
static void GBASIODolphinSetMode(struct GBASIODriver* driver, enum GBASIOMode mode);
static bool GBASIODolphinHandlesMode(struct GBASIODriver* driver, enum GBASIOMode mode);
static int GBASIODolphinConnectedDevices(struct GBASIODriver* driver);
static void GBASIODolphinProcessEvents(struct mTiming* timing, void* context, uint32_t cyclesLate);

static int32_t _processCommand(struct GBASIODolphin* dol, uint32_t cyclesLate);
static void _flush(struct GBASIODolphin* dol);

void GBASIODolphinCreate(struct GBASIODolphin* dol) {
	memset(&dol->d, 0, sizeof(dol->d));
	dol->d.init = GBASIODolphinInit;
	dol->d.reset = GBASIODolphinReset;
	dol->d.setMode = GBASIODolphinSetMode;
	dol->d.handlesMode = GBASIODolphinHandlesMode;
	dol->d.connectedDevices = GBASIODolphinConnectedDevices;
	dol->event.context = dol;
	dol->event.name = "GB SIO Lockstep";
	dol->event.callback = GBASIODolphinProcessEvents;
	dol->event.priority = 0x80;

	dol->data = INVALID_SOCKET;
	dol->clock = INVALID_SOCKET;
	dol->active = false;
}

void GBASIODolphinDestroy(struct GBASIODolphin* dol) {
	if (!SOCKET_FAILED(dol->data)) {
		SocketClose(dol->data);
		dol->data = INVALID_SOCKET;
	}

	if (!SOCKET_FAILED(dol->clock)) {
		SocketClose(dol->clock);
		dol->clock = INVALID_SOCKET;
	}
}

bool GBASIODolphinConnect(struct GBASIODolphin* dol, const struct Address* address, short dataPort, short clockPort) {
	if (!SOCKET_FAILED(dol->data)) {
		SocketClose(dol->data);
		dol->data = INVALID_SOCKET;
	}
	if (!dataPort) {
		dataPort = DOLPHIN_DATA_PORT;
	}

	if (!SOCKET_FAILED(dol->clock)) {
		SocketClose(dol->clock);
		dol->clock = INVALID_SOCKET;
	}
	if (!clockPort) {
		clockPort = DOLPHIN_CLOCK_PORT;
	}

	dol->data = SocketConnectTCP(dataPort, address);
	if (SOCKET_FAILED(dol->data)) {
		return false;
	}

	dol->clock = SocketConnectTCP(clockPort, address);
	if (SOCKET_FAILED(dol->clock)) {
		SocketClose(dol->data);
		dol->data = INVALID_SOCKET;
		return false;
	}

	SocketSetBlocking(dol->data, false);
	SocketSetBlocking(dol->clock, false);
	SocketSetTCPPush(dol->data, true);
	return true;
}

static bool GBASIODolphinInit(struct GBASIODriver* driver) {
	struct GBASIODolphin* dol = (struct GBASIODolphin*) driver;
	dol->clockSlice = 0;
	dol->state = WAIT_FOR_FIRST_CLOCK;
	GBASIODolphinReset(driver);
	return true;
}

static void GBASIODolphinReset(struct GBASIODriver* driver) {
	struct GBASIODolphin* dol = (struct GBASIODolphin*) driver;
	dol->active = false;
	_flush(dol);
	mTimingDeschedule(&dol->d.p->p->timing, &dol->event);
	mTimingSchedule(&dol->d.p->p->timing, &dol->event, 0);
}

static void GBASIODolphinSetMode(struct GBASIODriver* driver, enum GBASIOMode mode) {
	struct GBASIODolphin* dol = (struct GBASIODolphin*) driver;
	dol->active = mode == GBA_SIO_JOYBUS;
}

static bool GBASIODolphinHandlesMode(struct GBASIODriver* driver, enum GBASIOMode mode) {
	UNUSED(driver);
	return mode == GBA_SIO_JOYBUS;
}

static int GBASIODolphinConnectedDevices(struct GBASIODriver* driver) {
	UNUSED(driver);
	return 1;
}

void GBASIODolphinProcessEvents(struct mTiming* timing, void* context, uint32_t cyclesLate) {
	struct GBASIODolphin* dol = context;
	if (SOCKET_FAILED(dol->data)) {
		return;
	}

	dol->clockSlice -= cyclesLate;

	int32_t clockSlice;

	int32_t nextEvent = CLOCK_GRAIN;
	switch (dol->state) {
	case WAIT_FOR_FIRST_CLOCK:
		dol->clockSlice = 0;
		// Fall through
	case WAIT_FOR_CLOCK:
		if (dol->clockSlice < 0) {
			Socket r = dol->clock;
			SocketPoll(1, &r, 0, 0, CLOCK_WAIT);
		}
		if (SocketRecv(dol->clock, &clockSlice, 4) == 4) {
			clockSlice = ntohl(clockSlice);
			dol->clockSlice += clockSlice;
			dol->state = WAIT_FOR_COMMAND;
			nextEvent = 0;
		}
		// Fall through
	case WAIT_FOR_COMMAND:
		if (dol->clockSlice < -VIDEO_TOTAL_LENGTH * 4) {
			Socket r = dol->data;
			SocketPoll(1, &r, 0, 0, CLOCK_WAIT);
		}
		if (_processCommand(dol, cyclesLate) >= 0) {
			dol->state = WAIT_FOR_CLOCK;
			nextEvent = CLOCK_GRAIN;
		}
		break;
	}

	dol->clockSlice -= nextEvent;
	mTimingSchedule(timing, &dol->event, nextEvent);
}

void _flush(struct GBASIODolphin* dol) {
	uint8_t buffer[32];
	while (SocketRecv(dol->clock, buffer, sizeof(buffer)) == sizeof(buffer));
	while (SocketRecv(dol->data, buffer, sizeof(buffer)) == sizeof(buffer));
}

int32_t _processCommand(struct GBASIODolphin* dol, uint32_t cyclesLate) {
	// This does not include the stop bits due to compatibility reasons
	int bitsOnLine = 8;
	uint8_t buffer[6];
	int gotten = SocketRecv(dol->data, buffer, 1);
	if (gotten < 1) {
		return -1;
	}

	switch (buffer[0]) {
	case JOY_RESET:
	case JOY_POLL:
		bitsOnLine += 24;
		break;
	case JOY_RECV:
		gotten = SocketRecv(dol->data, &buffer[1], 4);
		if (gotten < 4) {
			return -1;
		}
		mLOG(GBA_SIO, DEBUG, "DOL recv: %02X%02X%02X%02X", buffer[1], buffer[2], buffer[3], buffer[4]);
		// Fall through
	case JOY_TRANS:
		bitsOnLine += 40;
		break;
	}

	if (!dol->active) {
		return 0;
	}

	int sent = GBASIOJOYSendCommand(&dol->d, buffer[0], &buffer[1]);
	SocketSend(dol->data, &buffer[1], sent);

	return bitsOnLine * CYCLES_PER_BIT - cyclesLate;
}

bool GBASIODolphinIsConnected(struct GBASIODolphin* dol) {
	return dol->data != INVALID_SOCKET;
}
