// SPDX-License-Identifier: Apache-2.0
/*
 * Copyright (C) 2023 Gwenhael Goavec-Merou <gwenhael.goavec-merou@trabucayre.com>
 */

#include "remoteBitbang_client.hpp"

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <math.h>

#include <map>
#include <stdexcept>
#include <string>
#include <regex>
#include <utility>
#include <vector>

#include "display.hpp"


#define TCK_OFFSET 2
#define TMS_OFFSET 1
#define TDI_OFFSET 0
#define TCK_BIT    (1 << TCK_OFFSET)
#define TMS_BIT    (1 << TMS_OFFSET)
#define TDI_BIT    (1 << TDI_OFFSET)

RemoteBitbang_client::RemoteBitbang_client(const std::string &ip_addr, int port,
		int8_t verbose):
	_xfer_buf(NULL), _num_bytes(0), _last_tms(TMS_BIT),
	_last_tdi(0), _buffer_size(2048), _sock(0), _port(port)
{
	(void) verbose;
	/* create client to server */
	if (!open_connection(ip_addr))
		throw std::runtime_error("connection failure");

	/* set led to low */
	if (xfer_pkt('b', NULL) < 0)
		throw std::runtime_error("can't set led low");

	_xfer_buf = reinterpret_cast<uint8_t *>(malloc(sizeof(uint8_t)
				* _buffer_size));
	if (!_xfer_buf)
		throw std::runtime_error("can't allocate internal buffer");
}

RemoteBitbang_client::~RemoteBitbang_client()
{
	// flush buffers before quit
	if (_num_bytes != 0)
		flush();

	// set led high
	if (xfer_pkt('B', NULL) < 0)
		printf("can't set led high");

	// send close request
	if (xfer_pkt('Q', NULL) < 0)
		printf("can't send close request");

	// cleanup
	if (_xfer_buf)
		free(_xfer_buf);
	// close socket
	close(_sock);
}

int RemoteBitbang_client::writeTMS(const uint8_t *tms, uint32_t len,
		bool flush_buffer, __attribute__((unused)) const uint8_t tdi)
{
	// empty buffer
	// if asked flush
	if (len == 0)
		return ((flush_buffer) ? flush() : 0);

	uint8_t base_v = '0' + _last_tdi;
	for (uint32_t pos = 0; pos < len; pos++) {
		// buffer full -> write
		if (_num_bytes == _buffer_size)
			ll_write(NULL);
		_last_tms = (tms[pos >> 3] & (1 << (pos & 0x07))) ? TMS_BIT : 0;
		_xfer_buf[_num_bytes++] = base_v + _last_tms;
		_xfer_buf[_num_bytes++] = base_v + _last_tms + TCK_BIT;
	}

	// flush where it's asked or if the buffer is full
	if (flush_buffer || _num_bytes == _buffer_size * 8)
		return flush();
	return len;
}

int RemoteBitbang_client::writeTDI(const uint8_t *tx, uint8_t *rx, uint32_t len,
		bool end)
{
	if (len == 0)  // nothing to do
		return 0;

	uint8_t base_v = '0' + _last_tms;
	for (uint32_t pos = 0; pos < len; pos++) {
		if (_num_bytes == _buffer_size)
			ll_write(NULL);  // NULL because _num_bytes is always 0 when read
		_last_tdi = (tx[pos >> 3] & (1 << (pos & 0x07))) ? TDI_BIT : 0;
		if (end && pos == len - 1) {
			_last_tms = TMS_BIT;
			base_v = '0' + _last_tms;
		}
		_xfer_buf[_num_bytes++] = base_v + _last_tdi;
		_xfer_buf[_num_bytes++] = base_v + _last_tdi + TCK_BIT;
		if (rx) {
			uint8_t tdo;
			ll_write(&tdo);
			if (tdo == '1')
				rx[pos >> 3] |= (1 << (pos & 0x07));
			else
				rx[pos >> 3] &= ~(1 << (pos & 0x07));
		}
	}

	return len;
}

// toggle clk with constant TDI and TMS.
int RemoteBitbang_client::toggleClk(uint8_t tms, uint8_t tdi, uint32_t clk_len)
{
	// nothing to do
	if (clk_len == 0)
		return 0;
	if (_num_bytes != 0)
		flush();

	_last_tms = tms;
	_last_tdi = tdi;
	uint8_t val = (_last_tms | _last_tdi);

	// flush buffer before starting
	if (_num_bytes != 0)
		flush();

	for (uint32_t len = 0; len < clk_len; len++) {
		if (len == _num_bytes)
			ll_write(NULL);
		_xfer_buf[_num_bytes++] = '0' + val;
		_xfer_buf[_num_bytes++] = '0' + (val | TCK_BIT);
	}

	ll_write(NULL);

	return clk_len;
}

int RemoteBitbang_client::flush()
{
	return ll_write(NULL);
}

int RemoteBitbang_client::setClkFreq(uint32_t clkHz)
{
	printWarn("clock speed is not configurable");
	return clkHz;
}

bool RemoteBitbang_client::open_connection(const std::string &ip_addr)
{
	struct sockaddr_in addr;
	addr.sin_family = AF_INET;
	addr.sin_port = htons(_port);
	addr.sin_addr.s_addr = inet_addr(ip_addr.c_str());

	_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (_sock == -1) {
		printError("Socket creation error");
		return false;
	}

	if (connect(_sock, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
		printError("Connection error");
		close(_sock);
		return false;
	}

	int one = 1;
	setsockopt(_sock, IPPROTO_TCP, TCP_NODELAY, (const char *)&one,
			sizeof(one));

	return true;
}

ssize_t RemoteBitbang_client::xfer_pkt(uint8_t instr, uint8_t *rx)
{
	ssize_t len;
	// 1. instruction
	if ((len = write(_sock, &instr, 1)) == -1) {
		printError("Send instruction failed with error " +
				std::to_string(len));
		return -1;
	}

	if (rx) {
		len = recv(_sock, rx, 1, 0);
		if (len < 0) {
			printError("Receive error");
			return len;
		}
	}

	return (rx) ? 1 : 0;
}

bool RemoteBitbang_client::ll_write(uint8_t *tdo)
{
	if (_num_bytes == 0)
		return true;

	ssize_t len;
	// write current buffer
	if ((len = write(_sock, _xfer_buf, _num_bytes)) == -1) {
		printError("Send error error: " + std::to_string(len));
		return false;
	}
	_num_bytes = 0;

	// read only one char (if tdo is not null
	if (tdo) {
		if (xfer_pkt('R', tdo) < 0) {
			printError("read request error");
			return false;
		}
	}

	return true;
}
