#define _GNU_SOURCE
#include <Threads.h>
#include <cstring>
#include <cstdio>
#include "Document.hh"
#include "Uri.hh"
#include "defines.h"
#include "utils.h"
#include "parser.h"
#include "net.hh"
#include <string>
#include <deque>
#include <stdexcept>
#include <memory>
#include <optional>

/* Global variables */
Boolean gDone;
Boolean gBackground = false;
Boolean gCurInLink = false;
WindowPtr gMainWindow;

/* Prototypes */
void GcUpdate(WindowPtr);
void GcUpdateAbout(WindowPtr);
void GcHandleActivate(EventRecord *);
void GcToolboxInit(void);
void GcOTInit(void);
void GcEventLoop(void);
OSErr GcEventInit(void);
void GcHandleEvent(EventRecord *);
void GcHandleMouseDown(EventRecord *);
pascal OSErr GcHandleAppleEvent(
	const AEDesc *, AEDesc *, long);

void GcMenuBarInit(void);
void GcHandleMenu(EventRecord *event, long menuChoice);
void GcHandleAppleMenu(short itemId);
void GcOpenAboutWindow(void);
void GcHandleClick(EventRecord *event, WindowPtr inWin);
void GcGoToURL(const char *urlString);
pascal void *GcProcessingThread(void *);

uint8_t gEventByte = 0;

std::deque<net_request> gProcessQueue;

std::optional<Document> gCurrentDocument;

Cursor gIBeamCursor;

struct Link
{
	Rect bounds;
	std::string target;

	Link(Rect bounds, std::string target) : bounds(std::move(bounds)), target(std::move(target)) {};
};

struct RenderData
{
	RgnHandle regularRgn;
	RgnHandle linkRgn;

	std::vector<Link> links;

	RenderData(RgnHandle regular, RgnHandle link) : regularRgn(regular), linkRgn(link), links()
	{
	}

	RenderData(const RenderData &other) = delete;
	RenderData &operator=(const RenderData &other) = delete;

	RenderData(RenderData &&other)
	{
		*this = std::move(other);
	}

	RenderData &operator=(RenderData &&other)
	{
		this->linkRgn = other.linkRgn;
		this->regularRgn = other.regularRgn;
		this->links = std::move(other.links);
		other.linkRgn = nil;
		other.regularRgn = nil;
		return *this;
	}

	~RenderData()
	{
		if (regularRgn)
			DisposeRgn(regularRgn);

		if (linkRgn)
			DisposeRgn(linkRgn);
	}
};

std::optional<RenderData> gRenderData;

int main(void)
{
	ThreadID tid;
	OSErr res;

	GcToolboxInit();
	MaxApplZone();

	Boolean appearancePresent = false;

	if (GcIsAppearancePresent(&appearancePresent) != noErr || !appearancePresent)
	{
		GcDie("Appearance manager not present!");
	}
	else
	{
		RegisterAppearanceClient();
	}

	gProcessQueue = std::deque<net_request>();
	gCurrentDocument = std::optional<Document>();
	gRenderData.reset();

	gIBeamCursor = **GetCursor(iBeamCursor);

	// print something so the console window opens
	// below the main window
	DEBUG_PRINT("Initializing console window...\n");

	gMainWindow = GetNewCWindow(kBaseRes, nil, kMoveToFront);
	SetWRefCon(gMainWindow, wMainWindow);

	if (gMainWindow == nil)
	{
		SysBeep(20);
		ExitToShell();
	}

	GcEventInit();
	GcMenuBarInit();
	GcOTInit();

	SizeWindow(gMainWindow, RectWidth(&qd.screenBits.bounds) - 40, RectHeight(&qd.screenBits.bounds) - 60, true);

	CenterWindow(gMainWindow);

	ShowWindow(gMainWindow);
	GcUpdate(gMainWindow);

	res = NewThread(kCooperativeThread, GcProcessingThread,
					nil, 2 * 1024 * 1024, 0, nil, &tid);

	if (res < 0)
		GcDie("NewThread failed");

	GcGoToURL("about:arugula");

	GcEventLoop();

	GcOTFini();

	return 0;
}

pascal void *GcProcessingThread(void *)
{
	while (1)
	{
		if (!gProcessQueue.empty())
		{
			net_request req = std::move(gProcessQueue.front());
			gProcessQueue.pop_front();
			int redirect_counter = 0;

		fetch:
			if (GcFetchURI(&req))
				continue;

			auto newline = req.resp_buf.find('\n');
			if (newline == std::string::npos)
			{
				GcWarn("Unable to fulfill request: malformed response");
				continue;
			}
			auto response_line = req.resp_buf.substr(0, newline);
			std::string rest = req.resp_buf.substr(std::min(req.resp_buf.size(), newline + 1));

			DEBUG_PRINT("got response line: %s\n", response_line.c_str());
			gem_response resp = GemParseResponse(response_line.data());

			switch (resp.type)
			{
			case RT_INPUT:
			case RT_SENSITIVE_INPUT:
				GcWarn("input not supported!");
				break;
			case RT_SUCCESS:
				break;
			case RT_TEMPORARY_REDIRECTION:
			case RT_PERMANENT_REDIRECTION:
				if (redirect_counter < 5)
				{
					redirect_counter++;
					// trailing whitespace has been found in redirect URLs in the wild, and it breaks uriparser.
					GcStripTrailingWhitespace(resp.data);
					Uri uri;
					if (uriParseSingleUriA(uri, resp.data, NULL) != 0)
					{
						DEBUG_PRINT("invalid redirect URL: '%s'\n", resp.data);
						continue;
					}
					uriMakeOwnerA(uri);

					if (!uri->absolutePath)
					{
						Uri resolvedUri;
						if (uriAddBaseUriA(resolvedUri, uri, req.uri) != 0)
						{
							GcDie("Unable to resolve relative URI");
						}
						uriMakeOwnerA(resolvedUri);
						uri = std::move(resolvedUri);
					}
					req.uri = std::move(uri);
					req.resp_buf.clear();
					goto fetch;
				}
				else
				{
					GcDie("too many redirects");
				}
				break;
			case RT_TEMPORARY_FAILURE:
			case RT_SERVER_UNAVAILABLE:
			case RT_CGI_ERROR:
			case RT_PROXY_ERROR:
			case RT_SLOW_DOWN:
			case RT_PERMANENT_FAILURE:
			case RT_NOT_FOUND:
			case RT_GONE:
			case RT_PROXY_REFUSED:
			case RT_BAD_REQUEST:
			case RT_CERTIFICATE_REQUIRED:
			case RT_CERTIFICATE_UNAUTHORIZED:
			case RT_CERTIFICATE_NOT_VALID:
			case RT_INVALID:
				GcDie("unhandled error response");
				break;
			}

			gCurrentDocument = Document::parse(std::move(req.uri), rest.c_str());
			*req.completeEvent |= 1;
		}
		YieldToAnyThread();
	}
}

void GcOpenAboutWindow(void)
{
	WindowPtr aboutWin;
	aboutWin = GetNewCWindow(wAbout, nil, kMoveToFront);

	CenterWindow(aboutWin);
	ShowWindow(aboutWin);
	SetWRefCon(aboutWin, wAbout);
}

void GcMenuBarInit(void)
{
	Handle menuBar;
	MenuHandle menu;

	menuBar = GetNewMBar(kBaseRes);
	SetMenuBar(menuBar);

	/* Add the Apple menu items to the Apple menu. */
	menu = GetMenuHandle(kBaseRes);
	AppendResMenu(menu, 'DRVR');

	DrawMenuBar();
}

void GcInvalWindow(void)
{
	GrafPtr oldPort;
	GetPort(&oldPort);
	SetPort(gMainWindow);

	EraseRect(&gMainWindow->portRect);
	InvalRect(&gMainWindow->portRect);

	SetPort(oldPort);
}

void GcUpdateAbout(WindowPtr win)
{
	long w1, w2;

	TextFont(0);
	TextSize(18);

	w1 = StringWidth("\pArugula");

	MoveTo((RectWidth(&win->portRect) - w1) / 2, 40);
	DrawString("\pArugula");

	TextSize(12);
	w2 = StringWidth("\pby Sam van Kampen");
	MoveTo((RectWidth(&win->portRect) - w2) / 2, 60);
	DrawString("\pby Sam van Kampen");
}

int GcGetLineHeight(void)
{
	FontInfo info;
	GetFontInfo(&info);
	return info.ascent + info.descent + info.leading + 3;
}

void GcUpdateMain(WindowPtr win, Rect *drawingRect)
{
	if (!gCurrentDocument)
	{
		return;
	}

	TextFont(1);
	TextSize(10);

	RgnHandle regularRgn = NewRgn();
	RgnHandle linkRgn = NewRgn();
	RgnHandle tmpRgn = NewRgn();

	RGBColor blue;
	blue.blue = UINT16_MAX;
	blue.red = 0;
	blue.green = 0;

	RGBColor black;
	black.blue = 0;
	black.red = 0;
	black.green = 0;

	RectRgn(regularRgn, drawingRect);

	short lineHeight = 0;

	short maxWidth = RectWidth(drawingRect);

	maxWidth -= 10;

	short hOffset = 10;
	short vOffset = 5;

	const Document &doc = *gCurrentDocument;

	RenderData rData(regularRgn, linkRgn);

	for (const gem_line &line : doc.lines)
	{
		switch (line.type)
		{
		case LT_HEADING:
			TextFace(bold);
			break;
		case LT_LINK:
			TextFace(underline);
			break;
		case LT_QUOTE:
			TextFace(italic);
			break;
		default:
			TextFace(normal);
		}

		lineHeight = GcGetLineHeight();

		vOffset += lineHeight;
		MoveTo(hOffset, vOffset);

		short curWidth = 20;
		short textOffset = 0;

		while (line.text[textOffset])
		{
			char *spaceLoc = strchr(&line.text[textOffset], ' ');
			short bytesToDraw;
			if (!spaceLoc)
			{
				// rest of line
				bytesToDraw = strlen(&line.text[textOffset]);
			}
			else
			{
				bytesToDraw = spaceLoc - (line.text + textOffset) + 1;
			}
			short width = TextWidth(line.text, textOffset, bytesToDraw);
			// printf("textOffset: %d, bytes to draw: %d, width: %d\r\n", textOffset, bytesToDraw, width);
			if (curWidth + width < maxWidth)
			{
				Point p;
				GetPen(&p);
				if (line.type == LT_LINK)
					RGBForeColor(&blue);
				DrawText(line.text, textOffset, bytesToDraw);
				if (line.type == LT_LINK)
					RGBForeColor(&black);

				if (line.type == LT_LINK)
				{
					OpenRgn();

					Rect r;
					SetRect(&r, p.h, p.v - lineHeight + 3, p.h + width, p.v + 3);
					rData.links.emplace_back(r, std::string(line.url));
					FrameRect(&r);

					CloseRgn(tmpRgn);
					UnionRgn(tmpRgn, linkRgn, linkRgn);
				}
				textOffset += bytesToDraw;
				curWidth += width;
			}
			else
			{
				curWidth = 20;
				vOffset += lineHeight;
				MoveTo(hOffset, vOffset);
			}
		}
	}

	DisposeRgn(tmpRgn);
	DiffRgn(regularRgn, linkRgn, regularRgn);

	gRenderData = std::move(rData);
}

void GcUpdate(WindowPtr win)
{
	GrafPtr oldPort;
	Rect drawingRect;
	RgnHandle tempRgn;
	long refCon;

	GetPort(&oldPort);
	SetPort(win);

	refCon = GetWRefCon(win);

	switch (refCon)
	{
	case wAbout:
		GcUpdateAbout(win);
		break;
	case wMainWindow:
		DrawGrowIcon(win);

		tempRgn = NewRgn();
		GetClip(tempRgn);

		drawingRect = win->portRect;
		drawingRect.right -= kScrollbarAdjust;
		drawingRect.bottom -= kScrollbarAdjust;

		ClipRect(&drawingRect);
		EraseRect(&drawingRect);

		GcUpdateMain(win, &drawingRect);

		SetClip(tempRgn);
		DisposeRgn(tempRgn);
		break;
	}

	SetPort(oldPort);
}

void GcHandleActivate(EventRecord *event)
{
	WindowPtr win = (WindowPtr)event->message;
	if (GetWRefCon(win) == wMainWindow)
		DrawGrowIcon(win);
}

typedef pascal short (*AppleEventProcPtr)(const AEDesc *, AEDesc *, long);

OSErr GcEventInit(void)
{
	OSErr err;
#if __POWERPC__
	AEEventHandlerUPP handlePtr = NewAEEventHandlerProc(GcHandleAppleEvent);
#else
	AppleEventProcPtr handlePtr = GcHandleAppleEvent;
#endif
	err = AEInstallEventHandler(
		kCoreEventClass, kAEOpenApplication,
		handlePtr, 0L, false);
	if (err != noErr)
		goto end;
	err = AEInstallEventHandler(
		kCoreEventClass, kAEOpenDocuments,
		handlePtr, 0L, false);
	if (err != noErr)
		goto end;
	err = AEInstallEventHandler(
		kCoreEventClass, kAEPrintDocuments,
		handlePtr, 0L, false);
	if (err != noErr)
		goto end;
	err = AEInstallEventHandler(
		kCoreEventClass, kAEQuitApplication,
		handlePtr, 0L, false);
	if (err != noErr)
		goto end;

end:
	return err;
}

pascal OSErr GcHandleAppleEvent(
	const AEDesc *evt,
	AEDesc *reply,
	long refCon)
{
	(void)evt;
	(void)reply;
	(void)refCon;

	return noErr;
}

void GcUpdateCursor(RgnHandle cursorRgn, Point where)
{
	if (FrontWindow() != gMainWindow || !gRenderData)
	{
		SetEmptyRgn(cursorRgn);
		SetCursor(&qd.arrow);
		return;
	}

	GrafPtr port;
	GetPort(&port);
	SetPort(gMainWindow);

	GlobalToLocal(&where);

	Point globalOffset = {0, 0};
	LocalToGlobal(&globalOffset);

	if (PtInRgn(where, gRenderData->linkRgn))
	{
		SetCursor(&qd.arrow);
		CopyRgn(gRenderData->linkRgn, cursorRgn);
		OffsetRgn(cursorRgn, globalOffset.h, globalOffset.v);
	}
	else if (PtInRgn(where, gRenderData->regularRgn))
	{
		SetCursor(&gIBeamCursor);
		CopyRgn(gRenderData->regularRgn, cursorRgn);
		OffsetRgn(cursorRgn, globalOffset.h, globalOffset.v);
	}
	else
	{
		SetEmptyRgn(cursorRgn);
		SetCursor(&qd.arrow);
	}

	SetPort(port);
}

void GcEventLoop(void)
{
	EventRecord event;

	gDone = false;

	RgnHandle cursorRgn = NewRgn();

	while (gDone == false)
	{
		if (WaitNextEvent(everyEvent, &event, kMaxSleep, cursorRgn))
		{
			if (event.what != kHighLevelEvent && !gBackground)
			{
				GcUpdateCursor(cursorRgn, event.where);
			}
			GcHandleEvent(&event);
		}
		YieldToAnyThread();

		if (gEventByte & 1)
		{
			gEventByte = 0;

			GcInvalWindow();
		}
	}
}

void GcHandleEvent(EventRecord *event)
{
	WindowPtr win;
	char theChar;

	switch (event->what)
	{
	case mouseDown:
		GcHandleMouseDown(event);
		break;
	case updateEvt:
		win = (WindowPtr)event->message;
		BeginUpdate(win);
		GcUpdate(win);
		EndUpdate(win);
		break;
	case activateEvt:
		GcHandleActivate(event);
		break;
	case keyDown:
	case autoKey:
		theChar = event->message & maskCharCode;
		if (event->modifiers & cmdKey)
		{
			GcHandleMenu(event, MenuKey(theChar));
		}
	case osEvt:
		break;
	default:
		break;
	}
}

void GcLoadAbout()
{
	std::optional<Document> doc = Document::parse(
		"about:arugula",
		"# About Arugula\r\n"
		"Arugula is a browser for the Gemini protocol on Classic Mac OS.\r\n"
		"=> gemini://home.segfault.party/ Arugula homepage\r\n"
		"=> gemini://geminiprotocol.net/ Gemini protocol documentation\r\n"
		"> Here is a quote\r\n");

	gCurrentDocument = std::move(doc);
	gEventByte |= 1;
}

void GcGoToURL(const char *urlString)
{
	Uri uri;
	int res;

	const char *errorPos;
	if (res = uriParseSingleUriA(uri, urlString, &errorPos) != 0)
	{
		char buf[128];
		sprintf(buf, "unable to parse URI: %d", res);
		GcDie(buf);
	}
	uriMakeOwnerA(uri);

	if (strncmp(uri->scheme.first, "gemini", 6) == 0)
	{
		ThreadID tid;

		net_request req = {
			.uri = std::move(uri),
			.resp_buf = {},
			.completeEvent = &gEventByte};

		gProcessQueue.emplace_back(std::move(req));
	}
	else if (strncmp(uri->scheme.first, "about", 5) == 0)
	{
		// internal URI
		if (strncmp(uri->pathHead->text.first, "arugula", 7) == 0)
		{
			GcLoadAbout();
		}
	}
}

Boolean GcURLOpenFilter(DialogRef dlog, EventRecord *theEvent, DialogItemIndex *itemHit)
{
	short itemType;
	Handle ok, urlTextHnd;
	Str255 urlText;

	GetDialogItem(dlog, 1, &itemType, &ok, nil);
	GetDialogItem(dlog, 2, &itemType, &urlTextHnd, nil);

	GetDialogItemText(urlTextHnd, urlText);

	if (theEvent->what == keyDown)
	{
		char charCode = theEvent->message & charCodeMask;
		switch (charCode)
		{
		case kReturnCharCode:
		case kEnterCharCode:
			if (urlText[0])
			{
				*itemHit = GetDialogDefaultItem(dlog);
				return true;
			}
			else
			{
				// swallow the event
				theEvent->what = nullEvent;
				return false;
			}
			break;
		case '.':
			if (theEvent->modifiers & cmdKey)
			{
				*itemHit = GetDialogCancelItem(dlog);
				return true;
			}
			break;
		}
	}

	if (urlText[0] == 0)
		HiliteControl((ControlRef)ok, kDisableButton);
	else
		HiliteControl((ControlRef)ok, kEnableButton);

	return false;
}

void GcOpenURLDialog()
{
	short itemHit, itemType;
	bool dialogDone = false;
	bool dialogCancelled = false;
	Handle urlTextHnd;
	Handle ok, cancel;
	Str255 urlText;
	DialogPtr dlog = GetNewDialog(kBaseRes, nil, kMoveToFront);
	ShowWindow(dlog);
	GetDialogItem(dlog, 1, &itemType, &ok, nil);
	GetDialogItem(dlog, 2, &itemType, &cancel, nil);
	GetDialogItem(dlog, 3, &itemType, &urlTextHnd, nil);
	int charsWritten;

	SetDialogDefaultItem(dlog, 1);
	SetDialogCancelItem(dlog, 2);
	SetDialogTracksCursor(dlog, true);

	char renderedUri[255];
	uriToStringA(renderedUri, gCurrentDocument->uri, 255, &charsWritten);

	setdialogitemtext(urlTextHnd, renderedUri);
	SelectDialogItemText(dlog, 3, 0, 255);

	if (renderedUri[0] == 0)
		HiliteControl((ControlRef)ok, kDisableButton);

	while (!dialogDone)
	{
		ModalDialog(NewModalFilterUPP(GcURLOpenFilter), &itemHit);
		switch (itemHit)
		{
		case 1:
			GetDialogItemText(urlTextHnd, urlText);
			dialogDone = true;
			break;
		case 2:
			dialogCancelled = true;
			dialogDone = true;
			break;
		default:
			break;
		}
	}

	DisposeDialog(dlog);

	if (dialogCancelled)
		return;

	OSStatus res;

	int urlLen = urlText[0];
	char *urlCStr = p2cstr(urlText);

	char properSchemeURL[280];

	if (strncmp(urlCStr, "gemini://", 9) != 0 && strncmp(urlCStr, "about:", 6) != 0)
	{
		memcpy(properSchemeURL, "gemini://", 9);
		memcpy(properSchemeURL + 9, urlCStr, urlLen + 1);
		urlCStr = properSchemeURL;
	}
	DEBUG_PRINT("Raw URL: %s\n", urlCStr);

	GcGoToURL(urlCStr);
}

void GcHandleFileMenu(short itemId)
{
	switch (itemId)
	{
	case iOpenURL:
		GcOpenURLDialog();
		break;
	case iQuit:
		gDone = true;
		break;
	default:
		break;
	}
}

void GcHandleMenu(EventRecord *event, long menuChoice)
{
	short menuId;
	short itemId;

	menuId = HiWord(menuChoice);
	itemId = LoWord(menuChoice);

	switch (menuId)
	{
	case mApple:
		GcHandleAppleMenu(itemId);
		break;
	case mFile:
		GcHandleFileMenu(itemId);
		break;
	default:
		break;
	}

	HiliteMenu(0);
}

void GcHandleAppleMenu(short itemId)
{
	MenuHandle appleMenu;
	Str255 accName;

	switch (itemId)
	{
	case iAbout:
		GcOpenAboutWindow();
		break;
	default:
		appleMenu = GetMenuHandle(mApple);
		GetMenuItemText(appleMenu, itemId, accName);
		OpenDeskAcc(accName);
		break;
	}
}

void GcHandleLink(const char *toUrl)
{
	if (!gCurrentDocument)
		GcDie("link clicked without document open?");

	auto &baseUri = gCurrentDocument->uri;

	Uri resolvedUri;
	Uri relUri;

	if (uriParseSingleUriA(relUri, toUrl, nil) != 0)
		GcDie("Link: invalid target URI?");

	if (uriAddBaseUriA(resolvedUri, relUri, baseUri) != 0)
		GcDie("Link: unable to resolve relative URI");

	char resolvedUriText[512];
	int charsWritten;
	uriToStringA(resolvedUriText, resolvedUri, 511, &charsWritten);

	resolvedUriText[charsWritten] = 0;
	GcGoToURL(resolvedUriText);
}

void GcHandleClick(EventRecord *event, WindowPtr inWin)
{
	long refCon;

	if (FrontWindow() != inWin)
	{
		// bring window to front
		SelectWindow(inWin);
		return;
	}

	refCon = GetWRefCon(inWin);
	if (refCon == wAbout)
	{
		CloseWindow(inWin);
	}
	else if (refCon == wMainWindow)
	{
		if (!gRenderData)
			return;

		Point where = event->where;
		GlobalToLocal(&where);
		for (const Link &lnk : gRenderData->links)
		{
			if (PtInRect(where, &lnk.bounds))
			{
				GcHandleLink(lnk.target.c_str());
			}
		}
	}
}

void GcHandleMouseDown(EventRecord *event)
{
	WindowPtr win;
	GrafPtr oldPort;
	Rect growRect = {kMinHeight, kMinWidth, 2048, 2048};
	long inPart;
	long windSize;
	long menuChoice;

	inPart = FindWindow(event->where, &win);

	GetPort(&oldPort);

	switch (inPart)
	{
	case inMenuBar:
		menuChoice = MenuSelect(event->where);
		if (menuChoice != 0)
			GcHandleMenu(event, menuChoice);
		break;
	case inGoAway:
		if (TrackGoAway(win, event->where))
		{
			CloseWindow(win);
		}
		break;
	case inDrag:
		DragWindow(win, event->where, &qd.screenBits.bounds);
		break;
	case inGrow:
		SetPort(win);
		windSize = GrowWindow(win, event->where, &growRect);
		EraseRect(&win->portRect);
		SizeWindow(win, LoWord(windSize), HiWord(windSize), true);
		InvalRect(&win->portRect);
		SetPort(oldPort);
		break;
	case inSysWindow:
		SystemClick(event, win);
		break;
	case inContent:
		SetPort(win);
		GcHandleClick(event, win);
		SetPort(oldPort);
	default:
		break;
	}
}

// Initialize the different toolbox managers
void GcToolboxInit(void)
{
	/* Initialize all the needed managers. */
	InitGraf(&qd.thePort);
	InitFonts();
	InitWindows();
	InitMenus();
	TEInit();
	InitDialogs(nil);
	InitCursor();
}
