/*
 * Copyright (c) 2008-2024 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/>.
 */

#include "config.h"

#import "OFINISection.h"
#import "OFINISection+Private.h"
#import "OFArray.h"
#import "OFCharacterSet.h"
#import "OFStream.h"
#import "OFString.h"

#import "OFInvalidArgumentException.h"
#import "OFInvalidFormatException.h"

@interface OFINISectionPair: OFObject
{
@public
	OFString *_key, *_value;
}
@end

@interface OFINISectionComment: OFObject
{
@public
	OFString *_comment;
}
@end

static OFCharacterSet *needsEscapeCharacterSet;

static OFString *
escapeString(OFString *string)
{
	OFMutableString *mutableString;

	if (![string hasPrefix: @" "] && ![string hasSuffix: @" "] &&
	    ![string hasPrefix: @"\t"] && ![string hasSuffix: @"\t"] &&
	    [string indexOfCharacterFromSet: needsEscapeCharacterSet] ==
	    OFNotFound)
		return string;

	mutableString = [[string mutableCopy] autorelease];

	[mutableString replaceOccurrencesOfString: @"\\" withString: @"\\\\"];
	[mutableString replaceOccurrencesOfString: @"\f" withString: @"\\f"];
	[mutableString replaceOccurrencesOfString: @"\r" withString: @"\\r"];
	[mutableString replaceOccurrencesOfString: @"\n" withString: @"\\n"];
	[mutableString replaceOccurrencesOfString: @"\"" withString: @"\\\""];

	[mutableString insertString: @"\"" atIndex: 0];
	[mutableString appendString: @"\""];

	[mutableString makeImmutable];

	return mutableString;
}

@implementation OFINISectionPair
- (void)dealloc
{
	[_key release];
	[_value release];

	[super dealloc];
}

- (OFString *)description
{
	return [OFString stringWithFormat: @"%@ = %@", _key, _value];
}
@end

@implementation OFINISectionComment
- (void)dealloc
{
	[_comment release];

	[super dealloc];
}

- (OFString *)description
{
	return [[_comment copy] autorelease];
}
@end

@implementation OFINISection
@synthesize name = _name;

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

	needsEscapeCharacterSet = [[OFCharacterSet alloc]
	    initWithCharactersInString: @"\r\n\f\"\\=;#"];
}

- (instancetype)of_initWithName: (OFString *)name OF_DIRECT
{
	self = [super init];

	@try {
		_name = [name copy];
		_lines = [[OFMutableArray alloc] init];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (void)dealloc
{
	[_name release];
	[_lines release];

	[super dealloc];
}

static void
parseQuoted(const char **cString, const char **start, size_t *length)
{
	bool inEscape = false;

	(*cString)++;
	*start = *cString;

	while (**cString != '\0') {
		if (inEscape)
			inEscape = false;
		else {
			if (**cString == '\\')
				inEscape = true;
			else if (**cString == '"')
				break;
		}
		(*cString)++;
	}
	if (**cString == '\0')
		@throw [OFInvalidFormatException exception];

	*length = *cString - *start;

	(*cString)++;

	while (OFASCIIIsSpace(**cString))
		(*cString)++;
}

static void
unescapeMutableString(OFMutableString *string)
{
	[string replaceOccurrencesOfString: @"\\f" withString: @"\f"];
	[string replaceOccurrencesOfString: @"\\r" withString: @"\r"];
	[string replaceOccurrencesOfString: @"\\n" withString: @"\n"];
	[string replaceOccurrencesOfString: @"\\\"" withString: @"\""];
	[string replaceOccurrencesOfString: @"\\\\" withString: @"\\"];
}

- (void)of_parseLine: (OFString *)line
{
	void *pool = objc_autoreleasePoolPush();
	const char *cString = line.UTF8String;
	bool keyIsQuoted = false, valueIsQuoted = false;
	const char *keyStart, *valueStart;
	size_t keyLength, valueLength;
	OFMutableString *key, *value;
	OFINISectionPair *pair;

	while (OFASCIIIsSpace(*cString))
		cString++;

	if (*cString == ';' || *cString == '#') {
		OFINISectionComment *comment =
		    [[[OFINISectionComment alloc] init] autorelease];
		comment->_comment = [line copy];
		[_lines addObject: comment];
		return;
	}

	if (*cString == '"') {
		keyIsQuoted = true;
		parseQuoted(&cString, &keyStart, &keyLength);
	} else {
		keyStart = cString;

		while (*cString != '=' && *cString != '\0')
			cString++;

		keyLength = cString - keyStart;
	}

	if (*cString != '=')
		@throw [OFInvalidFormatException exception];
	cString++;

	while (OFASCIIIsSpace(*cString))
		cString++;

	if (*cString == '"') {
		valueIsQuoted = true;
		parseQuoted(&cString, &valueStart, &valueLength);
	} else {
		valueStart = cString;

		while (*cString != '\0')
			cString++;

		valueLength = cString - valueStart;
	}

	while (*cString != '\0') {
		if (!OFASCIIIsSpace(*cString))
			@throw [OFInvalidFormatException exception];

		cString++;
	}

	key = [OFMutableString stringWithUTF8String: keyStart
					     length: keyLength];
	value = [OFMutableString stringWithUTF8String: valueStart
					       length: valueLength];

	if (keyIsQuoted)
		unescapeMutableString(key);
	else
		[key deleteEnclosingWhitespaces];
	if (valueIsQuoted)
		unescapeMutableString(value);
	else
		[value deleteEnclosingWhitespaces];

	[key makeImmutable];
	[value makeImmutable];

	pair = [[[OFINISectionPair alloc] init] autorelease];
	pair->_key = [key copy];
	pair->_value = [value copy];
	[_lines addObject: pair];

	objc_autoreleasePoolPop(pool);
}

- (OFString *)stringValueForKey: (OFString *)key
{
	return [self stringValueForKey: key defaultValue: nil];
}

- (OFString *)stringValueForKey: (OFString *)key
		   defaultValue: (OFString *)defaultValue
{
	for (id line in _lines) {
		OFINISectionPair *pair;

		if (![line isKindOfClass: [OFINISectionPair class]])
			continue;

		pair = line;

		if ([pair->_key isEqual: key])
			return [[pair->_value copy] autorelease];
	}

	return defaultValue;
}

- (long long)longLongValueForKey: (OFString *)key
		    defaultValue: (long long)defaultValue
{
	void *pool = objc_autoreleasePoolPush();
	OFString *value = [self stringValueForKey: key defaultValue: nil];
	long long ret;

	if (value != nil)
		ret = [value longLongValueWithBase: 0];
	else
		ret = defaultValue;

	objc_autoreleasePoolPop(pool);

	return ret;
}

- (bool)boolValueForKey: (OFString *)key defaultValue: (bool)defaultValue
{
	void *pool = objc_autoreleasePoolPush();
	OFString *value = [self stringValueForKey: key defaultValue: nil];
	bool ret;

	if (value != nil) {
		if ([value isEqual: @"true"])
			ret = true;
		else if ([value isEqual: @"false"])
			ret = false;
		else
			@throw [OFInvalidFormatException exception];
	} else
		ret = defaultValue;

	objc_autoreleasePoolPop(pool);

	return ret;
}

- (float)floatValueForKey: (OFString *)key defaultValue: (float)defaultValue
{
	void *pool = objc_autoreleasePoolPush();
	OFString *value = [self stringValueForKey: key defaultValue: nil];
	float ret;

	if (value != nil)
		ret = value.floatValue;
	else
		ret = defaultValue;

	objc_autoreleasePoolPop(pool);

	return ret;
}

- (double)doubleValueForKey: (OFString *)key defaultValue: (double)defaultValue
{
	void *pool = objc_autoreleasePoolPush();
	OFString *value = [self stringValueForKey: key defaultValue: nil];
	double ret;

	if (value != nil)
		ret = value.doubleValue;
	else
		ret = defaultValue;

	objc_autoreleasePoolPop(pool);

	return ret;
}

- (OFArray OF_GENERIC(OFString *) *)arrayValueForKey: (OFString *)key
{
	OFMutableArray *ret = [OFMutableArray array];
	void *pool = objc_autoreleasePoolPush();

	for (id line in _lines) {
		OFINISectionPair *pair;

		if (![line isKindOfClass: [OFINISectionPair class]])
			continue;

		pair = line;

		if ([pair->_key isEqual: key])
			[ret addObject: [[pair->_value copy] autorelease]];
	}

	objc_autoreleasePoolPop(pool);

	[ret makeImmutable];

	return ret;
}

- (void)setStringValue: (OFString *)string forKey: (OFString *)key
{
	void *pool = objc_autoreleasePoolPush();
	OFINISectionPair *pair;

	for (id line in _lines) {
		if (![line isKindOfClass: [OFINISectionPair class]])
			continue;

		pair = line;

		if ([pair->_key isEqual: key]) {
			OFString *old = pair->_value;
			pair->_value = [string copy];
			[old release];

			objc_autoreleasePoolPop(pool);

			return;
		}
	}

	pair = [[[OFINISectionPair alloc] init] autorelease];
	pair->_key = nil;
	pair->_value = nil;

	@try {
		pair->_key = [key copy];
		pair->_value = [string copy];
		[_lines addObject: pair];
	} @catch (id e) {
		[pair->_key release];
		[pair->_value release];

		@throw e;
	}

	objc_autoreleasePoolPop(pool);
}

- (void)setLongLongValue: (long long)longLongValue forKey: (OFString *)key
{
	void *pool = objc_autoreleasePoolPush();

	[self setStringValue: [OFString stringWithFormat:
				  @"%lld", longLongValue]
		      forKey: key];

	objc_autoreleasePoolPop(pool);
}

- (void)setBoolValue: (bool)boolValue forKey: (OFString *)key
{
	[self setStringValue: (boolValue ? @"true" : @"false") forKey: key];
}

- (void)setFloatValue: (float)floatValue forKey: (OFString *)key
{
	void *pool = objc_autoreleasePoolPush();

	[self setStringValue: [OFString stringWithFormat: @"%g", floatValue]
		      forKey: key];

	objc_autoreleasePoolPop(pool);
}

- (void)setDoubleValue: (double)doubleValue forKey: (OFString *)key
{
	void *pool = objc_autoreleasePoolPush();

	[self setStringValue: [OFString stringWithFormat: @"%g", doubleValue]
		      forKey: key];

	objc_autoreleasePoolPop(pool);
}

- (void)setArrayValue: (OFArray OF_GENERIC(OFString *) *)arrayValue
	       forKey: (OFString *)key
{
	void *pool;
	OFMutableArray *pairs;
	id const *lines;
	size_t count;
	bool replaced;

	if (arrayValue.count == 0) {
		[self removeValueForKey: key];
		return;
	}

	pool = objc_autoreleasePoolPush();

	pairs = [OFMutableArray arrayWithCapacity: arrayValue.count];

	for (OFString *string in arrayValue) {
		OFINISectionPair *pair;

		if (![string isKindOfClass: [OFString class]])
			@throw [OFInvalidArgumentException exception];

		pair = [[[OFINISectionPair alloc] init] autorelease];
		pair->_key = [key copy];
		pair->_value = [string copy];

		[pairs addObject: pair];
	}

	lines = _lines.objects;
	count = _lines.count;
	replaced = false;

	for (size_t i = 0; i < count; i++) {
		OFINISectionPair *pair;

		if (![lines[i] isKindOfClass: [OFINISectionPair class]])
			continue;

		pair = lines[i];

		if ([pair->_key isEqual: key]) {
			[_lines removeObjectAtIndex: i];

			if (!replaced) {
				[_lines insertObjectsFromArray: pairs
						       atIndex: i];

				replaced = true;
				/* Continue after inserted pairs */
				i += arrayValue.count - 1;
			} else
				i--;	/* Continue at same position */

			lines = _lines.objects;
			count = _lines.count;

			continue;
		}
	}

	if (!replaced)
		[_lines addObjectsFromArray: pairs];

	objc_autoreleasePoolPop(pool);
}

- (void)removeValueForKey: (OFString *)key
{
	void *pool = objc_autoreleasePoolPush();
	id const *lines = _lines.objects;
	size_t count = _lines.count;

	for (size_t i = 0; i < count; i++) {
		OFINISectionPair *pair;

		if (![lines[i] isKindOfClass: [OFINISectionPair class]])
			continue;

		pair = lines[i];

		if ([pair->_key isEqual: key]) {
			[_lines removeObjectAtIndex: i];

			lines = _lines.objects;
			count = _lines.count;

			i--;	/* Continue at same position */
			continue;
		}
	}

	objc_autoreleasePoolPop(pool);
}

- (bool)of_writeToStream: (OFStream *)stream
		encoding: (OFStringEncoding)encoding
		   first: (bool)first
{
	if (_lines.count == 0)
		return false;

	if (_name.length > 0) {
		if (first)
			[stream writeFormat: @"[%@]\r\n", _name];
		else
			[stream writeFormat: @"\r\n[%@]\r\n", _name];
	}

	for (id line in _lines) {
		if ([line isKindOfClass: [OFINISectionComment class]]) {
			OFINISectionComment *comment = line;
			[stream writeFormat: @"%@\r\n", comment->_comment];
		} else if ([line isKindOfClass: [OFINISectionPair class]]) {
			OFINISectionPair *pair = line;
			OFString *key = escapeString(pair->_key);
			OFString *value = escapeString(pair->_value);
			OFString *tmp = [OFString
			    stringWithFormat: @"%@=%@\r\n", key, value];
			[stream writeString: tmp encoding: encoding];
		} else
			@throw [OFInvalidArgumentException exception];
	}

	return true;
}

- (OFString *)description
{
	return [OFString stringWithFormat: @"<%@ \"%@\": %@>",
					   self.class, _name, _lines];
}
@end
