/*
 * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017,
 *               2018, 2019, 2020
 *   Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This file is part of ObjFW. It may be distributed under the terms of the
 * Q Public License 1.0, which can be found in the file LICENSE.QPL included in
 * the packaging of this file.
 *
 * Alternatively, it may be distributed under the terms of the GNU General
 * Public License, either version 2 or 3, which can be found in the file
 * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this
 * file.
 */

#include "config.h"

#import "OFMutableURL.h"
#import "OFArray.h"
#ifdef OF_HAVE_FILES
# import "OFFileManager.h"
#endif
#import "OFNumber.h"
#import "OFString.h"

#import "OFInvalidFormatException.h"

extern void of_url_verify_escaped(OFString *, OFCharacterSet *);

@implementation OFMutableURL
@dynamic scheme, URLEncodedScheme, host, URLEncodedHost, port, user;
@dynamic URLEncodedUser, password, URLEncodedPassword, path, URLEncodedPath;
@dynamic pathComponents, query, URLEncodedQuery, queryDictionary, fragment;
@dynamic URLEncodedFragment;

+ (instancetype)URL
{
	return [[[self alloc] init] autorelease];
}

- (void)setScheme: (OFString *)scheme
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedScheme;

	_URLEncodedScheme = [[scheme stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLSchemeAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedScheme: (OFString *)URLEncodedScheme
{
	OFString *old;

	if (URLEncodedScheme != nil)
		of_url_verify_escaped(URLEncodedScheme,
		    [OFCharacterSet URLSchemeAllowedCharacterSet]);

	old = _URLEncodedScheme;
	_URLEncodedScheme = [URLEncodedScheme copy];
	[old release];
}

- (void)setHost: (OFString *)host
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedHost;

	if (of_url_is_ipv6_host(host))
		_URLEncodedHost = [[OFString alloc]
		    initWithFormat: @"[%@]", host];
	else
		_URLEncodedHost = [[host
		    stringByURLEncodingWithAllowedCharacters:
		    [OFCharacterSet URLHostAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedHost: (OFString *)URLEncodedHost
{
	OFString *old;

	if ([URLEncodedHost hasPrefix: @"["] &&
	    [URLEncodedHost hasSuffix: @"]"]) {
		if (!of_url_is_ipv6_host([URLEncodedHost substringWithRange:
		    of_range(1, URLEncodedHost.length - 2)]))
			@throw [OFInvalidFormatException exception];
	} else if (URLEncodedHost != nil)
		of_url_verify_escaped(URLEncodedHost,
		    [OFCharacterSet URLHostAllowedCharacterSet]);

	old = _URLEncodedHost;
	_URLEncodedHost = [URLEncodedHost copy];
	[old release];
}

- (void)setPort: (OFNumber *)port
{
	OFNumber *old = _port;
	_port = [port copy];
	[old release];
}

- (void)setUser: (OFString *)user
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedUser;

	_URLEncodedUser = [[user stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLUserAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedUser: (OFString *)URLEncodedUser
{
	OFString *old;

	if (URLEncodedUser != nil)
		of_url_verify_escaped(URLEncodedUser,
		    [OFCharacterSet URLUserAllowedCharacterSet]);

	old = _URLEncodedUser;
	_URLEncodedUser = [URLEncodedUser copy];
	[old release];
}

- (void)setPassword: (OFString *)password
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedPassword;

	_URLEncodedPassword = [[password
	    stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLPasswordAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedPassword: (OFString *)URLEncodedPassword
{
	OFString *old;

	if (URLEncodedPassword != nil)
		of_url_verify_escaped(URLEncodedPassword,
		    [OFCharacterSet URLPasswordAllowedCharacterSet]);

	old = _URLEncodedPassword;
	_URLEncodedPassword = [URLEncodedPassword copy];
	[old release];
}

- (void)setPath: (OFString *)path
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedPath;

	_URLEncodedPath = [[path stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLPathAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedPath: (OFString *)URLEncodedPath
{
	OFString *old;

	if (URLEncodedPath != nil)
		of_url_verify_escaped(URLEncodedPath,
		    [OFCharacterSet URLPathAllowedCharacterSet]);

	old = _URLEncodedPath;
	_URLEncodedPath = [URLEncodedPath copy];
	[old release];
}

- (void)setPathComponents: (OFArray *)components
{
	void *pool = objc_autoreleasePoolPush();

	if (components == nil) {
		self.path = nil;
		return;
	}

	if (components.count == 0)
		@throw [OFInvalidFormatException exception];

	if ([components.firstObject length] != 0)
		@throw [OFInvalidFormatException exception];

	self.path = [components componentsJoinedByString: @"/"];

	objc_autoreleasePoolPop(pool);
}

- (void)setQuery: (OFString *)query
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedQuery;

	_URLEncodedQuery = [[query stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLQueryAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedQuery: (OFString *)URLEncodedQuery
{
	OFString *old;

	if (URLEncodedQuery != nil)
		of_url_verify_escaped(URLEncodedQuery,
		    [OFCharacterSet URLQueryAllowedCharacterSet]);

	old = _URLEncodedQuery;
	_URLEncodedQuery = [URLEncodedQuery copy];
	[old release];
}

- (void)setQueryDictionary:
    (OFDictionary OF_GENERIC(OFString *, OFString *) *)dictionary
{
	void *pool = objc_autoreleasePoolPush();
	OFMutableString *URLEncodedQuery = [OFMutableString string];
	OFEnumerator *keyEnumerator = [dictionary keyEnumerator];
	OFEnumerator *objectEnumerator = [dictionary objectEnumerator];
	OFCharacterSet *characterSet =
	    [OFCharacterSet URLQueryKeyValueAllowedCharacterSet];
	OFString *key, *object, *old;

	while ((key = [keyEnumerator nextObject]) != nil &&
	    (object = [objectEnumerator nextObject]) != nil) {
		key = [key
		    stringByURLEncodingWithAllowedCharacters: characterSet];
		object = [object
		    stringByURLEncodingWithAllowedCharacters: characterSet];

		if (URLEncodedQuery.length > 0)
			[URLEncodedQuery appendString: @"&"];

		[URLEncodedQuery appendFormat: @"%@=%@", key, object];
	}

	old = _URLEncodedQuery;
	_URLEncodedQuery = [URLEncodedQuery copy];
	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setFragment: (OFString *)fragment
{
	void *pool = objc_autoreleasePoolPush();
	OFString *old = _URLEncodedFragment;

	_URLEncodedFragment = [[fragment
	    stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLFragmentAllowedCharacterSet]] copy];

	[old release];

	objc_autoreleasePoolPop(pool);
}

- (void)setURLEncodedFragment: (OFString *)URLEncodedFragment
{
	OFString *old;

	if (URLEncodedFragment != nil)
		of_url_verify_escaped(URLEncodedFragment,
		    [OFCharacterSet URLFragmentAllowedCharacterSet]);

	old = _URLEncodedFragment;
	_URLEncodedFragment = [URLEncodedFragment copy];
	[old release];
}

- (id)copy
{
	OFMutableURL *copy = [self mutableCopy];

	[copy makeImmutable];

	return copy;
}

- (void)appendPathComponent: (OFString *)component
{
	[self appendPathComponent: component
		      isDirectory: false];

#ifdef OF_HAVE_FILES
	if ([_URLEncodedScheme isEqual: @"file"] &&
	    ![_URLEncodedPath hasSuffix: @"/"] &&
	    [[OFFileManager defaultManager] directoryExistsAtURL: self]) {
		void *pool = objc_autoreleasePoolPush();
		OFString *path = [_URLEncodedPath
		    stringByAppendingString: @"/"];

		[_URLEncodedPath release];
		_URLEncodedPath = [path retain];

		objc_autoreleasePoolPop(pool);
	}
#endif
}

- (void)appendPathComponent: (OFString *)component
		isDirectory: (bool)isDirectory
{
	void *pool;
	OFString *path;

	if ([component isEqual: @"/"] && [_URLEncodedPath hasSuffix: @"/"])
		return;

	pool = objc_autoreleasePoolPush();
	component = [component stringByURLEncodingWithAllowedCharacters:
	    [OFCharacterSet URLPathAllowedCharacterSet]];

#if defined(OF_WINDOWS) || defined(OF_MSDOS)
	if ([_URLEncodedPath hasSuffix: @"/"] ||
	    ([_URLEncodedScheme isEqual: @"file"] &&
	    [_URLEncodedPath hasSuffix: @":"]))
#else
	if ([_URLEncodedPath hasSuffix: @"/"])
#endif
		path = [_URLEncodedPath stringByAppendingString: component];
	else
		path = [_URLEncodedPath
		    stringByAppendingFormat: @"/%@", component];

	if (isDirectory && ![path hasSuffix: @"/"])
		path = [path stringByAppendingString: @"/"];

	[_URLEncodedPath release];
	_URLEncodedPath = [path retain];

	objc_autoreleasePoolPop(pool);
}

- (void)standardizePath
{
	void *pool;
	OFMutableArray OF_GENERIC(OFString *) *array;
	bool done = false, endsWithEmpty;
	OFString *path;

	if (_URLEncodedPath == nil)
		return;

	pool = objc_autoreleasePoolPush();

	array = [[[_URLEncodedPath
	    componentsSeparatedByString: @"/"] mutableCopy] autorelease];

	if ([array.firstObject length] != 0)
		@throw [OFInvalidFormatException exception];

	endsWithEmpty = ([array.lastObject length] == 0);

	while (!done) {
		size_t length = array.count;

		done = true;

		for (size_t i = 0; i < length; i++) {
			OFString *current = [array objectAtIndex: i];
			OFString *parent =
			    (i > 0 ? [array objectAtIndex: i - 1] : nil);

			if ([current isEqual: @"."] || current.length == 0) {
				[array removeObjectAtIndex: i];

				done = false;
				break;
			}

			if ([current isEqual: @".."] && parent != nil &&
			    ![parent isEqual: @".."]) {
				[array removeObjectsInRange:
				    of_range(i - 1, 2)];

				done = false;
				break;
			}
		}
	}

	[array insertObject: @""
		    atIndex: 0];
	if (endsWithEmpty)
		[array addObject: @""];

	path = [array componentsJoinedByString: @"/"];
	if (path.length == 0)
		path = @"/";

	[self setURLEncodedPath: path];

	objc_autoreleasePoolPop(pool);
}

- (void)makeImmutable
{
	object_setClass(self, [OFURL class]);
}
@end
