#include "defines.h"
#include "net.hh"
#include "utils.h"
#include <OpenTransport.h>
#include <OpenTptInternet.h>
#include <Threads.h>
#include <uriparser/Uri.h>
#include "cryanc.h"
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>

extern char gMessageBuf[255];
extern unsigned char gEventByte;

#define BIG_STRING_SIZE 0x800

#define OT_CHECK(x)                                     \
	{                                                   \
		OTResult res = (x);                             \
		char buf[64] = {0};                             \
		if (res != noErr)                               \
		{                                               \
			snprintf(buf, 64, "OT Error at %s:%d: %ld", \
					 __FILE__, __LINE__, res);          \
			GcDie(buf);                                 \
		}                                               \
	}

static Boolean gOTInited;

void GcOTInit(void)
{
	OSStatus status;

	OT_CHECK(InitOpenTransport());

	gOTInited = true;
}

static pascal void MyNotifier(
	void *contextPtr, OTEventCode code,
	OTResult result, void *cookie)
{
	switch (code)
	{
	case kOTSyncIdleEvent:
		YieldToAnyThread();
		break;
	default:
		break;
	}
}

static EndpointRef get_ep(void)
{
	EndpointRef ep;
	OSStatus err = noErr;

	// OpenTransport configuration: TCP/IP over Ethernet
	ep = OTOpenEndpoint(
		OTCreateConfiguration("tcp,ip"),
		0, NULL, &err);
	OT_CHECK(err);

	OT_CHECK(OTSetSynchronous(ep));
	OT_CHECK(OTSetBlocking(ep));
	// OT_CHECK(OTUseSyncIdleEvents(ep, false));
	OT_CHECK(OTBind(ep, nil, nil));
	OT_CHECK(OTSetNonBlocking(ep));

	return ep;
}

void GcWaitWritable(EndpointRef ep)
{
	OTResult res;
	DEBUG_PRINT("waiting for writable\n");
	while (true)
	{
		res = OTLook(ep);
		if (res == T_GODATA)
			break;

		DEBUG_PRINT("waiting for writable (%d)\n", res);
		YieldToAnyThread();
	}
}

int GcSendPending(EndpointRef ep, struct TLSContext *context)
{
	unsigned int out_buffer_len = 0;
	unsigned int out_buffer_index = 0;
	const unsigned char *out_buffer;
	OTResult send_res, res;
	unsigned long junkFlags;

	OT_CHECK(OTSetBlocking(ep));
	out_buffer = tls_get_write_buffer(context, &out_buffer_len);
	while ((out_buffer) && (out_buffer_len > 0))
	{
		res = OTSnd(ep, (void *)&out_buffer[out_buffer_index], out_buffer_len, 0);
		if (res == kOTFlowErr)
		{
			GcWaitWritable(ep);
			continue;
		}
		if (res <= 0)
		{
			DEBUG_PRINT("OTSnd result <=0: %d\n", res);
			send_res = res;
			break;
		}
		out_buffer_len -= res;
		out_buffer_index += res;
	}
	tls_buffer_clear(context);
	OT_CHECK(OTSetNonBlocking(ep));
	return send_res;
}

int GcWaitReadable(EndpointRef ep)
{
	OTResult res;
	while (true)
	{
		res = OTLook(ep);
		switch (res)
		{
		case T_DATA:
			return 0;
		case T_DISCONNECT:
			DEBUG_PRINT("*** Unorderly disconnect\n");
			OT_CHECK(OTRcvDisconnect(ep, nil));
			return -1;
		case T_ORDREL:
			DEBUG_PRINT("*** Orderly disconnect\n");
			OT_CHECK(OTRcvOrderlyDisconnect(ep));
			OT_CHECK(OTSndOrderlyDisconnect(ep));
			return -1;
		case kOTNoError:
			// nothing doing.
			break;
		default:
			DEBUG_PRINT("Unhandled OTLook result: %x\n", res);
			break;
		}
		YieldToAnyThread();
	}
}

int GcValCert(struct TLSContext *c, struct TLSCertificate **chain, int len)
{
	return no_error;
}

void GcFixNewlines(char *buf, size_t buf_len)
{
	int i;
	for (i = 0; i < buf_len; ++i)
	{
		if (buf[i] == '\r')
			buf[i] = ' ';
	}
}

#define GEMINI_DEFAULT_PORT "1965"

int GcFetchURI(net_request *net_req)
{
	UriUriA *uri = net_req->uri;
	OSErr err;
	TCall connectCall;
	DNSAddress addr;
	size_t dnsAddrSz;
	EndpointRef ep = get_ep();

	struct TLSContext *context;
	unsigned char client_message[1024];
	unsigned char read_buffer[BIG_STRING_SIZE];

	OTResult nread;
	OTFlags flags;
	int sent = false;

	char *request, *hostname, *host;
	int charsRequired, charsWritten, hostnameSz, portSz;
	int retcode = 0;

	uriToStringCharsRequiredA(uri, &charsRequired);
	// 3 = \r\n + null terminator
	request = (char *)malloc(charsRequired + 3);
	uriToStringA(request, uri, charsRequired + 1, &charsWritten);
	memcpy(request + charsRequired, "\r\n", 3);

	hostnameSz = uri->hostText.afterLast - uri->hostText.first;
	hostname = (char *)malloc(hostnameSz + 1);
	memcpy(hostname, uri->hostText.first, hostnameSz);
	hostname[hostnameSz] = 0;

	// 7 = port separator + longest port (65536) + null terminator
	host = (char *)malloc(hostnameSz + 7);
	memcpy(host, hostname, hostnameSz);
	host[hostnameSz] = ':';

	if (uri->portText.first)
	{
		portSz = uri->portText.afterLast - uri->portText.first;
		memcpy(host + hostnameSz + 1, uri->portText.first, portSz);
		host[hostnameSz + portSz + 1] = 0;
	}
	else
	{
		memcpy(host + hostnameSz + 1, GEMINI_DEFAULT_PORT, sizeof(GEMINI_DEFAULT_PORT));
	}

	DEBUG_PRINT("Request: %s\nHostname: %s\nHost: %s\n", request, hostname, host);

	context = tls_create_context(0, TLS_V12);
	if (!tls_sni_set(context, hostname))
		GcDie("Unable to set SNI hostname; broken hostname?");

	dnsAddrSz = OTInitDNSAddress(&addr, host);

	OTMemzero(&connectCall, sizeof(TCall));

	connectCall.addr.buf = (unsigned char *)&addr;
	connectCall.addr.len = dnsAddrSz; // sizeof(InetAddress);

	err = OTConnect(ep, &connectCall, nil);
	if (err == kOTLookErr)
	{
		OTResult res = OTLook(ep);
		DEBUG_PRINT("OTConnect error: %d + %d\n", err, res);
		goto fail;
	}
	else if (err)
	{
		DEBUG_PRINT("OTConnect error: %d\n", err);
		goto fail;
	}

	tls_client_connect(context);
	GcSendPending(ep, context);

	while (GcWaitReadable(ep) >= 0)
	{
		nread = OTRcv(ep, client_message, sizeof(client_message), &flags);
		if (nread > 0)
		{
			int i = tls_consume_stream(context, client_message, nread, GcValCert);
			if (i < 0)
			{
				DEBUG_PRINT("*** error in tls_consume_stream\n");
				goto fail;
			}
			GcSendPending(ep, context);

			if (!tls_established(context))
				continue;

			if (!sent)
			{
				tls_write(context, (unsigned char *)request, strlen(request));
				GcSendPending(ep, context);
				sent = true;
			}

			while (nread = tls_read(context, read_buffer, sizeof(read_buffer)))
			{
				GcFixNewlines((char *)read_buffer, nread);
				net_req->resp_buf.append((char *)read_buffer, nread);
				YieldToAnyThread();
			}
		}
	}

	goto success;

fail:
	retcode = -1;
success:

	OTUnbind(ep);
	OTCloseProvider(ep);

	DEBUG_PRINT("done.\n");

	free(request);
	free(hostname);
	free(host);

	return retcode;
}

void GcOTFini(void)
{
	if (gOTInited)
	{
		CloseOpenTransport();
	}
}
