/*
 * Copyright (c) 2026 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

#import "G2HConverter.h"
#import "G2HConverter+Private.h"

static OFCharacterSet *whitespaceCS, *nonWhitespaceCS;

static OF_INLINE OFString *
escape(OFString *string)
{
	return string.stringByXMLEscaping;
}

@implementation G2HConverter
@synthesize imageExtensions = _imageExtensions;
@synthesize localLinkEmoji = _localLinkEmoji;
@synthesize externalLinkEmoji = _externalLinkEmoji;

+ (void)initialize
{
	if (self != [G2HConverter class])
		return;

	void *pool = objc_autoreleasePoolPush();
	whitespaceCS = [[OFCharacterSet alloc]
	    initWithCharactersInString: @" \t"];
	nonWhitespaceCS = objc_retain(whitespaceCS.invertedSet);
	objc_autoreleasePoolPop(pool);
}

+ (instancetype)converter
{
	return objc_autoreleaseReturnValue([[self alloc] init]);
}

- (instancetype)init
{
	self = [super init];

	@try {
		_imageExtensions = [[OFSet alloc] initWithObjects:
		    @"gif", @"jpeg", @"jpg", @"png", @"qoi", nil];
		_localLinkEmoji = @"👉";
		_externalLinkEmoji = @"🌐";
	} @catch (id e) {
		objc_release(self);
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	objc_release(_imageExtensions);
	objc_release(_localLinkEmoji);
	objc_release(_externalLinkEmoji);
	objc_release(_lastReturnedString);

	[super dealloc];
}

- (OFString *)processLine: (OFString *)line
{
	OFMutableString *output = [OFMutableString string];
	void *pool = objc_autoreleasePoolPush();

	if (_preformatted) {
		if ([line hasPrefix: @"```"]) {
			[output appendString: @"</pre>"];
			_preformatted = false;
		} else
			[output appendString: escape(line)];
	} else {
		bool isHeading = false;
		bool isListItem = [line hasPrefix: @"* "];
		bool isQuote = [line hasPrefix: @">"];

		if (_previousIsListItem && !isListItem)
			[output appendString: @"</ul>\n"];
		if (_previousIsQuote && !isQuote)
			[output appendString: @"</blockquote>\n"];

		if ([line hasPrefix: @"=>"])
			[self g2h_processLink: [line substringFromIndex: 2]
				       output: output];
		else if ([line hasPrefix: @"```"]) {
			[output appendString:
			    @"<pre class='gemtext-preformatted' "
			    @"style='margin-top: 0; margin-bottom: 0'>"];
			_preformatted = true;
		} else if ([line hasPrefix: @"###"]) {
			[output appendFormat:
			    @"<h3 class='gemtext-heading3'>%@</h3>",
			    escape([line substringFromIndex: 3])];
			isHeading = true;
		} else if ([line hasPrefix: @"##"]) {
			[output appendFormat:
			    @"<h2 class='gemtext-heading2'>%@</h2>",
			    escape([line substringFromIndex: 2])];
			isHeading = true;
		} else if ([line hasPrefix: @"#"]) {
			[output appendFormat:
			    @"<h1 class='gemtext-heading1'>%@</h1>",
			    escape([line substringFromIndex: 1])];
			isHeading = true;
		} else if (isListItem) {
			if (!_previousIsListItem)
				[output appendString:
				    @"<ul class='gemtext-list' "
				    @"style='margin-top: 0; "
				    @"margin-bottom: 0'>\n"];

			[output appendFormat:
			    @"<li class='gemtext-list-item'>%@</li>",
			    escape([line substringFromIndex: 2])];
		} else if ([line hasPrefix: @">"]) {
			if (!_previousIsQuote)
				[output appendString:
				    @"<blockquote class='gemtext-quote' "
				    @"style='margin-top: 0; "
				    @"margin-bottom: 0'>\n"];

			[output appendFormat: @"%@<br/>",
			    escape([line substringFromIndex: 1])];
		} else if (!_previousIsHeading || [line rangeOfCharacterFromSet:
		    nonWhitespaceCS].location != OFNotFound)
			[output appendFormat: @"%@<br/>", escape(line)];

		_previousIsHeading = isHeading;
		_previousIsListItem = isListItem;
		_previousIsQuote = isQuote;
	}

	[output makeImmutable];

	objc_autoreleasePoolPop(pool);

	objc_release(_lastReturnedString);
	_lastReturnedString = nil;
	_lastReturnedString = [output copy];

	return output;
}

- (void)g2h_processLink: (OFString *)line output: (OFMutableString *)output
{
	size_t pos = [line rangeOfCharacterFromSet: nonWhitespaceCS].location;
	if (pos == OFNotFound)
		@throw [OFInvalidFormatException exception];

	line = [line substringFromIndex: pos];

	pos = [line rangeOfCharacterFromSet: whitespaceCS].location;
	if (pos == OFNotFound) {
no_description:
		if ([self g2h_IRIStringIsImage: line])
			[output appendFormat:
			    @"<img class='gemtext-image' src='%@'/><br/>",
			    escape(line)];
		else
			[output appendFormat:
			    @"<span class='%@'>%@</span> "
			    @"<a class='%@' href='%@'>%@</a><br/>",
			    [self g2h_emojiCSSClassForIRIString: line],
			    [self g2h_emojiForIRIString: line],
			    [self g2h_CSSClassForIRIString: line], escape(line),
			    escape(line)];

		return;
	}

	OFString *IRI = [line substringToIndex: pos];
	line = [line substringFromIndex: pos + 1];

	pos = [line rangeOfCharacterFromSet: nonWhitespaceCS].location;
	if (pos == OFNotFound) {
		line = IRI;
		goto no_description;
	}

	line = [line substringFromIndex: pos];

	if ([self g2h_IRIStringIsImage: IRI])
		[output appendFormat:
		    @"<img class='gemtext-image' src='%@' alt='%@'/><br/>",
		    escape(IRI), escape(line)];
	else
		[output appendFormat:
		    @"<span class='%@'>%@</span> "
		    @"<a class='%@' href='%@'>%@</a><br/>",
		    [self g2h_emojiCSSClassForIRIString: IRI],
		    [self g2h_emojiForIRIString: IRI],
		    [self g2h_CSSClassForIRIString: IRI], escape(IRI),
		    escape(line)];
}

- (bool)g2h_IRIStringIsImage: (OFString *)IRIString
{
	OFRange range = [IRIString rangeOfString: @"?"];
	if (range.location != OFNotFound)
		IRIString = [IRIString substringToIndex: range.location];

	range = [IRIString rangeOfString: @"."
				 options: OFStringSearchBackwards];
	if (range.location == OFNotFound)
		return false;

	OFString *extension =
	    [IRIString substringFromIndex: range.location + 1].lowercaseString;
	return [_imageExtensions containsObject: extension];
}

- (OFString *)g2h_emojiCSSClassForIRIString: (OFString *)IRIString
{
	if ([IRIString containsString: @"://"] || [IRIString hasPrefix: @"//"])
		return @"gemtext-external-link-emoji";

	return @"gemtext-local-link-emoji";
}

- (OFString *)g2h_emojiForIRIString: (OFString *)IRIString
{
	if ([IRIString containsString: @"://"] || [IRIString hasPrefix: @"//"])
		return _externalLinkEmoji;

	return _localLinkEmoji;
}

- (OFString *)g2h_CSSClassForIRIString: (OFString *)IRIString
{
	if ([IRIString containsString: @"://"] || [IRIString hasPrefix: @"//"])
		return @"gemtext-external-link";

	return @"gemtext-local-link";
}

- (OFString *)finishProcessing
{
	if (_preformatted)
		@throw [OFInvalidFormatException exception];

	objc_release(_lastReturnedString);

	if (_previousIsListItem)
		_lastReturnedString = @"</ul>";
	else if (_previousIsQuote)
		_lastReturnedString = @"</blockquote>";
	else
		_lastReturnedString = @"";

	_previousIsHeading = false;
	_previousIsListItem = false;
	_previousIsQuote = false;

	return _lastReturnedString;
}

G2HConverter *
g2h_converter_new(void)
{
	void *pool = objc_autoreleasePoolPush();
	G2HConverter *ret;

	@try {
		ret = [[G2HConverter alloc] init];
	} @catch (id e) {
		ret = nil;
	}

	objc_autoreleasePoolPop(pool);

	return ret;
}

void
g2h_converter_free(G2HConverter *converter)
{
	objc_release(converter);
}

bool
g2h_converter_set_image_extensions(G2HConverter *converter,
    const char *imageExtensions[])
{
	void *pool = objc_autoreleasePoolPush();

	@try {
		OFMutableSet *set = [OFMutableSet set];

		for (; *imageExtensions != NULL; imageExtensions++)
			[set addObject:
			    [OFString stringWithUTF8String: *imageExtensions]];

		[set makeImmutable];

		converter.imageExtensions = set;
	} @catch (id e) {
		objc_autoreleasePoolPop(pool);
		return false;
	}

	objc_autoreleasePoolPop(pool);

	return true;
}

bool g2h_converter_set_local_link_emoji(G2HConverter *converter,
    const char *localLinkEmoji)
{
	void *pool = objc_autoreleasePoolPush();

	@try {
		converter.localLinkEmoji =
		    [OFString stringWithUTF8String: localLinkEmoji];
	} @catch (id e) {
		objc_autoreleasePoolPop(pool);
		return false;
	}

	objc_autoreleasePoolPop(pool);

	return true;
}

bool g2h_converter_set_external_link_emoji(G2HConverter *converter,
    const char *externalLinkEmoji)
{
	void *pool = objc_autoreleasePoolPush();

	@try {
		converter.externalLinkEmoji =
		    [OFString stringWithUTF8String: externalLinkEmoji];
	} @catch (id e) {
		objc_autoreleasePoolPop(pool);
		return false;
	}

	objc_autoreleasePoolPop(pool);

	return true;
}

const char *
g2h_converter_process_line(G2HConverter *converter, const char *line)
{
	void *pool = objc_autoreleasePoolPush();

	@try {
		[converter processLine: [OFString stringWithUTF8String: line]];
	} @catch (id e) {
		objc_autoreleasePoolPop(pool);
		return NULL;
	}

	objc_autoreleasePoolPop(pool);

	return converter->_lastReturnedString.UTF8String;
}

const char *
g2h_converter_finish_processing(G2HConverter *converter)
{
	void *pool = objc_autoreleasePoolPush();

	@try {
		[converter finishProcessing];
	} @catch (id e) {
		objc_autoreleasePoolPop(pool);
		return NULL;
	}

	objc_autoreleasePoolPop(pool);

	return converter->_lastReturnedString.UTF8String;
}
@end
