// SPDX-License-Identifier: LGPL-3.0-only

#include <rz_io.h>
#include <rz_lib.h>
#include <rz_util.h>
#include <rz_util/rz_print.h>

#if __UNIX__
#include <errno.h>
#include <fcntl.h>
#endif

#define USE_OWNTIMER 1
#if USE_OWNTIMER
#include "io_gprobe.h"
#else
#define Timersub timersub
#define Timeradd timeradd
#define Timercmp timercmp
#endif

#if __WINDOWS__
#include <cfgmgr32.h>
#include <setupapi.h>
#include <tchar.h>
#include <windows.h>
#else

#if __linux__ || __APPLE__ || __OpenBSD__ || __FreeBSD__ || __NetBSD__ || __DragonFly__ || __HAIKU__
#include <sys/ioctl.h>
#include <termios.h>
#else
#include <stropts.h>
#endif

#endif

#define GPROBE_SIZE     (1LL << 32)
#define GPROBE_I2C_ADDR 0x6e

#define I2C_SLAVE 0x0703

/* serial port code adapted from git://sigrok.org/libserialport */
struct gport {
	const char *name;
#if __WINDOWS__
	HANDLE hdl;
	COMMTIMEOUTS timeouts;
	OVERLAPPED write_ovl;
	OVERLAPPED read_ovl;
	OVERLAPPED wait_ovl;
	DWORD events;
	BYTE pending_byte;
	BOOL writing;
	BOOL wait_running;
#else
	int fd;
#endif
	int (*send_request)(struct gport *port, RzBuffer *request);
	int (*get_reply)(struct gport *port, ut8 cmd, RzBuffer *reply);
	void (*frame)(RzBuffer *frame);

	ut32 max_rx_size;
	ut32 max_tx_size;
};

typedef struct {
	struct gport gport;
	ut64 offset;
} RzIOGprobe;

enum {
	GPROBE_DEBUGON = 0x09,
	GPROBE_DEBUGOFF = 0x0a,
	GPROBE_ACK = 0x0c,
	GPROBE_RESET = 0x20,
	GPROBE_GET_DEVICE_ID = 0x30,
	GPROBE_GET_INFORMATION = 0x40,
	GPROBE_RAM_READ_2 = 0x52,
	GPROBE_RAM_WRITE_2 = 0x53,
	GPROBE_RUN_CODE_2 = 0x54,
};

#if __UNIX__
static ut8 gprobe_checksum_i2c(const ut8 *p, unsigned int size, ut8 initial) {
	ut8 res = initial;
	unsigned int k;

	for (k = 0; k < size; k++) {
		res ^= p[k];
	}

	return res;
}

static void gprobe_frame_i2c(RzBuffer *frame) {
	ut8 size = rz_buf_size(frame) + 1;
	ut8 header[] = { 0x51, 0x80 + size + 3, 0xc2, 0x00, 0x00 };

	rz_buf_prepend_bytes(frame, &size, 1);
	rz_buf_prepend_bytes(frame, header, sizeof(header));

	ut64 tmpsz;
	const ut8 *tmp = rz_buf_data(frame, &tmpsz);
	ut8 checksum = gprobe_checksum_i2c(tmp, tmpsz, GPROBE_I2C_ADDR);

	rz_buf_append_bytes(frame, &checksum, 1);
}

static int gprobe_get_reply_i2c(struct gport *port, ut8 cmd, RzBuffer *reply) {
	ut8 buf[131];
	int count;
	int ddc2bi3_len;
	ut8 addr = 0x50;
	ut8 checksum;

	rz_sys_usleep(40000);

	count = read(port->fd, buf, sizeof(buf));

	if (count != sizeof(buf)) {
		return -1;
	}

	ddc2bi3_len = buf[1] & ~0x80;

	if (((buf[0] & 0xfe) != GPROBE_I2C_ADDR) || !(buf[1] & 0x80) || (buf[2] != 0xc2) || (buf[3] != 0x00) || (buf[4] != 0x00) || (cmd != buf[6]) || !(buf[5] - 2) || (buf[5] != ddc2bi3_len - 2)) {
		return -1;
	}

	checksum = gprobe_checksum_i2c(&addr, 1, 0);

	if (gprobe_checksum_i2c(buf, ddc2bi3_len + 2, checksum) != buf[ddc2bi3_len + 2]) {
		eprintf("gprobe rx checksum error\n");
	}

	rz_buf_append_bytes(reply, buf + 7, buf[5] - 3);

	return 0;
}

static int gprobe_send_request_i2c(struct gport *port, RzBuffer *request) {
	ut64 tmpsz;
	const ut8 *tmp = rz_buf_data(request, &tmpsz);
	if (write(port->fd, tmp, tmpsz) != rz_buf_size(request)) {
		return -1;
	}
	return 0;
}

static int i2c_open(struct gport *port) {
	char *end, filename[32];
	int i2cbus = strtol(port->name + 4, &end, 0);

	if (*end) {
		return -1;
	}

	snprintf(filename, sizeof(filename), "/dev/i2c/%d", i2cbus);
	filename[sizeof(filename) - 1] = '\0';
	int file = rz_sys_open(filename, O_RDWR, 0);

	if (file < 0 && (errno == ENOENT || errno == ENOTDIR)) {
		sprintf(filename, "/dev/i2c-%d", i2cbus);
		file = rz_sys_open(filename, O_RDWR, 0);
	}
	if (file < 0) {
		return -1;
	}
	if (ioctl(file, I2C_SLAVE, GPROBE_I2C_ADDR >> 1) < 0) {
		close(file);
		port->fd = -1;
		return -1;
	}
	port->fd = file;
	return 0;
}
#endif

static int sp_close(struct gport *port) {
#if __WINDOWS__
	/* Returns non-zero upon success, 0 upon failure. */
	if (CloseHandle(port->hdl) == 0) {
		return -1;
	}
	port->hdl = INVALID_HANDLE_VALUE;

	/* Close event handles for overlapped structures. */
#define CLOSE_OVERLAPPED(ovl) \
	do { \
		if (port->ovl.hEvent != INVALID_HANDLE_VALUE && \
			CloseHandle(port->ovl.hEvent) == 0) \
			return -1; \
	} while (0)
	CLOSE_OVERLAPPED(read_ovl);
	CLOSE_OVERLAPPED(write_ovl);
	CLOSE_OVERLAPPED(wait_ovl);
#else
	if (close(port->fd) == -1) {
		return -1;
	}

	port->fd = -1;
#endif
	return 0;
}

#if __WINDOWS__
/* To be called after port receive buffer is emptied. */
static int restart_wait(struct gport *port) {
	DWORD wait_result;

	if (port->wait_running) {
		/* Check status of running wait operation. */
		if (GetOverlappedResult(port->hdl, &port->wait_ovl,
			    &wait_result, FALSE)) {
			port->wait_running = FALSE;
		} else if (GetLastError() == ERROR_IO_INCOMPLETE) {
			return 0;
		}
		return -1;
	}
	if (!port->wait_running) {
		/* Start new wait operation. */
		if (WaitCommEvent(port->hdl, &port->events,
			    &port->wait_ovl)) {
		} else if (GetLastError() == ERROR_IO_PENDING) {
			port->wait_running = TRUE;
		}
		return -1;
	}
	return 0;
}
#endif

static int sp_open(struct gport *port) {
#if __WINDOWS__
	int ret;
	DWORD errors;
	char *escaped_port_name;
	COMSTAT status;
	DCB dcb;
	LPTSTR filename_;

	/* Prefix port name with '\\.\' to work with ports above COM9. */
	if (!(escaped_port_name = malloc(strlen(port->name) + 5))) {
		return -1;
	}
	sprintf(escaped_port_name, "\\\\.\\%s", port->name);

	filename_ = rz_sys_conv_utf8_to_win(escaped_port_name);

	port->hdl = CreateFile(filename_, GENERIC_READ | GENERIC_WRITE, 0, 0,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, 0);

	free(escaped_port_name);

	if (port->hdl == INVALID_HANDLE_VALUE) {
		return -1;
	}

	/* All timeouts initially disabled. */
	port->timeouts.ReadIntervalTimeout = 0;
	port->timeouts.ReadTotalTimeoutMultiplier = 0;
	port->timeouts.ReadTotalTimeoutConstant = 0;
	port->timeouts.WriteTotalTimeoutMultiplier = 0;
	port->timeouts.WriteTotalTimeoutConstant = 0;

	if (SetCommTimeouts(port->hdl, &port->timeouts) == 0) {
		sp_close(port);
		return -1;
	}

	/* Prepare OVERLAPPED structures. */
#define INIT_OVERLAPPED(ovl) \
	do { \
		memset(&port->ovl, 0, sizeof(port->ovl)); \
		port->ovl.hEvent = INVALID_HANDLE_VALUE; \
		if ((port->ovl.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL)) == INVALID_HANDLE_VALUE) { \
			sp_close(port); \
			return -1; \
		} \
	} while (0)

	INIT_OVERLAPPED(read_ovl);
	INIT_OVERLAPPED(write_ovl);
	INIT_OVERLAPPED(wait_ovl);

	/* Set event mask for RX and error events. */
	if (SetCommMask(port->hdl, EV_RXCHAR | EV_ERR) == 0) {
		sp_close(port);
		return -1;
	}

	port->writing = FALSE;
	port->wait_running = FALSE;

	ret = restart_wait(port);

	if (ret < 0) {
		sp_close(port);
		return -1;
	}

	dcb.fBinary = TRUE;
	dcb.fDsrSensitivity = FALSE;
	dcb.fErrorChar = FALSE;
	dcb.fNull = FALSE;
	dcb.fAbortOnError = FALSE;

	if (ClearCommError(port->hdl, &errors, &status) == 0) {
		return -1;
	}

	dcb.BaudRate = CBR_115200;

	dcb.ByteSize = 8;
	dcb.Parity = NOPARITY;
	dcb.StopBits = ONESTOPBIT;
	dcb.fRtsControl = RTS_CONTROL_DISABLE;
	dcb.fOutxCtsFlow = FALSE;
	dcb.fDtrControl = DTR_CONTROL_DISABLE;
	dcb.fOutxDsrFlow = FALSE;
	dcb.fInX = FALSE;
	dcb.fOutX = FALSE;

	if (!SetCommState(port->hdl, &dcb)) {
		return -1;
	}

	return 0;
#else
	struct termios tty = { 0 };

	if ((port->fd = rz_sys_open(port->name, O_NONBLOCK | O_NOCTTY | O_RDWR, 0)) < 0) {
		return -1;
	}

	if (tcgetattr(port->fd, &tty) != 0) {
		sp_close(port);
		return -1;
	}

	cfsetospeed(&tty, B115200);
	cfsetispeed(&tty, B115200);

	tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;
	tty.c_iflag &= ~IGNBRK;
	tty.c_lflag = 0;
	tty.c_oflag = 0;
	tty.c_cc[VMIN] = 0;
	tty.c_cc[VTIME] = 0;
	tty.c_iflag &= ~(IXON | IXOFF | IXANY);

	tty.c_cflag |= (CLOCAL | CREAD);
	tty.c_cflag &= ~(PARENB | PARODD);
	tty.c_cflag &= ~CSTOPB;
#ifdef CRTSCTS
	tty.c_cflag &= ~CRTSCTS;
#else
	tty.c_cflag &= ~020000000000;
#endif

	if (tcsetattr(port->fd, TCSANOW, &tty) != 0) {
		return -1;
	}

	return 0;
#endif
}

#if __WINDOWS__
/* Restart wait operation if buffer was emptied. */
static int restart_wait_if_needed(struct gport *port, unsigned int bytes_read) {
	DWORD errors;
	COMSTAT comstat;

	if (bytes_read == 0) {
		return 0;
	}

	if (ClearCommError(port->hdl, &errors, &comstat) == 0) {
		return -1;
	}

	if (comstat.cbInQue == 0) {
		if (restart_wait(port)) {
			return -1;
		}
	}

	return 0;
}
#endif

static int sp_blocking_read(struct gport *port, void *buf,
	size_t count, unsigned int timeout_ms) {
#if __WINDOWS__
	DWORD bytes_read = 0;

	/* Set timeout. */
	if (port->timeouts.ReadIntervalTimeout != 0 ||
		port->timeouts.ReadTotalTimeoutMultiplier != 0 ||
		port->timeouts.ReadTotalTimeoutConstant != timeout_ms) {
		port->timeouts.ReadIntervalTimeout = 0;
		port->timeouts.ReadTotalTimeoutMultiplier = 0;
		port->timeouts.ReadTotalTimeoutConstant = timeout_ms;
		if (SetCommTimeouts(port->hdl, &port->timeouts) == 0) {
			return -1;
		}
	}

	/* Start read. */
	if (ReadFile(port->hdl, buf, count, NULL, &port->read_ovl)) {
		bytes_read = count;
	} else if (GetLastError() == ERROR_IO_PENDING) {
		if (GetOverlappedResult(port->hdl, &port->read_ovl, &bytes_read, TRUE) == 0)
			return -1;
	} else {
		return -1;
	}

	if (restart_wait_if_needed(port, bytes_read)) {
		return -1;
	}

	return bytes_read;
#else
	size_t bytes_read = 0;
	unsigned char *ptr = (unsigned char *)buf;
	struct timeval start, delta, now, end = { 0, 0 };
	int started = 0;
	fd_set fds;
	int result;

	if (timeout_ms) {
		/* Get time at start of operation. */
		gettimeofday(&start, NULL);
		/* Define duration of timeout. */
		delta.tv_sec = timeout_ms / 1000;
		delta.tv_usec = (timeout_ms % 1000) * 1000;
		/* Calculate time at which we should give up. */
		Timeradd(&start, &delta, &end);
	}

	FD_ZERO(&fds);
	FD_SET(port->fd, &fds);

	/* Loop until we have the requested number of bytes. */
	while (bytes_read < count) {
		/*
		 * Check timeout only if we have run select() at least once,
		 * to avoid any issues if a short timeout is reached before
		 * select() is even run.
		 */
		if (timeout_ms && started) {
			gettimeofday(&now, NULL);
			if (Timercmp(&now, &end, >)) {
				/* Timeout has expired. */
				break;
			}
			Timersub(&end, &now, &delta);
		}
		result = select(port->fd + 1, &fds, NULL, NULL, timeout_ms ? &delta : NULL);
		started = 1;
		if (result < 0) {
			if (errno == EINTR) {
				continue;
			} else {
				return -1;
			}
		} else if (result == 0) {
			/* Timeout has expired. */
			break;
		}

		/* Do read. */
		result = read(port->fd, ptr, count - bytes_read);

		if (result < 0) {
			if (errno == EAGAIN) {
				/*
				 * This shouldn't happen because we did a
				 * select() first, but handle anyway.
				 */
				continue;
			} else {
				/* This is an actual failure. */
				return -1;
			}
		}

		bytes_read += result;
		ptr += result;
	}

	return bytes_read;
#endif
}

static int sp_flush(struct gport *port) {
#if __WINDOWS__
	/* Returns non-zero upon success, 0 upon failure. */
	if (PurgeComm(port->hdl, PURGE_RXCLEAR) == 0) {
		return -1;
	}

	if (restart_wait(port)) {
		return -1;
	}
#else
	if (tcflush(port->fd, TCIFLUSH) < 0) {
		return -1;
	}
#endif

	return 0;
}

#if __WINDOWS__
static int await_write_completion(struct gport *port) {
	DWORD bytes_written;
	BOOL result;

	/* Wait for previous non-blocking write to complete, if any. */
	if (port->writing) {
		result = GetOverlappedResult(port->hdl, &port->write_ovl, &bytes_written, TRUE);
		port->writing = 0;
		if (!result) {
			return -1;
		}
	}

	return 0;
}
#endif

static int sp_blocking_write(struct gport *port, const void *buf,
	size_t count, unsigned int timeout_ms) {
#if __WINDOWS__
	DWORD bytes_written = 0;

	if (await_write_completion(port)) {
		return -1;
	}

	/* Set timeout. */
	if (port->timeouts.WriteTotalTimeoutConstant != timeout_ms) {
		port->timeouts.WriteTotalTimeoutConstant = timeout_ms;
		if (SetCommTimeouts(port->hdl, &port->timeouts) == 0) {
			return -1;
		}
	}

	/* Start write. */
	if (WriteFile(port->hdl, buf, count, NULL, &port->write_ovl)) {
		return count;
	} else if (GetLastError() == ERROR_IO_PENDING) {
		if (GetOverlappedResult(port->hdl, &port->write_ovl, &bytes_written, TRUE) == 0) {
			if (GetLastError() == ERROR_SEM_TIMEOUT)
				return 0;
			else
				return -1;
		}
		return bytes_written;
	} else {
		return -1;
	}
#else
	size_t bytes_written = 0;
	unsigned char *ptr = (unsigned char *)buf;
	struct timeval start, delta, now, end = { 0, 0 };
	int started = 0;
	fd_set fds;
	int result;

	if (timeout_ms) {
		/* Get time at start of operation. */
		gettimeofday(&start, NULL);
		/* Define duration of timeout. */
		delta.tv_sec = timeout_ms / 1000;
		delta.tv_usec = (timeout_ms % 1000) * 1000;
		/* Calculate time at which we should give up. */
		Timeradd(&start, &delta, &end);
	}

	FD_ZERO(&fds);
	FD_SET(port->fd, &fds);

	/* Loop until we have written the requested number of bytes. */
	while (bytes_written < count) {
		/*
		 * Check timeout only if we have run select() at least once,
		 * to avoid any issues if a short timeout is reached before
		 * select() is even run.
		 */
		if (timeout_ms && started) {
			gettimeofday(&now, NULL);
			if (Timercmp(&now, &end, >)) {
				/* Timeout has expired. */
				break;
			}
			Timersub(&end, &now, &delta);
		}
		result = select(port->fd + 1, NULL, &fds, NULL, timeout_ms ? &delta : NULL);
		started = 1;
		if (result < 0) {
			if (errno == EINTR) {
				continue;
			} else {
				return -1;
			}
		} else if (result == 0) {
			/* Timeout has expired. */
			break;
		}

		/* Do write. */
		result = write(port->fd, ptr, count - bytes_written);

		if (result < 0) {
			if (errno == EAGAIN) {
				/* This shouldn't happen because we did a select() first, but handle anyway. */
				continue;
			} else {
				/* This is an actual failure. */
				return -1;
			}
		}

		bytes_written += result;
		ptr += result;
	}

	return bytes_written;
#endif
}

static ut8 gprobe_checksum(const ut8 *p, unsigned int size) {
	ut8 res = 0;
	unsigned int k;

	for (k = 0; k < size; k++) {
		res += p[k];
	}

	res = ~res + 1;

	return res;
}

static void gprobe_frame_sp(RzBuffer *frame) {
	ut64 size;
	const ut8 *tmp = rz_buf_data(frame, &size);
	size += 2;
	ut8 checksum;

	rz_buf_prepend_bytes(frame, (const ut8 *)&size, 1);
	checksum = gprobe_checksum(tmp, size - 1);

	rz_buf_append_bytes(frame, &checksum, 1);
}

static int gprobe_get_reply_sp(struct gport *port, ut8 cmd, RzBuffer *reply) {
	ut8 buf[256];
	int count = sp_blocking_read(port, buf, 2, 50);

	if (count < 2) {
		return -1;
	}

	if (cmd != buf[1]) {
		return -1;
	}

	if (!(buf[0] - 2)) {
		return 0;
	}

	count = sp_blocking_read(port, buf + 2, buf[0] - 2, 50) + 2;

	if (count != buf[0]) {
		return -1;
	}

/* checksumming answers does not work reliably */
#if 0
	if (gprobe_checksum(buf, count - 1) != buf[count - 1]) {
		printf("### CHECKSUM FAILED\n");
	}
#endif

	rz_buf_append_bytes(reply, buf + 2, count - 3);

	return 0;
}

static int gprobe_send_request_sp(struct gport *port, RzBuffer *request) {
	sp_flush(port);

	ut64 tmpsz;
	const ut8 *tmp = rz_buf_data(request, &tmpsz);
	if (sp_blocking_write(port, tmp, tmpsz, 100) != tmpsz) {
		return -1;
	}

	return 0;
}

static int gprobe_read(struct gport *port, ut32 addr, ut8 *buf, ut32 count) {
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_RAM_READ_2;
	ut8 addr_be[4];
	ut8 count_be[4];
	int res;

	if (!request || !reply) {
		rz_buf_free(request);
		rz_buf_free(reply);
		return -1;
	}

	count = RZ_MIN(port->max_rx_size, count);

	rz_write_be32(addr_be, addr);
	rz_write_be32(count_be, count);

	rz_buf_append_bytes(request, &cmd, 1);
	rz_buf_append_bytes(request, addr_be, 4);
	rz_buf_append_bytes(request, count_be, 4);

	port->frame(request);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, cmd, reply)) {
		goto fail;
	}

	res = rz_buf_read_at(reply, 0, buf, rz_buf_size(reply));

	rz_buf_free(request);
	rz_buf_free(reply);

	return res;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_write(struct gport *port, ut32 addr, const ut8 *buf, ut32 count) {
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_RAM_WRITE_2;
	ut8 addr_be[4];
	ut8 count_be[4];

	if (!request || !reply) {
		rz_buf_free(request);
		rz_buf_free(reply);
		return -1;
	}

	count = RZ_MIN(port->max_tx_size, count);

	rz_write_be32(addr_be, addr);
	rz_write_be32(count_be, count);

	rz_buf_append_bytes(request, &cmd, 1);
	rz_buf_append_bytes(request, addr_be, 4);
	rz_buf_append_bytes(request, buf, count);

	port->frame(request);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, GPROBE_ACK, reply)) {
		goto fail;
	}

	rz_buf_free(request);
	rz_buf_free(reply);

	return count;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_reset(struct gport *port, ut8 code) {
	if (!port) {
		return -1;
	}
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_RESET;

	if (!request || !reply) {
		goto fail;
	}

	rz_buf_append_bytes(request, &cmd, 1);
	rz_buf_append_bytes(request, &code, 1);

	port->frame(request);

	sp_flush(port);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, GPROBE_ACK, reply)) {
		goto fail;
	}

	rz_buf_free(request);
	rz_buf_free(reply);

	return 0;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_debugon(struct gport *port) {
	if (!port) {
		return -1;
	}
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_DEBUGON;

	if (!request || !reply) {
		goto fail;
	}
	rz_buf_append_bytes(request, &cmd, 1);

	port->frame(request);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, GPROBE_ACK, reply)) {
		goto fail;
	}

	rz_buf_free(request);
	rz_buf_free(reply);

	return 0;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_debugoff(struct gport *port) {
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_DEBUGOFF;

	if (!request || !reply) {
		goto fail;
	}
	rz_buf_append_bytes(request, &cmd, 1);
	port->frame(request);
	if (port->send_request(port, request)) {
		goto fail;
	}
	if (port->get_reply(port, GPROBE_ACK, reply)) {
		goto fail;
	}
	rz_buf_free(request);
	rz_buf_free(reply);
	return 0;
fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_runcode(struct gport *port, ut32 addr) {
	if (!port) {
		return -1;
	}
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_RUN_CODE_2;
	ut8 addr_be[4];

	if (!request || !reply) {
		goto fail;
	}

	rz_write_be32(addr_be, addr);

	rz_buf_append_bytes(request, &cmd, 1);
	rz_buf_append_bytes(request, addr_be, 4);

	port->frame(request);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, GPROBE_ACK, reply)) {
		goto fail;
	}

	rz_buf_free(request);
	rz_buf_free(reply);

	return 0;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_getdeviceid(struct gport *port, ut8 index) {
	if (!port) {
		return -1;
	}
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_GET_DEVICE_ID;

	if (!request || !reply) {
		goto fail;
	}

	rz_buf_append_bytes(request, &cmd, 1);
	rz_buf_append_bytes(request, &index, 1);

	port->frame(request);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, cmd, reply)) {
		goto fail;
	}

	char *s = rz_buf_to_string(reply);
	if (s) {
		printf("%s\n", s);
		free(s);
	}

	rz_buf_free(request);
	rz_buf_free(reply);

	return 0;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int gprobe_getinformation(struct gport *port) {
	if (!port) {
		return -1;
	}
	RzBuffer *request = rz_buf_new();
	RzBuffer *reply = rz_buf_new();
	const ut8 cmd = GPROBE_GET_INFORMATION;
	const ut8 index = 0;

	if (!request || !reply) {
		goto fail;
	}

	rz_buf_append_bytes(request, &cmd, 1);
	rz_buf_append_bytes(request, &index, 1);

	port->frame(request);

	if (port->send_request(port, request)) {
		goto fail;
	}

	if (port->get_reply(port, cmd, reply)) {
		goto fail;
	}

	ut64 tmpsz;
	const ut8 *tmp = rz_buf_data(reply, &tmpsz);
	rz_print_hexdump(NULL, 0, tmp, tmpsz, 16, 1, 1);

	rz_buf_free(request);
	rz_buf_free(reply);

	return 0;

fail:
	rz_buf_free(request);
	rz_buf_free(reply);
	return -1;
}

static int __write(RzIO *io, RzIODesc *fd, const ut8 *buf, int count) {
	RzIOGprobe *gprobe;
	int res;
	int has_written = 0;

	if (!fd || !fd->data || !buf) {
		return -1;
	}

	gprobe = (RzIOGprobe *)fd->data;

	if ((gprobe->offset + count) > GPROBE_SIZE) {
		count = GPROBE_SIZE - gprobe->offset;
	}

	while (has_written < count) {
		res = gprobe_write(&gprobe->gport, gprobe->offset, buf + has_written, count - has_written);
		if (res <= 0) {
			return -1;
		}
		gprobe->offset += res;
		has_written += res;
	}

	return count;
}

static int __read(RzIO *io, RzIODesc *fd, ut8 *buf, int count) {
	int res;
	RzIOGprobe *gprobe;
	int has_read = 0;

	if (!fd || !fd->data || !buf) {
		return -1;
	}

	gprobe = (RzIOGprobe *)fd->data;

	if ((gprobe->offset + count) > GPROBE_SIZE) {
		count = GPROBE_SIZE - gprobe->offset;
	}

	while (has_read < count) {
		res = gprobe_read(&gprobe->gport, gprobe->offset, buf + has_read, count - has_read);
		if (res <= 0) {
			return -1;
		}
		gprobe->offset += res;
		has_read += res;
	}

	return has_read;
}

static int __close(RzIODesc *fd) {
	RzIOGprobe *gprobe;

	if (!fd || !fd->data) {
		return -1;
	}
	gprobe = (RzIOGprobe *)fd->data;

	sp_close(&gprobe->gport);
	RZ_FREE(fd->data);

	return 0;
}

static ut64 __lseek(RzIO *io, RzIODesc *fd, ut64 offset, int whence) {
	RzIOGprobe *gprobe;
	if (!fd || !fd->data) {
		return offset;
	}
	gprobe = (RzIOGprobe *)fd->data;
	switch (whence) {
	case SEEK_SET:
		if (offset >= GPROBE_SIZE) {
			return gprobe->offset = GPROBE_SIZE - 1;
		}
		return gprobe->offset = offset;
	case SEEK_CUR:
		if ((gprobe->offset + offset) >= GPROBE_SIZE) {
			return gprobe->offset = GPROBE_SIZE - 1;
		}
		return gprobe->offset += offset;
	case SEEK_END:
		return gprobe->offset = GPROBE_SIZE - 1;
	}
	return offset;
}

static bool __plugin_open(RzIO *io, const char *pathname, bool many) {
	return pathname && rz_str_startswith(pathname, "gprobe://") && strlen(pathname + strlen("gprobe://"));
}

static RzIODesc *__open(RzIO *io, const char *pathname, int rw, int mode) {
	if (__plugin_open(io, pathname, 0)) {
		RzIOGprobe *gprobe = RZ_NEW0(RzIOGprobe);

		gprobe->offset = 0LL;
		gprobe->gport.name = pathname + strlen("gprobe://");

		if (rz_str_startswith(gprobe->gport.name, "i2c-")) {
#if __UNIX__
			gprobe->gport.send_request = gprobe_send_request_i2c;
			gprobe->gport.get_reply = gprobe_get_reply_i2c;
			gprobe->gport.frame = gprobe_frame_i2c;
			gprobe->gport.max_tx_size = 117;
			gprobe->gport.max_rx_size = 121;

			if (i2c_open(&gprobe->gport)) {
				RZ_FREE(gprobe);
				return NULL;
			}
#else
			RZ_FREE(gprobe);
			return NULL;
#endif
		} else {
			gprobe->gport.send_request = gprobe_send_request_sp;
			gprobe->gport.get_reply = gprobe_get_reply_sp;
			gprobe->gport.frame = gprobe_frame_sp;
			gprobe->gport.max_tx_size = 248;
			gprobe->gport.max_rx_size = 252;

			if (sp_open(&gprobe->gport)) {
				RZ_FREE(gprobe);
				return NULL;
			}
		}

		return rz_io_desc_new(io, &rz_io_plugin_gprobe, pathname, rw, mode, gprobe);
	}

	return NULL;
}

static char *__system(RzIO *io, RzIODesc *fd, const char *cmd) {
	RzIOGprobe *gprobe;

	if (!fd || !fd->data) {
		return NULL;
	}
	gprobe = (RzIOGprobe *)fd->data;

	if (!cmd[0] || cmd[0] == '?' || !strcmp(cmd, "help")) {
		printf("Usage: =!cmd args\n"
		       " =!reset code\n"
		       " =!debugon\n"
		       " =!debugoff\n"
		       " =!runcode address\n"
		       " =!getdeviceid\n"
		       " =!getinformation\n");
		return NULL;
	}

	if (rz_str_startswith(cmd, "reset") && (strlen(cmd) > 6)) {
		ut32 code = (ut32)strtoul(cmd + 6, NULL, 10);

		gprobe_reset(&gprobe->gport, code);

		return NULL;
	}

	if (rz_str_startswith(cmd, "debugon")) {
		gprobe_debugon(&gprobe->gport);

		return NULL;
	}

	if (rz_str_startswith(cmd, "debugoff")) {
		gprobe_debugoff(&gprobe->gport);

		return NULL;
	}

	if (rz_str_startswith(cmd, "runcode") && (strlen(cmd) > 8)) {
		ut32 address = (ut32)strtoul(cmd + 8, NULL, 0);

		gprobe_runcode(&gprobe->gport, address);

		return NULL;
	}

	if (rz_str_startswith(cmd, "getdeviceid")) {
		ut8 index = 0;

		while (!gprobe_getdeviceid(&gprobe->gport, index++)) {
		};

		return NULL;
	}

	if (rz_str_startswith(cmd, "getinformation")) {
		gprobe_getinformation(&gprobe->gport);

		return NULL;
	}

	printf("Try: '=!?'\n");

	return NULL;
}

RzIOPlugin rz_io_plugin_gprobe = {
	.name = "gprobe",
	.desc = "Open gprobe connection",
	.license = "LGPL3",
	.uris = "gprobe://",
	.open = __open,
	.close = __close,
	.read = __read,
	.check = __plugin_open,
	.lseek = __lseek,
	.write = __write,
	.system = __system,
};

#ifndef RZ_PLUGIN_INCORE
RZ_API RzLibStruct rizin_plugin = {
	.type = RZ_LIB_TYPE_IO,
	.data = &rz_io_plugin_gprobe,
	.version = RZ_VERSION
};
#endif
