/*

net.c - Networking

*/

/*...sincludes:0:*/
#include <stdio.h>
#include "interrupts.h"
#include "list.h"
#include "fiber.h"
#include "sem.h"
#include "cpmio.h"
#include "wiz.h"
#include "net.h"

/*...vinterrupts\46\h:0:*/
/*...vlist\46\h:0:*/
/*...vfiber\46\h:0:*/
/*...vsem\46\h:0:*/
/*...vcpmio\46\h:0:*/
/*...vwiz\46\h:0:*/
/*...vnet\46\h:0:*/
/*...e*/

/*...svars:0:*/
static const byte ip_subnet[]  = { 255,255,255,0 };
static const byte ip[]         = { 192,168,1,123 }; /* chip errata, cannot be 0.0.0.0 */
static const byte ip_gateway[] = { 192,168,1,1 };

// https://en.wikipedia.org/wiki/MAC_address
// http://standards-oui.ieee.org/oui/oui.txt
// 00-08-DC-xx-xx-xx is Wiznet
// x2-xx-xx-xx-xx-xx is locally administered unicast
static const byte mac[] = { 0x00,0x08,0xdc, 'N','F','X' };

#define	N_SOCKETS 4
static SOCKET net_sockets[N_SOCKETS];
static LIST net_sockets_free;
static LIST net_sockets_waiters;

static word net_ephemeral = 0; 
/*...e*/

/*...snet_fatal:0:*/
static void net_fatal(const char *error)
	{
	interrupts_disable();
	bdos_printf(error);
	for ( ;; )
		;
	}
/*...e*/

/*...sdiags:0:*/
static char nibble[] = "_$";
static void outnibble(byte n)
	{
	nibble[0] = ( n < 10 ) ? ( n+'0' ) : ( -10+'A' );
	bdos_printf(nibble);
	}
static void outbyte(byte b)
	{
	outnibble(b>>4);
	outnibble(b   );
	}
/*...e*/

/*...snet_alloc:0:*/
void net_alloc(SOCKET_ALLOC *sockalloc, NF nf, void *nfp)
	{
	SOCKET *socket = list_remove_first(&net_sockets_free);
	if ( socket != NULL )
		{
		socket->state = SS_ALLOCATED;
		sockalloc->socket = socket;
		nf(nfp);
		}
	else
		{
		sockalloc->fiber.nf  = nf;
		sockalloc->fiber.nfp = nfp;
		list_add_last(&net_sockets_waiters, sockalloc);
		}
	}
/*...e*/
/*...snet_free:0:*/
void net_free(SOCKET *socket)
	{
	SOCKET_ALLOC *sockalloc = list_remove_first(&net_sockets_waiters);
	if ( socket->state != SS_ALLOCATED )
		net_fatal("net_free: socket not SS_ALLOCATED\r\n$");
	if ( sockalloc != NULL )
		{
		sockalloc->socket = socket;
		fiber_enqueue((FIBER *) sockalloc);
		}
	else
		{
		socket->state = SS_FREE;
		list_add_first(&net_sockets_free, socket);
		}
	}
/*...e*/

/*...snet_next_ephemeral_port:0:*/
static word net_next_ephemeral_port(void)
	{
	return 0x8000 + (net_ephemeral++) % 0x7fff;
	}
/*...e*/
/*...snet_init_socket_as_tcp:0:*/
static void net_init_socket_as_tcp(word sra, word port)
	{
	for ( ;; )
		{
		wiz_write(sra+W5100_OFFSET_SOCKET_MR, W5100_SOCKET_MR_TCP);
		wiz_write_word(sra+W5100_OFFSET_SOCKET_PORT0, port);
		wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_OPEN);
		if ( wiz_read(sra+W5100_OFFSET_SOCKET_SR) == W5100_SOCKET_SR_SOCK_INIT )
			break;
		wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_CLOSE);
		}
	}
/*...e*/
/*...snet_complete_op:0:*/
static void net_complete_op(SOCKET *socket)
	{
	(socket->fiber_op.nf)(socket->fiber_op.nfp);
	}
/*...e*/

/*...snet_connect:0:*/
static void net_connect_ok(SOCKET *socket)
	{
	socket->state = SS_ESTABLISHED;
	net_complete_op(socket);
	}

static void net_connect_error(SOCKET *socket)
	{
	word sra = socket->sra;
	wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_CLOSE);
	socket->state = SS_ALLOCATED;
	net_complete_op(socket);
	}

void net_connect(SOCKET *socket, const byte *ip, word port, NF nf, void *nfp)
	{
	word sra = socket->sra;
	if ( socket->state != SS_ALLOCATED )
		net_fatal("net_connect: socket not SS_ALLOCATED\r\n$");
	socket->fiber_op.nf  = nf;
	socket->fiber_op.nfp = nfp;
	socket->consumed = 0;
	net_init_socket_as_tcp(sra, net_next_ephemeral_port());
	wiz_write_n(sra+W5100_OFFSET_SOCKET_DIPR0, ip, 4);
	wiz_write_word(sra+W5100_OFFSET_SOCKET_DPORT0, port);
	wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_CONNECT);
	socket->state = SS_CONNECT;
	}
/*...e*/
/*...snet_listen:0:*/
static void net_listen_ok(SOCKET *socket)
	{
	socket->state = SS_ESTABLISHED;
	net_complete_op(socket);
	}

void net_listen(SOCKET *socket, word port, NF nf, void *nfp)
	{
	word sra = socket->sra;
	if ( socket->state != SS_ALLOCATED )
		net_fatal("net_listen: socket not SS_ALLOCATED\r\n$");
	socket->fiber_op.nf  = nf;
	socket->fiber_op.nfp = nfp;
	socket->consumed = 0;
	net_init_socket_as_tcp(sra, port);
	wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_LISTEN);
	socket->state = SS_LISTEN;
	}
/*...e*/
/*...snet_disconnect:0:*/
static void net_disconnect_ok(SOCKET *socket)
	{
	word sra = socket->sra;
	wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_CLOSE);
	socket->state = SS_ALLOCATED;
	net_complete_op(socket);
	}

void net_disconnect(SOCKET *socket, NF nf, void *nfp)
	{
	word sra = socket->sra;
	if ( socket->state != SS_ESTABLISHED && socket->state != SS_ERROR )
		net_fatal("net_disconnect: socket not SS_ESTABLISHED or SS_ERROR\r\n$");
	socket->fiber_op.nf  = nf;
	socket->fiber_op.nfp = nfp;
	wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_DISCON);
	socket->state = SS_DISCONNECT;
	}
/*...e*/

/*...snet_send:0:*/
static void net_send_chunk_error(SOCKET *socket)
	{
	socket->state = SS_ERROR;
	net_complete_op(socket);
	}

static void net_send_chunk(SOCKET *socket)
	{
	if ( socket->actual == socket->length )
		{
		socket->state = SS_ESTABLISHED;
		net_complete_op(socket);
		}
	else
		{
		word sra = socket->sra;
		word wr = wiz_read_word(sra+W5100_OFFSET_SOCKET_TX_WR0);
		word wr_modulo = wr & 2047;
		word thisgo = socket->length - socket->actual;
		if ( thisgo > 2048 )
			thisgo = 2048;
		if ( wiz_read_word(sra+W5100_OFFSET_SOCKET_TX_FSR0) < thisgo )
			net_fatal("net_send_chunk: no free space\r\n$");
		if ( wr_modulo + thisgo <= 2048 )
			wiz_write_n(socket->tx_buf+wr_modulo, socket->send_ptr, thisgo);
		else
			{
			word thisgo1 = 2048-wr_modulo;
			word thisgo2 = thisgo-thisgo1;
			wiz_write_n(socket->tx_buf+wr_modulo, socket->send_ptr        , thisgo1);
			wiz_write_n(socket->tx_buf          , socket->send_ptr+thisgo1, thisgo2); 
			}
		wiz_write_word(sra+W5100_OFFSET_SOCKET_TX_WR0, wr+thisgo);
		socket->thisgo = thisgo;
		wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_SEND);
		}
	}

static void net_send_chunk_ok(SOCKET *socket)
	{
	socket->send_ptr += socket->thisgo;
	socket->actual   += socket->thisgo;
	net_send_chunk(socket);
	}

void net_send(SOCKET *socket, const byte *ptr, size_t length, NF nf, void *nfp)
	{
	word sra = socket->sra;
	if ( socket->state != SS_ESTABLISHED )
		net_fatal("net_send: socket not SS_ESTABLISHED\r\n$");
	socket->fiber_op.nf  = nf;
	socket->fiber_op.nfp = nfp;
	socket->send_ptr     = ptr;
	socket->length       = length;
	socket->actual       = 0;
	socket->state        = SS_SEND;
	net_send_chunk(socket);
	}
/*...e*/
/*...snet_recv:0:*/
static void net_recv_error(SOCKET *socket)
	{
	socket->state = SS_ERROR;
	net_complete_op(socket);
	}

static void net_recv_consume(SOCKET *socket, word available)
	{
	word sra = socket->sra;
	word rd = wiz_read_word(sra+W5100_OFFSET_SOCKET_RX_RD0) + socket->consumed;
	word rd_modulo = rd & 2047;
	word thisgo = socket->length;
	if ( thisgo > available )
		thisgo = available;
	if ( rd_modulo + thisgo <= 2048 )
		wiz_read_n(socket->rx_buf+rd_modulo, socket->recv_ptr, thisgo);
	else
		{
		word thisgo1 = 2048-rd_modulo;
		word thisgo2 = thisgo-thisgo1;
		wiz_read_n(socket->rx_buf+rd_modulo, socket->recv_ptr        , thisgo1);
		wiz_read_n(socket->rx_buf          , socket->recv_ptr+thisgo1, thisgo2); 
		}
	if ( thisgo < available )
		socket->consumed += thisgo;
	else
		{
		socket->consumed = 0;
		wiz_write_word(sra+W5100_OFFSET_SOCKET_RX_RD0, rd+thisgo);
		wiz_write(sra+W5100_OFFSET_SOCKET_CR, W5100_SOCKET_CR_RECV); /* keep receiving */
		}
	socket->actual = thisgo;
	socket->state  = SS_ESTABLISHED;
	net_complete_op(socket);
	}

static void net_recv_more(SOCKET *socket)
	{
	word sra = socket->sra;
	word available = wiz_read_word(sra+W5100_OFFSET_SOCKET_RX_RSR0) - socket->consumed;
	if ( available > 0 )
		net_recv_consume(socket, available);
	}

void net_recv(SOCKET *socket, byte *ptr, size_t length, NF nf, void *nfp)
	{
	if ( socket->state != SS_ESTABLISHED )
		net_fatal("net_send: socket not SS_ESTABLISHED\r\n$");
	socket->fiber_op.nf  = nf;
	socket->fiber_op.nfp = nfp;
	socket->recv_ptr     = ptr;
	socket->length       = length;
	socket->actual       = 0;
	if ( length == 0 )
		net_complete_op(socket);
	else
		{
		socket->state = SS_RECV;
		net_recv_more(socket);
		}
	}
/*...e*/

/*...snet_interrupt:0:*/
static void net_interrupt(SOCKET *socket)
	{
	word sra = socket->sra;
	byte ir = wiz_read(sra+W5100_OFFSET_SOCKET_IR);
	wiz_write(sra+W5100_OFFSET_SOCKET_IR, ir); /* clear it */
	socket->fiber_int_queued = FALSE;
//bdos_printf("s=$"); outbyte(socket->state); bdos_printf(" ir=$"); outbyte(ir); bdos_printf("\r\n$");
	switch ( socket->state )
		{
		case SS_CONNECT:
			if ( ir & (W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_connect_error(socket);
			else if ( ir & W5100_SOCKET_IR_CON )
				net_connect_ok(socket);
			if ( ir & ~(W5100_SOCKET_IR_CON|W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_fatal("net_interrupt: unexpected interrupt in SS_CONNECT\r\n$");
			break;
		case SS_LISTEN:
			if ( ir & W5100_SOCKET_IR_CON )
				net_listen_ok(socket);
			if ( ir & ~(W5100_SOCKET_IR_CON|W5100_SOCKET_IR_RECV) )
				net_fatal("net_interrupt: unexpected interrupt in SS_LISTEN\r\n$");
			break;
		case SS_ESTABLISHED:
			if ( ir & ~W5100_SOCKET_IR_RECV )
				net_fatal("net_interrupt: unexpected interrupt in SS_ESTABLISHED\r\n$");
			break;
		case SS_SEND:
			if ( ir & (W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_send_chunk_error(socket);
			else if ( ir & W5100_SOCKET_IR_SEND_OK )
				net_send_chunk_ok(socket);
			if ( ir & ~(W5100_SOCKET_IR_SEND_OK|W5100_SOCKET_IR_RECV|W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_fatal("net_interrupt: unexpected interrupt in SS_SEND\r\n$");
			break;
		case SS_RECV:
			if ( ir & (W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_recv_error(socket);
			else if ( ir & W5100_SOCKET_IR_RECV )
				net_recv_more(socket);
			if ( ir & ~(W5100_SOCKET_IR_RECV|W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_fatal("net_interrupt: unexpected interrupt in SS_RECV\r\n$");
			break;
		case SS_ERROR:
			break;
		case SS_DISCONNECT:
			if ( ir & (W5100_SOCKET_IR_DISCON|W5100_SOCKET_IR_TIMEOUT) )
				net_disconnect_ok(socket);
			break;
		default:
			net_fatal("net_interrupt: unexpected interrupt\r\n$");
		}
	}
/*...e*/
/*...snet_handle_interrupts:0:*/
void net_handle_interrupts(void)
	{
	byte ir = wiz_read(W5100_ADDR_REG_IR);
	byte i;
	if ( ir & W5100_IR_CONFLICT )
		{
		bdos_printf("net_handle_interrupts: IP Conflict\r\n$");
		wiz_write(W5100_ADDR_REG_IR, W5100_IR_CONFLICT);
		}
	for ( i = 0; i < N_SOCKETS; i++ )
		{
		SOCKET *socket = &(net_sockets[i]);
		if ( ir & socket->int_bit )
			if ( ! socket->fiber_int_queued )
				{
				fiber_enqueue(&(socket->fiber_int));
				socket->fiber_int_queued = TRUE;
				}
		}
	}
/*...e*/

/*...snet_init:0:*/
void net_init(void)
	{
	byte i;
	list_init(&net_sockets_free);
	list_init(&net_sockets_waiters);
	for ( i = 0; i < N_SOCKETS; i++ )
		{
		SOCKET *socket = &(net_sockets[i]);
		socket->fiber_int.nf     = (NF)net_interrupt;
		socket->fiber_int.nfp    = socket;
		socket->fiber_int_queued = FALSE;
		socket->sra              = W5100_ADDR_SOCKET + (i<< 8);
		socket->tx_buf           = W5100_ADDR_TX     + (i<<11);
		socket->rx_buf           = W5100_ADDR_RX     + (i<<11);
		socket->int_bit          = (0x01<<i); /* per W5100_IR_Sx_INT */
		socket->state            = SS_FREE;
		list_add_last(&net_sockets_free, socket);
		}
	wiz_reset();
	wiz_write(W5100_ADDR_REG_IMR, 0x00); /* turn off all interrupts */
	wiz_write_n(W5100_ADDR_REG_SUBR0, ip_subnet, 4);
	wiz_write_n(W5100_ADDR_REG_SIPR0, ip, 4);
	wiz_write_n(W5100_ADDR_REG_GAR0 , ip_gateway, 4);
	wiz_write_n(W5100_ADDR_REG_SHAR0, mac, 6);
	wiz_write_word(W5100_ADDR_REG_RTR0, 2000);
	wiz_write(W5100_ADDR_REG_RCR, 10); /* retries */
	wiz_write(W5100_ADDR_REG_RMSR, 0x55); /* 2KB each for sockets 0,1,2,3 */
	wiz_write(W5100_ADDR_REG_TMSR, 0x55); /* 2KB each for sockets 0,1,2,3 */
	}
/*...e*/
