/*
 * 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"

#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#import "OFBlock.h"
#ifdef OF_HAVE_ATOMIC_OPS
# import "OFAtomic.h"
#endif
#ifdef OF_HAVE_THREADS
# import "OFPlainMutex.h"
#endif
#import "OFString.h"

#import "OFAllocFailedException.h"
#import "OFInitializationFailedException.h"

#if defined(OF_OBJFW_RUNTIME)
# import "runtime/private.h"
#endif

struct Block {
	Class isa;
	int flags;
	int reserved;
	void (*invoke)(void *block, ...);
	struct {
		unsigned long reserved;
		unsigned long size;
		void (*_Nullable copyHelper)(void *dest, void *src);
		void (*_Nullable disposeHelper)(void *src);
		const char *signature;
	} *descriptor;
};

struct Byref {
	Class isa;
	struct Byref *forwarding;
	int flags;
	int size;
	void (*keepByref)(void *dest, void *src);
	void (*disposeByref)(void *);
};

enum {
	OFBlockHasCopyDispose = (1 << 25),
	OFBlockHasCtor	      = (1 << 26),
	OFBlockIsGlobal	      = (1 << 28),
	OFBlockHasStret	      = (1 << 29),
	OFBlockHasSignature   = (1 << 30)
};
#define OFBlockRefCountMask 0xFFFF

enum {
	OFBlockFieldIsObject =   3,
	OFBlockFieldIsBlock  =   7,
	OFBlockFieldIsByref  =   8,
	OFBlockFieldIsWeak   =  16,
	OFBlockByrefCaller   = 128
};

@protocol RetainRelease
- (instancetype)retain;
- (void)release;
@end

#ifdef OF_OBJFW_RUNTIME
/* Begin of ObjC module */
static struct objc_class _NSConcreteStackBlock_metaclass = {
	Nil, Nil, "OFStackBlock", 8, OBJC_CLASS_INFO_METACLASS,
	sizeof(_NSConcreteStackBlock_metaclass), NULL, NULL
};

struct objc_class _NSConcreteStackBlock = {
	&_NSConcreteStackBlock_metaclass, (Class)(void *)"OFBlock",
	"OFStackBlock", 8, OBJC_CLASS_INFO_CLASS, sizeof(struct Block),
	NULL, NULL
};

static struct objc_class _NSConcreteGlobalBlock_metaclass = {
	Nil, Nil, "OFGlobalBlock", 8, OBJC_CLASS_INFO_METACLASS,
	sizeof(_NSConcreteGlobalBlock_metaclass), NULL, NULL
};

struct objc_class _NSConcreteGlobalBlock = {
	&_NSConcreteGlobalBlock_metaclass, (Class)(void *)"OFBlock",
	"OFGlobalBlock", 8, OBJC_CLASS_INFO_CLASS, sizeof(struct Block),
	NULL, NULL
};

static struct objc_class _NSConcreteMallocBlock_metaclass = {
	Nil, Nil, "OFMallocBlock", 8, OBJC_CLASS_INFO_METACLASS,
	sizeof(_NSConcreteMallocBlock_metaclass), NULL, NULL
};

struct objc_class _NSConcreteMallocBlock = {
	&_NSConcreteMallocBlock_metaclass, (Class)(void *)"OFBlock",
	"OFMallocBlock", 8, OBJC_CLASS_INFO_CLASS, sizeof(struct Block),
	NULL, NULL
};

static struct {
	unsigned long unknown;
	struct objc_selector *selectorRefs;
	uint16_t classDefsCount, categoryDefsCount;
	void *defs[4];
} symtab = {
	0, NULL, 3, 0,
	{
		&_NSConcreteStackBlock, &_NSConcreteGlobalBlock,
		&_NSConcreteMallocBlock, NULL
	}
};

static struct _objc_module module = {
	8, sizeof(module), NULL, (struct objc_symtab *)&symtab
};

OF_CONSTRUCTOR()
{
	__objc_exec_class(&module);
}
/* End of ObjC module */
#elif defined(OF_APPLE_RUNTIME)
extern Class objc_initializeClassPair(Class, const char *, Class, Class);

struct class {
	struct class *isa, *superclass;
	const char *name;
	long version, info, instanceSize;
	struct ivar_list *iVars;
	struct method_list **methodList;
	struct cache *cache;
	struct protocol_list *protocols;
	const char *iVarLayout;
	struct class_ext *ext;
};

struct class _NSConcreteStackBlock;
struct class _NSConcreteGlobalBlock;
struct class _NSConcreteMallocBlock;
# if defined(__OBJC2__) && !defined(OF_POWERPC64)
struct class _NSConcreteStackBlock_metaclass;
struct class _NSConcreteGlobalBlock_metaclass;
struct class _NSConcreteMallocBlock_metaclass;
# endif
#endif

static struct {
	Class isa;
} allocFailedException;

#ifndef OF_HAVE_ATOMIC_OPS
# define numSpinlocks 8	/* needs to be a power of 2 */
# define SPINLOCK_HASH(p) ((uintptr_t)p >> 4) & (numSpinlocks - 1)
static OFSpinlock blockSpinlocks[numSpinlocks];
static OFSpinlock byrefSpinlocks[numSpinlocks];
#endif

void *
_Block_copy(const void *block_)
{
	struct Block *block = (struct Block *)block_;

	if ([(id)block isMemberOfClass: (Class)&_NSConcreteStackBlock]) {
		struct Block *copy;

		if ((copy = malloc(block->descriptor->size)) == NULL) {
			object_setClass((id)&allocFailedException,
			    [OFAllocFailedException class]);
			@throw (OFAllocFailedException *)&allocFailedException;
		}
		memcpy(copy, block, block->descriptor->size);

		object_setClass((id)copy, (Class)&_NSConcreteMallocBlock);
		copy->flags++;

		if (block->flags & OFBlockHasCopyDispose)
			block->descriptor->copyHelper(copy, block);

		return copy;
	}

	if ([(id)block isMemberOfClass: (Class)&_NSConcreteMallocBlock]) {
#ifdef OF_HAVE_ATOMIC_OPS
		OFAtomicIntIncrease(&block->flags);
#else
		unsigned hash = SPINLOCK_HASH(block);

		OFEnsure(OFSpinlockLock(&blockSpinlocks[hash]) == 0);
		block->flags++;
		OFEnsure(OFSpinlockUnlock(&blockSpinlocks[hash]) == 0);
#endif
	}

	return block;
}

void
_Block_release(const void *block_)
{
	struct Block *block = (struct Block *)block_;

	if (object_getClass((id)block) != (Class)&_NSConcreteMallocBlock)
		return;

#ifdef OF_HAVE_ATOMIC_OPS
	if ((OFAtomicIntDecrease(&block->flags) & OFBlockRefCountMask) == 0) {
		if (block->flags & OFBlockHasCopyDispose)
			block->descriptor->disposeHelper(block);

		free(block);
	}
#else
	unsigned hash = SPINLOCK_HASH(block);

	OFEnsure(OFSpinlockLock(&blockSpinlocks[hash]) == 0);
	if ((--block->flags & OFBlockRefCountMask) == 0) {
		OFEnsure(OFSpinlockUnlock(&blockSpinlocks[hash]) == 0);

		if (block->flags & OFBlockHasCopyDispose)
			block->descriptor->disposeHelper(block);

		free(block);

		return;
	}
	OFEnsure(OFSpinlockUnlock(&blockSpinlocks[hash]) == 0);
#endif
}

void
_Block_object_assign(void *dst_, const void *src_, const int flags_)
{
	int flags = flags_ & (OFBlockFieldIsBlock | OFBlockFieldIsObject |
	    OFBlockFieldIsByref);

	if (src_ == NULL)
		return;

	switch (flags) {
	case OFBlockFieldIsBlock:
		*(struct Block **)dst_ = _Block_copy(src_);
		break;
	case OFBlockFieldIsObject:
		if (!(flags_ & OFBlockByrefCaller))
			*(id *)dst_ = [(id)src_ retain];
		break;
	case OFBlockFieldIsByref:;
		struct Byref *src = (struct Byref *)src_;
		struct Byref **dst = (struct Byref **)dst_;

		src = src->forwarding;

		if ((src->flags & OFBlockRefCountMask) == 0) {
			if ((*dst = malloc(src->size)) == NULL) {
				object_setClass((id)&allocFailedException,
				    [OFAllocFailedException class]);
				@throw (OFAllocFailedException *)
				    &allocFailedException;
			}

			memcpy(*dst, src, src->size);
			(*dst)->flags =
			    ((*dst)->flags & ~OFBlockRefCountMask) | 1;
			(*dst)->forwarding = *dst;

			if (src->flags & OFBlockHasCopyDispose)
				src->keepByref(*dst, src);

#ifdef OF_HAVE_ATOMIC_OPS
			if (!OFAtomicPointerCompareAndSwap(
			    (void **)&src->forwarding, src, *dst)) {
				src->disposeByref(*dst);
				free(*dst);

				*dst = src->forwarding;
			}
#else
			unsigned hash = SPINLOCK_HASH(src);

			OFEnsure(OFSpinlockLock(&byrefSpinlocks[hash]) == 0);
			if (src->forwarding == src)
				src->forwarding = *dst;
			else {
				src->disposeByref(*dst);
				free(*dst);

				*dst = src->forwarding;
			}
			OFEnsure(OFSpinlockUnlock(&byrefSpinlocks[hash]) == 0);
#endif
		} else
			*dst = src;

#ifdef OF_HAVE_ATOMIC_OPS
		OFAtomicIntIncrease(&(*dst)->flags);
#else
		unsigned hash = SPINLOCK_HASH(*dst);

		OFEnsure(OFSpinlockLock(&byrefSpinlocks[hash]) == 0);
		(*dst)->flags++;
		OFEnsure(OFSpinlockUnlock(&byrefSpinlocks[hash]) == 0);
#endif
		break;
	}
}

void
_Block_object_dispose(const void *object_, const int flags_)
{
	const int flags = flags_ & (OFBlockFieldIsBlock | OFBlockFieldIsObject |
	    OFBlockFieldIsByref);

	if (object_ == NULL)
		return;

	switch (flags) {
	case OFBlockFieldIsBlock:
		_Block_release(object_);
		break;
	case OFBlockFieldIsObject:
		if (!(flags_ & OFBlockByrefCaller))
			[(id)object_ release];
		break;
	case OFBlockFieldIsByref:;
		struct Byref *object = (struct Byref *)object_;

		object = object->forwarding;

#ifdef OF_HAVE_ATOMIC_OPS
		if ((OFAtomicIntDecrease(&object->flags) &
		    OFBlockRefCountMask) == 0) {
			if (object->flags & OFBlockHasCopyDispose)
				object->disposeByref(object);

			free(object);
		}
#else
		unsigned hash = SPINLOCK_HASH(object);

		OFEnsure(OFSpinlockLock(&byrefSpinlocks[hash]) == 0);
		if ((--object->flags & OFBlockRefCountMask) == 0) {
			OFEnsure(OFSpinlockUnlock(&byrefSpinlocks[hash]) == 0);

			if (object->flags & OFBlockHasCopyDispose)
				object->disposeByref(object);

			free(object);
		}
		OFEnsure(OFSpinlockUnlock(&byrefSpinlocks[hash]) == 0);
#endif
		break;
	}
}

@implementation OFBlock
+ (void)load
{
#ifndef OF_HAVE_ATOMIC_OPS
	for (size_t i = 0; i < numSpinlocks; i++)
		if (OFSpinlockNew(&blockSpinlocks[i]) != 0 ||
		    OFSpinlockNew(&byrefSpinlocks[i]) != 0)
			@throw [OFInitializationFailedException
			    exceptionWithClass: self];
#endif

#ifdef OF_APPLE_RUNTIME
	Class tmp;

# if defined(__OBJC2__) && !defined(OF_POWERPC64)
	tmp = objc_initializeClassPair(self, "OFStackBlock",
	    (Class)&_NSConcreteStackBlock,
	    (Class)&_NSConcreteStackBlock_metaclass);
	if (tmp == Nil)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
	objc_registerClassPair(tmp);

	tmp = objc_initializeClassPair(self, "OFGlobalBlock",
	    (Class)&_NSConcreteGlobalBlock,
	    (Class)&_NSConcreteGlobalBlock_metaclass);
	if (tmp == Nil)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
	objc_registerClassPair(tmp);

	tmp = objc_initializeClassPair([OFBlock class], "OFMallocBlock",
	    (Class)&_NSConcreteMallocBlock,
	    (Class)&_NSConcreteMallocBlock_metaclass);
	if (tmp == Nil)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
	objc_registerClassPair(tmp);
# else
	/*
	 * There is no objc_initializeClassPair in 10.5.
	 * However, objc_allocateClassPair does not register the new class with
	 * the subclass in the ObjC1 runtime like the ObjC2 runtime does, so
	 * this workaround should be fine.
	 */
	if ((tmp = objc_allocateClassPair(self, "OFStackBlock", 0)) == NULL)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
	memcpy(&_NSConcreteStackBlock, tmp, sizeof(_NSConcreteStackBlock));
	free(tmp);
	objc_registerClassPair((Class)&_NSConcreteStackBlock);

	if ((tmp = objc_allocateClassPair(self, "OFGlobalBlock", 0)) == NULL)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
	memcpy(&_NSConcreteGlobalBlock, tmp, sizeof(_NSConcreteGlobalBlock));
	free(tmp);
	objc_registerClassPair((Class)&_NSConcreteGlobalBlock);

	if ((tmp = objc_allocateClassPair(self, "OFMallocBlock", 0)) == NULL)
		@throw [OFInitializationFailedException
		    exceptionWithClass: self];
	memcpy(&_NSConcreteMallocBlock, tmp, sizeof(_NSConcreteMallocBlock));
	free(tmp);
	objc_registerClassPair((Class)&_NSConcreteMallocBlock);
# endif
#endif
}

+ (instancetype)alloc
{
	OF_UNRECOGNIZED_SELECTOR
}

- (instancetype)init
{
	OF_INVALID_INIT_METHOD
}

- (instancetype)retain
{
	if ([self isMemberOfClass: (Class)&_NSConcreteMallocBlock])
		return Block_copy(self);

	return self;
}

- (id)copy
{
	return Block_copy(self);
}

- (instancetype)autorelease
{
	if ([self isMemberOfClass: (Class)&_NSConcreteMallocBlock])
		return [super autorelease];

	return self;
}

- (unsigned int)retainCount
{
	if ([self isMemberOfClass: (Class)&_NSConcreteMallocBlock])
		return ((struct Block *)self)->flags & OFBlockRefCountMask;

	return OFMaxRetainCount;
}

- (void)release
{
	if ([self isMemberOfClass: (Class)&_NSConcreteMallocBlock])
		Block_release(self);
}

- (void)dealloc
{
	OF_DEALLOC_UNSUPPORTED
}
@end
