/*
 * 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 "OFOptionsParser.h"
#import "OFApplication.h"
#import "OFArray.h"
#import "OFMapTable.h"

#import "OFInvalidArgumentException.h"

static unsigned long
stringHash(void *object)
{
	return ((OFString *)object).hash;
}

static bool
stringEqual(void *object1, void *object2)
{
	return [(OFString *)object1 isEqual: (OFString *)object2];
}

@implementation OFOptionsParser
@synthesize lastOption = _lastOption, lastLongOption = _lastLongOption;
@synthesize argument = _argument;

+ (instancetype)parserWithOptions: (const OFOptionsParserOption *)options
{
	return [[[self alloc] initWithOptions: options] autorelease];
}

- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)initWithOptions: (const OFOptionsParserOption *)options
{
	self = [super init];

	@try {
		size_t count = 0;
		const OFOptionsParserOption *iter;
		OFOptionsParserOption *iter2;
		const OFMapTableFunctions keyFunctions = {
			.hash = stringHash,
			.equal = stringEqual
		};
		const OFMapTableFunctions objectFunctions = { NULL };

		/* Count, sanity check, initialize pointers */
		for (iter = options;
		    iter->shortOption != '\0' || iter->longOption != nil;
		    iter++) {
			if (iter->hasArgument < -1 || iter->hasArgument > 1)
				@throw [OFInvalidArgumentException exception];

			if (iter->shortOption != '\0' &&
			    iter->hasArgument == -1)
				@throw [OFInvalidArgumentException exception];

			if (iter->hasArgument == 0 && iter->argumentPtr != NULL)
				@throw [OFInvalidArgumentException exception];

			if (iter->isSpecifiedPtr)
				*iter->isSpecifiedPtr = false;
			if (iter->argumentPtr)
				*iter->argumentPtr = nil;

			count++;
		}

		_options = OFAllocMemory(count + 1, sizeof(*_options));
		_longOptions = [[OFMapTable alloc]
		    initWithKeyFunctions: keyFunctions
			 objectFunctions: objectFunctions];

		for (iter = options, iter2 = _options;
		    iter->shortOption != '\0' || iter->longOption != nil;
		    iter++, iter2++) {
			iter2->shortOption = iter->shortOption;
			iter2->longOption = nil;
			iter2->hasArgument = iter->hasArgument;
			iter2->isSpecifiedPtr = iter->isSpecifiedPtr;
			iter2->argumentPtr = iter->argumentPtr;

			if (iter->longOption != nil) {
				@try {
					iter2->longOption =
					    [iter->longOption copy];

					if ([_longOptions objectForKey:
					    iter2->longOption] != NULL)
						@throw
						    [OFInvalidArgumentException
						    exception];

					[_longOptions
					    setObject: iter2
					       forKey: iter2->longOption];
				} @catch (id e) {
					/*
					 * Make sure we are in a consistent
					 * state where dealloc works.
					 */
					[iter2->longOption release];

					iter2->shortOption = '\0';
					iter2->longOption = nil;

					@throw e;
				}
			}
		}
		iter2->shortOption = '\0';
		iter2->longOption = nil;

		_arguments = [[OFApplication arguments] retain];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	if (_options != NULL)
		for (OFOptionsParserOption *iter = _options;
		    iter->shortOption != '\0' || iter->longOption != nil;
		    iter++)
			[iter->longOption release];

	OFFreeMemory(_options);
	[_longOptions release];

	[_arguments release];
	[_argument release];

	[super dealloc];
}

- (OFUnichar)nextOption
{
	OFOptionsParserOption *iter;
	OFString *argument;

	if (_done || _index >= _arguments.count)
		return '\0';

	[_lastLongOption release];
	[_argument release];
	_lastLongOption = nil;
	_argument = nil;

	argument = [_arguments objectAtIndex: _index];

	if (_subIndex == 0) {
		if (argument.length < 2 ||
		    [argument characterAtIndex: 0] != '-') {
			_done = true;
			return '\0';
		}

		if ([argument isEqual: @"--"]) {
			_done = true;
			_index++;
			return '\0';
		}

		if ([argument hasPrefix: @"--"]) {
			void *pool = objc_autoreleasePoolPush();
			size_t pos;
			OFOptionsParserOption *option;

			_lastOption = '-';
			_index++;

			if ((pos = [argument rangeOfString: @"="].location) !=
			    OFNotFound)
				_argument = [[argument
				    substringFromIndex: pos + 1] copy];
			else
				pos = argument.length;

			_lastLongOption = [[argument substringWithRange:
			    OFMakeRange(2, pos - 2)] copy];

			objc_autoreleasePoolPop(pool);

			option = [_longOptions objectForKey: _lastLongOption];
			if (option == NULL)
				return '?';

			if (option->hasArgument == 1 && _argument == nil)
				return ':';
			if (option->hasArgument == 0 && _argument != nil)
				return '=';

			if (option->isSpecifiedPtr != NULL)
				*option->isSpecifiedPtr = true;
			if (option->argumentPtr != NULL)
				*option->argumentPtr =
				    [[_argument copy] autorelease];

			if (option->shortOption != '\0')
				_lastOption = option->shortOption;

			return _lastOption;
		}

		_subIndex = 1;
	}

	_lastOption = [argument characterAtIndex: _subIndex++];

	if (_subIndex >= argument.length) {
		_index++;
		_subIndex = 0;
	}

	for (iter = _options;
	    iter->shortOption != '\0' || iter->longOption != nil; iter++) {
		if (iter->shortOption == _lastOption) {
			if (iter->hasArgument == 0) {
				if (iter->isSpecifiedPtr != NULL)
					*iter->isSpecifiedPtr = true;

				return _lastOption;
			}

			if (_index >= _arguments.count)
				return ':';

			argument = [_arguments objectAtIndex: _index];
			argument = [argument substringFromIndex: _subIndex];

			_argument = [argument copy];

			if (iter->isSpecifiedPtr != NULL)
				*iter->isSpecifiedPtr = true;
			if (iter->argumentPtr != NULL)
				*iter->argumentPtr =
				    [[_argument copy] autorelease];

			_index++;
			_subIndex = 0;

			return _lastOption;
		}
	}

	return '?';
}

- (OFArray *)remainingArguments
{
	return [_arguments objectsInRange:
	    OFMakeRange(_index, _arguments.count - _index)];
}
@end
