/*
 * Copyright (c) 2001-2023 Julien Nadeau Carriere <vedge@csoft.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 * The base class of the Agar object system.
 */

#include <agar/core/core.h>
#include <agar/core/config.h>

#include <stdarg.h>
#include <ctype.h>
#include <string.h>

/* Expensive debugging output related to AG_Object VFS operations. */
/* #define DEBUG_OBJECT */

/* Extra debugging output related to serialization. */
/* #define DEBUG_SERIALIZATION */

/* Debug class registration and module linking */
/* #define AG_DEBUG_CLASSES */

AG_ObjectClass agObjectClass = {
	"AG_Object",
	sizeof(AG_Object),
	{ 7,3, AGC_OBJECT, 0xE027 },
	NULL,  /* init */
	NULL,  /* reset */
	NULL,  /* destroy */
	NULL,  /* load */
	NULL,  /* save */
	NULL   /* edit */
};

Uint32 agObjectSignature = 0;           /* Object validity signature */
Uint32 agNonObjectSignature = 0;        /* Non-Object validity signature */

#ifdef AG_SERIALIZATION
int agObjectIgnoreUnknownObjs = 0;      /* Don't fail on unknown object types. */
int agObjectBackups = 1;	        /* Backup object save files. */
#endif

#ifdef AG_THREADS
AG_Mutex         agClassLock;           /* Lock on class tables */
#endif
AG_ObjectClass **agClasses;		/* Class table (flat array) */
Uint             agClassCount = 0;
AG_Tbl          *agClassTbl = NULL;	/* Class table (hash) */
#ifdef AG_NAMESPACES
AG_Namespace *agNamespaceTbl = NULL;	/* Namespace table */
int           agNamespaceCount = 0;
#endif
#ifdef AG_ENABLE_DSO
char **agModuleDirs = NULL;		/* Module search directories */
int    agModuleDirCount = 0;
#endif

/* Import inlinables */
#undef AG_INLINE_HEADER
#include <agar/core/inline_object.h>

/* Initialize an AG_Object instance. */
void
AG_ObjectInit(void *pObj, void *pClass)
{
	AG_Object *ob = pObj;
	AG_ObjectClass *C = (pClass != NULL) ? AGOBJECTCLASS(pClass) :
	                                       &agObjectClass;
	AG_ObjectClass **hier;
	int i, nHier;
	
	ob->tag = agObjectSignature;
	ob->cid = C->ver.cid;

#ifdef AG_DEBUG
	memset(ob->name, '\0', sizeof(ob->name));
#else
	ob->name[0] = '\0';
#endif
	ob->cls = C;
	ob->parent = NULL;
	ob->root = ob;
	ob->flags = 0;

	AG_MutexInitRecursive(&ob->lock);
	
	TAILQ_INIT(&ob->events);
#ifdef AG_TIMERS
	TAILQ_INIT(&ob->timers);
#endif
	TAILQ_INIT(&ob->vars);
	TAILQ_INIT(&ob->children);

	if (AG_ObjectGetInheritHier(ob, &hier, &nHier) != 0) {
		AG_FatalError(NULL);
	}
	for (i = 0; i < nHier; i++) {
		if (hier[i]->init != NULL)
			hier[i]->init(ob);
	}
	free(hier);
}

/* Initialize an AG_Object instance (and set the STATIC flag on it). */
void
AG_ObjectInitStatic(void *obj, void *C)
{
	AG_ObjectInit(obj, C);
	OBJECT(obj)->flags |= AG_OBJECT_STATIC;
}

/*
 * Allocate, initialize and attach a new object instance of the specified
 * class.
 */
void *
AG_ObjectNew(void *parent, const char *name, AG_ObjectClass *C)
{
	char nameGen[AG_OBJECT_NAME_MAX];
	AG_Object *obj;

	if (name == NULL) {
		AG_ObjectGenName(parent, C, nameGen, sizeof(nameGen));
	} else {
		if (parent != NULL &&
		    AG_ObjectFindChild(parent, name) != NULL) {
			AG_SetErrorV("E7", _("Existing child object"));
			return (NULL);
		}
	}

	if ((obj = TryMalloc(C->size)) == NULL) {
		return (NULL);
	}
	AG_ObjectInit(obj, C);
	AG_ObjectSetNameS(obj, (name != NULL) ? name : (const char *)nameGen);
	if (parent != NULL) {
		AG_ObjectAttach(parent, obj);
	}
	return (obj);
}

/*
 * Restore an object to an initial state prior to deserialization or release.
 */
void
AG_ObjectReset(void *p)
{
	AG_Object *ob = p;
	AG_ObjectClass **hier;
	int i, nHier;

	AG_ObjectLock(ob);

	if (AG_ObjectGetInheritHier(ob, &hier, &nHier) != 0) {
		AG_FatalError(NULL);
	}
	for (i = nHier-1; i >= 0; i--) {
		if (hier[i]->reset != NULL)
			hier[i]->reset(ob);
	}
	AG_ObjectUnlock(ob);

	free(hier);
}

#if AG_MODEL != AG_SMALL
/*
 * Recursive function to construct absolute object names.
 * The Object and its parent VFS must be locked.
 */
static int
GenerateObjectPath(void *_Nonnull obj, char *_Nonnull path, AG_Size path_len)
{
	AG_Object *ob = obj;
	AG_Size name_len, cur_len;
	int rv = 0;

	cur_len = strlen(path);
	name_len = strlen(ob->name);
	
	if (name_len+cur_len+1 > path_len) {
		AG_SetErrorV("E4", _("Path buffer overflow"));
		return (-1);
	}
	
	/* Prepend separator, object name. */
	memmove(&path[name_len+1], path, cur_len+1);    /* Move the NUL as well */
	path[0] = AG_PATHSEPCHAR;
	memcpy(&path[1], ob->name, name_len);		/* Omit the NUL */

	if (ob->parent != ob->root && ob->parent != NULL) {
		rv = GenerateObjectPath(ob->parent, path, path_len);
	}
	return (rv);
}

/*
 * Copy the absolute pathname of an object to a fixed-size buffer.
 * Buffer size must be at least 2 bytes in size.
 */
int
AG_ObjectCopyName(void *obj, char *path, AG_Size path_len)
{
	AG_Object *ob = obj;
	int rv = 0;

	if (path_len < 2) {
		AG_SetErrorV("E5", _("Path buffer overflow"));
		return (-1);
	}
	path[0] = AG_PATHSEPCHAR;
	path[1] = '\0';

	AG_LockVFS(ob);
	AG_ObjectLock(ob);
	if (ob == ob->root) {
		Strlcat(path, ob->name, path_len);
	} else {
		rv = GenerateObjectPath(ob, path, path_len);
	}
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (rv);
}

/*
 * Return the name of the class which obj belongs to.
 * 
 * If full=0, return only the last subclass (eg. "AG_Button").
 * If full=1, return the full inheritance hierarchy (eg. "AG_Widget:AG_Button").
 * The caller should free(3) the returned string after use.
 */
char *
AG_ObjectGetClassName(const void *obj, int full)
{
	const AG_Object *ob = obj;

	return Strdup(full ? AGOBJECT_CLASS(ob)->hier :
	                     AGOBJECT_CLASS(ob)->name);
}

/*
 * Return the fullpath of an object (relative to the root of its parent VFS).
 *
 * The returned path is true for as long as the VFS remains locked (the
 * caller may wish to use AG_LockVFS() to guarantee atomicity of operations
 * which are dependent on the returned path being true).
 *
 * The caller should free(3) the returned string after use.
 *
 * This routine uses recursion. It will fail and return NULL if insufficient
 * memory is available to construct the complete path.
 */
char *
AG_ObjectGetName(void *obj)
{
	AG_Object *ob = obj;
	AG_Object *pob;
	char *path;
	AG_Size pathLen = 1;
	
	AG_LockVFS(ob);
	AG_ObjectLock(ob);

	for (pob = ob;
	     pob->parent != NULL;
	     pob = pob->parent) {
		pathLen += strlen(pob->name) + 1;
	}
	if ((path = TryMalloc(pathLen+1)) == NULL) {
		goto fail;
	}
	path[0] = AG_PATHSEPCHAR;
	path[1] = '\0';

	if (ob == ob->root) {
		Strlcat(path, ob->name, pathLen);
	} else {
		GenerateObjectPath(ob, path, pathLen);
	}
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (path);
fail:
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (NULL);
}

/* Initialize an AG_Object instance (name argument variant). */
void
AG_ObjectInitNamed(void *obj, void *cl, const char *name)
{
	AG_ObjectInit(obj, cl);
	if (name != NULL) {
		AG_ObjectSetNameS(obj, name);
	} else {
		OBJECT(obj)->flags |= AG_OBJECT_NAME_ONATTACH;
	}
}
#endif /* !AG_SMALL */

/*
 * Set a variable which is a pointer to a function (with optional arguments).
 * The object must be locked.
 */
AG_Variable *
AG_SetFn(void *p, const char *name, AG_EventFn fn, const char *fmt, ...)
{
	AG_Object *obj = p;
	AG_Event *ev;
	AG_Variable *V;

	if (fn == NULL) {
		AG_Unset(obj, name);
		return (NULL);
	}

	ev = AG_EventNew(fn, obj, NULL);
	if (fmt) {
		va_list ap;

		va_start(ap, fmt);
		AG_EventGetArgs(ev, fmt, ap);
		va_end(ap);
	}
#ifdef AG_DEBUG
	Debug(obj, "Set \"" AGSI_YEL "%s" AGSI_RST "\" -> ("
	           AGSI_CYAN "Function" AGSI_RST " *)%p\n", name, fn);
#endif
	V = AG_FetchVariable(obj, name, AG_VARIABLE_FUNCTION);
	V->data.p = ev;
	V->info.pFlags = 0;
	return (V);
}

/* Attach an object to another object. */
void
AG_ObjectAttach(void *parentp, void *pChld)
{
	AG_Object *parent = parentp;
	AG_Object *chld = pChld;

	if (parent == NULL)
		return;
#ifdef AG_DEBUG
	if (parent == chld)
		AG_FatalErrorV("E31", "parent == chld");
#endif
#ifdef AG_TYPE_SAFETY
	if (!AG_OBJECT_VALID(parent)) { AG_FatalErrorV("E38a", "Parent object is invalid"); }
	if (!AG_OBJECT_VALID(chld)) { AG_FatalErrorV("E38b", "Child object is invalid"); }
#endif
	AG_LockVFS(parent);
	AG_ObjectLock(parent);
	AG_ObjectLock(chld);

	/*
	 * Update the child's "parent" and "root" pointers. If we are using
	 * a custom attach function, we assume that it will follow through.
	 */
	chld->parent = parent;
	chld->root = parent->root;
	
	/* Call the attach function if one is defined. */
	if (AG_Defined(chld, "attach-fn")) {
		AG_Event *ev;
		if ((ev = AG_GetPointer(chld,"attach-fn")) != NULL &&
		     ev->fn != NULL) {
			ev->fn(ev);
		}
		goto out;
	}

	/* Name the object if it has the name-on-attach flag set. */
	if (chld->name[0] == '\0' && (chld->flags & AG_OBJECT_NAME_ONATTACH)) {
		AG_ObjectGenName(parent, chld->cls, chld->name,
		    sizeof(chld->name));
	}
	
	/* Attach the object. */
	TAILQ_INSERT_TAIL(&parent->children, chld, cobjs);
      
	/* Notify the child object. */
	AG_PostEvent(chld, "attached", "%p", parent);

#ifdef DEBUG_OBJECT
	if (chld->name[0] != '\0') {
		Debug(parent, "Attached child: %s\n", chld->name);
	} else {
		Debug(parent, "Attached child: <%p>\n", chld);
	}
	if (parent->name[0] != '\0') {
		Debug(chld, "New parent: %s\n", parent->name);
	} else {
		Debug(chld, "New parent: <%p>\n", parent);
	}
#endif /* DEBUG_OBJECT */

out:
#ifdef AG_THREADS
	AG_ObjectUnlock(chld);
	AG_ObjectUnlock(parent);
	AG_UnlockVFS(parent);
#endif
	return;
}

/* Detach a child object from its parent. */
void
AG_ObjectDetach(void *pChld)
{
	AG_Object *chld = pChld;
#ifdef AG_THREADS
	AG_Object *root = chld->root;
#endif
	AG_Object *parent = chld->parent;
#ifdef AG_TIMERS
	AG_Timer *to, *toNext;
#endif
#ifdef AG_TYPE_SAFETY
	if (!AG_OBJECT_VALID(chld)) { AG_FatalErrorV("E36a", "Child object is invalid"); }
	if (!AG_OBJECT_VALID(parent)) { AG_FatalErrorV("E36b", "Parent object is invalid"); }
#endif
#ifdef AG_THREADS
	AG_LockVFS(root);
	AG_ObjectLock(parent);
	AG_ObjectLock(chld);
#endif
	/* Call the detach function if one is defined. */
	if (AG_Defined(chld, "detach-fn")) {
		AG_Event *ev;
		if ((ev = AG_GetPointer(chld,"detach-fn")) != NULL &&
		     ev->fn != NULL) {
			ev->fn(ev);
		}
		goto out;

	}
#ifdef AG_TIMERS
	/* Cancel any running timer associated with the object. */
	AG_LockTiming();
	for (to = TAILQ_FIRST(&chld->timers);
	     to != TAILQ_END(&chld->timers);
	     to = toNext) {
		toNext = TAILQ_NEXT(to, pvt.timers);
		AG_DelTimer(chld, to);
	}
	AG_UnlockTiming();
#endif
	AG_PostEvent(chld, "detached", "%p", parent);
	
	/* Remove the object from the parent's children list. */
	TAILQ_REMOVE(&parent->children, chld, cobjs);
	chld->parent = NULL;
	chld->root = chld;

#ifdef DEBUG_OBJECT
	if (chld->name[0] != '\0') {
		Debug(parent, "Detached child: %s\n", chld->name);
	} else {
		Debug(parent, "Detached child: <%p>\n", chld);
	}
#endif /* DEBUG_OBJECT */

out:
#ifdef AG_THREADS
	AG_ObjectUnlock(chld);
	AG_ObjectUnlock(parent);
	AG_UnlockVFS(root);
#endif
	return;
}

/*
 * Detach a child object from its parent (Lockless variant).
 * Both parent and child objects must be locked.
 */
void
AG_ObjectDetachLockless(void *pChld)
{
	AG_Object *chld = pChld;
	AG_Object *parent = chld->parent;
#ifdef AG_TIMERS
	AG_Timer *to, *toNext;
#endif
#ifdef AG_TYPE_SAFETY
	if (!AG_OBJECT_VALID(chld)) { AG_FatalErrorV("E36a", "Child object is invalid"); }
	if (!AG_OBJECT_VALID(parent)) { AG_FatalErrorV("E36b", "Parent object is invalid"); }
#endif
	if (AG_Defined(chld, "detach-fn")) {
		AG_Event *ev;
		if ((ev = AG_GetPointer(chld,"detach-fn")) != NULL &&
		     ev->fn != NULL) {
			ev->fn(ev);
		}
		return;
	}
#ifdef AG_TIMERS
	/* Cancel any running timer associated with the object. */
	AG_LockTiming();
	for (to = TAILQ_FIRST(&chld->timers);
	     to != TAILQ_END(&chld->timers);
	     to = toNext) {
		toNext = TAILQ_NEXT(to, pvt.timers);
		AG_DelTimer(chld, to);
	}
	AG_UnlockTiming();
#endif
	AG_PostEvent(chld, "detached", "%p", parent);
	
	/* Remove the object from the parent's children list. */
	TAILQ_REMOVE(&parent->children, chld, cobjs);
	chld->parent = NULL;
	chld->root = chld;

#ifdef DEBUG_OBJECT
	if (chld->name[0] != '\0') {
		Debug(parent, "Detached child: %s\n", chld->name);
	} else {
		Debug(parent, "Detached child: <%p>\n", chld);
	}
#endif /* DEBUG_OBJECT */
}

/* Traverse the object tree using a pathname. */
static void *_Nullable _Pure_Attribute
FindObjectByName(const AG_Object *_Nonnull parent, const char *_Nonnull name)
{
	char chldName[AG_OBJECT_PATH_MAX];
	void *rv;
	char *s;
	AG_Object *child;

	if (Strlcpy(chldName, name, sizeof(chldName)) >= sizeof(chldName)) {
		AG_SetErrorS(_("Path overflow"));
		return (NULL);
	}

	if ((s = strchr(chldName, AG_PATHSEPCHAR)) != NULL) {
		*s = '\0';
	}
	TAILQ_FOREACH(child, &parent->children, cobjs) {
		if (strcmp(child->name, chldName) != 0)
			continue;

		if ((s = strchr(name, AG_PATHSEPCHAR)) != NULL &&
		    s[1] != '\0') {
			rv = FindObjectByName(child, &s[1]);
			if (rv != NULL) {
				return (rv);
			} else {
				return (NULL);
			}
		}
		return (child);
	}
	return (NULL);
}

/*
 * Search for the named object (absolute path).
 *
 * Returned pointer is guaranteed to be valid as long as the VFS is locked.
 * If no such object exists, return NULL (without SetError).
 */
void *
AG_ObjectFindS(void *vfsRoot, const char *name)
{
	void *rv;

#ifdef AG_DEBUG
	if (name[0] != AG_PATHSEPCHAR)
		AG_FatalErrorV("E32", "Not an absolute path");
#endif
	if (name[0] == AG_PATHSEPCHAR && name[1] == '\0') {
		return (vfsRoot);
	}
	AG_LockVFS(vfsRoot);
	rv = FindObjectByName(vfsRoot, &name[1]);
	AG_UnlockVFS(vfsRoot);
	return (rv);
}

/*
 * Search for the named object (absolute path as format string).
 *
 * Returned pointer is guaranteed to be valid as long as the VFS is locked.
 * If no such object exists, return NULL (without SetError).
 */
void *
AG_ObjectFind(void *vfsRoot, const char *fmt, ...)
{
	char path[AG_OBJECT_PATH_MAX];
	void *rv;
	va_list ap;

	va_start(ap, fmt);
	Vsnprintf(path, sizeof(path), fmt, ap);
	va_end(ap);
#ifdef AG_DEBUG
	if (path[0] != AG_PATHSEPCHAR)
		AG_FatalErrorV("E32", "Not an absolute path");
#endif
	AG_LockVFS(vfsRoot);
	rv = FindObjectByName(vfsRoot, &path[1]);
	AG_UnlockVFS(vfsRoot);
	return (rv);
}

/*
 * Traverse an object's ancestry looking for parent object of the given class.
 * Return value is only valid as long as VFS is locked.
 */
void *
AG_ObjectFindParent(void *p, const char *name, const char *t)
{
	AG_Object *ob = AGOBJECT(p);

	AG_LockVFS(p);
	while (ob != NULL) {
		AG_Object *po = AGOBJECT(ob->parent);

		if (po == NULL) {
			goto fail;
		}
		if ((t == NULL || AG_ClassIsNamed(po->cls, t)) &&
		    (name == NULL || strcmp(po->name, name) == 0)) {
			AG_UnlockVFS(p);
			return ((void *)po);
		}
		ob = AGOBJECT(ob->parent);
	}
fail:
	AG_UnlockVFS(p);
	return (NULL);
}

void
AG_ObjectFreeChildrenLockless(AG_Object *obj)
{
	AG_Object *child, *childNext;
	struct ag_objectq detached;

#ifdef AG_TYPE_SAFETY
	if (!AG_OBJECT_VALID(obj))
		AG_FatalErrorV("E37", "Parent object is invalid");
#endif
	TAILQ_INIT(&detached);

	for (child = TAILQ_FIRST(&obj->children);
	     child != TAILQ_END(&obj->children);
	     child = childNext) {
		childNext = TAILQ_NEXT(child, cobjs);
#ifdef DEBUG_OBJECT
		if (child->name[0] != '\0') {
			Debug(obj, "Detaching child: %s\n", child->name);
		} else {
			Debug(obj, "Detaching child: <%p>\n", child);
		}
#endif
		AG_ObjectDetachLockless(child);
		TAILQ_INSERT_TAIL(&detached, child, cobjs);
	}
	TAILQ_INIT(&obj->children);

	for (child = TAILQ_FIRST(&detached);
	     child != TAILQ_END(&detached);
	     child = childNext) {
		childNext = TAILQ_NEXT(child, cobjs);
		AG_ObjectDestroy(child);
	}
}

/* Detach and destroy all child objects under a given parent. */
void
AG_ObjectFreeChildren(void *pObj)
{
	AG_Object *obj = pObj;

	if (!AG_OBJECT_VALID(obj))
		return;

	AG_ObjectLock(obj);
	AG_ObjectFreeChildrenLockless(obj);
	AG_ObjectUnlock(obj);
}

#if AG_MODEL != AG_SMALL
void
AG_ObjectFreeChildrenOfTypeLockless(AG_Object *obj, const char *pattern)
{
	AG_Object *child, *childNext;
	struct ag_objectq detached;

#ifdef AG_TYPE_SAFETY
	if (!AG_OBJECT_VALID(obj))
		AG_FatalErrorV("E37", "Parent object is invalid");
#endif
	TAILQ_INIT(&detached);

	for (child = TAILQ_FIRST(&obj->children);
	     child != TAILQ_END(&obj->children);
	     child = childNext) {
		childNext = TAILQ_NEXT(child, cobjs);
	
		if (!AG_OfClass(child, pattern))
			continue;
#ifdef DEBUG_OBJECT
		if (child->name[0] != '\0') {
			Debug(obj, "Detaching child: %s\n", child->name);
		} else {
			Debug(obj, "Detaching child: <%p>\n", child);
		}
#endif
		AG_ObjectDetachLockless(child);
		TAILQ_REMOVE(&obj->children, child, cobjs);
		TAILQ_INSERT_TAIL(&detached, child, cobjs);
	}

	for (child = TAILQ_FIRST(&detached);
	     child != TAILQ_END(&detached);
	     child = childNext) {
		childNext = TAILQ_NEXT(child, cobjs);
		AG_ObjectDestroy(child);
	}
}

/* Detach and destroy matching child objects under a given parent. */
void
AG_ObjectFreeChildrenOfType(void *pObj, const char *pattern)
{
	AG_Object *obj = pObj;

	if (!AG_OBJECT_VALID(obj))
		return;

	AG_ObjectLock(obj);
	AG_ObjectFreeChildrenOfTypeLockless(obj, pattern);
	AG_ObjectUnlock(obj);
}
#endif /* !AG_SMALL */

/* Destroy the object variables. */
void
AG_ObjectFreeVariables(void *pObj)
{
	AG_Object *ob = pObj;
	AG_Variable *V, *Vnext;

	AG_ObjectLock(ob);
	for (V = TAILQ_FIRST(&ob->vars);
	     V != TAILQ_END(&ob->vars);
	     V = Vnext) {
		Vnext = TAILQ_NEXT(V, vars);
		AG_FreeVariable(V);
		free(V);
	}
	TAILQ_INIT(&ob->vars);
	AG_ObjectUnlock(ob);
}

/* Destroy the event handler structures. */
void
AG_ObjectFreeEvents(AG_Object *ob)
{
	AG_Event *ev, *evNext;

	AG_ObjectLock(ob);
	for (ev = TAILQ_FIRST(&ob->events);
	     ev != TAILQ_END(&ob->events);
	     ev = evNext) {
		evNext = TAILQ_NEXT(ev, events);
		free(ev);
	}
	TAILQ_INIT(&ob->events);
	AG_ObjectUnlock(ob);
}

/*
 * Release all resources allocated by an object and its children.
 * Invokes the object reset() and destroy() operations.
 * 
 * None of the objects must be in use.
 */
void
AG_ObjectDestroy(void *p)
{
	AG_Object *ob = p;
	AG_ObjectClass **hier;
	AG_Object *child, *childNext;
	AG_Variable *V, *Vnext;
	AG_Event *ev, *evNext;
	int i, nHier;

#ifdef AG_TYPE_SAFETY
	if (!AG_OBJECT_VALID(ob))
		AG_FatalErrorV("E36", "Object is invalid");
#endif
#ifdef AG_DEBUG
	if (ob->parent != NULL) {
		AG_Debug(ob, "I'm still attached to %s\n", OBJECT(ob->parent)->name);
		AG_FatalErrorV("E33", "Object is still attached");
	}
#endif
	/*
	 * Release the child objects.
	 */
	for (child = TAILQ_FIRST(&ob->children);
	     child != TAILQ_END(&ob->children);
	     child = childNext) {
		childNext = TAILQ_NEXT(child, cobjs);
#ifdef DEBUG_OBJECT
		if (child->name[0] != '\0') {
			Debug(ob, "Freeing child: %s\n", child->name);
		} else {
			Debug(ob, "Freeing child: <%p>\n", child);
		}
#endif
		AG_ObjectDetachLockless(child);
		AG_ObjectDestroy(child);
	}

	/*
	 * Invoke reset() and destroy() for every class in the object's
	 * inheritance hierarchy.
	 */
	if (AG_ObjectGetInheritHier(ob, &hier, &nHier) != 0) {
		AG_FatalError(NULL);
	}
	for (i = nHier-1; i >= 0; i--) {
		if (hier[i]->reset != NULL)
			hier[i]->reset(ob);
		if (hier[i]->destroy != NULL)
			hier[i]->destroy(ob);
	}
	free(hier);

	/*
	 * Release defined variables and event handler structures.
	 */
	for (V = TAILQ_FIRST(&ob->vars);
	     V != TAILQ_END(&ob->vars);
	     V = Vnext) {
		Vnext = TAILQ_NEXT(V, vars);
		AG_FreeVariable(V);
		free(V);
	}
	for (ev = TAILQ_FIRST(&ob->events);
	     ev != TAILQ_END(&ob->events);
	     ev = evNext) {
		evNext = TAILQ_NEXT(ev, events);
		free(ev);
	}

	/* Invalidate the validity tag and class ID. */
	ob->tag = 0;
	ob->cid = 0;

	/* Release the object's locking device. */
	AG_MutexDestroy(&ob->lock);

	/* Release the object structure (if dynamically allocated). */
	if ((ob->flags & AG_OBJECT_STATIC) == 0)
		free(ob);
}

#ifdef AG_SERIALIZATION
/*
 * Search the AG_CONFIG_PATH_DATA path group for an object file matching
 * "<obj-name>.<obj-ext>" and return its fullpath into a fixed-size buffer.
 *
 * If set, the special AG_Object variable "archive-path" will override the
 * default path and arrange for the object to be loaded from a specific file.
 *
 * Under threads the returned path is only valid as long as the VFS is locked.
 * This function will clobber path even when it fails and returns -1.
 */
int
AG_ObjectCopyFilename(void *p, char *path, AG_Size pathSize)
{
	char name[AG_OBJECT_PATH_MAX];
	AG_ConfigPathQ *pathGroup = &agConfig->paths[AG_CONFIG_PATH_DATA];
	AG_ConfigPath *loadPath;
	const char *archivePath;
	AG_Object *ob = p;

	AG_ObjectLock(ob);
	if (AG_Defined(ob, "archive-path") &&
	    (archivePath = AG_GetStringP(ob,"archive-path")) != NULL &&
	     archivePath[0] != '\0') {
		Strlcpy(path, archivePath, pathSize);
		goto out;
	}

	AG_ObjectCopyName(ob, name, sizeof(name));

	TAILQ_FOREACH(loadPath, pathGroup, paths) {
	     	Strlcpy(path, loadPath->s, pathSize);
		Strlcat(path, name, pathSize);
		Strlcat(path, AG_PATHSEP, pathSize);
		Strlcat(path, ob->name, pathSize);
		Strlcat(path, ".", pathSize);
		Strlcat(path, ob->cls->name, pathSize);

		/* TODO: check signature */
		if (AG_FileExists(path))
			goto out;
	}
	AG_SetErrorV("E5", _("File not found"));
	AG_ObjectUnlock(ob);
	return (-1);
out:
	AG_ObjectUnlock(ob);
	return (0);
}

/*
 * Copy the full pathname of an object's data dir to a fixed-size buffer.
 * The path is only valid as long as the VFS is locked.
 */
int
AG_ObjectCopyDirname(void *p, char *path, AG_Size pathSize)
{
	char tp[AG_PATHNAME_MAX];
	char name[AG_OBJECT_PATH_MAX];
	AG_ConfigPathQ *pathGroup = &agConfig->paths[AG_CONFIG_PATH_DATA];
	AG_ConfigPath *loadPath;
	AG_Object *ob = p;

	AG_ObjectLock(ob);
	AG_ObjectCopyName(ob, name, sizeof(name));

	TAILQ_FOREACH(loadPath, pathGroup, paths) {
	     	Strlcpy(tp, loadPath->s, sizeof(tp));
		Strlcat(tp, name, sizeof(tp));

		if (AG_FileExists(tp)) {
			Strlcpy(path, tp, pathSize);
			goto out;
		}
	}
	AG_SetErrorV("E6", _("Directory is not in load-path."));
	AG_ObjectUnlock(ob);
	return (-1);
out:
	AG_ObjectUnlock(ob);
	return (0);
}

/* Load both the generic part and the dataset of an object from file. */
int
AG_ObjectLoadFromFile(void *p, const char *path)
{
	AG_Object *ob = p;
	int dataFound;

	AG_LockVFS(ob);
	AG_ObjectLock(ob);
	if (AG_ObjectLoadGenericFromFile(ob, path) == -1 ||
	    AG_ObjectLoadDataFromFile(ob, &dataFound, path) == -1) {
		goto fail;
	}
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (0);
fail:
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (-1);
}

int
AG_ObjectLoad(void *p)
{
	return AG_ObjectLoadFromFile(p, NULL);
}
int
AG_ObjectLoadData(void *p, int *dataFound)
{
	return AG_ObjectLoadDataFromFile(p, dataFound, NULL);
}
int AG_ObjectLoadGeneric(void *p)
{
	return AG_ObjectLoadGenericFromFile(p, NULL);
}

/* Read an Agar archive header. */
int
AG_ObjectReadHeader(AG_DataSource *ds, AG_ObjectHeader *oh)
{
	/* Signature and version data */
	if (AG_ReadVersion(ds, agObjectClass.name, &agObjectClass.ver, &oh->ver)
	    == -1)
		return (-1);

	/* Class hierarchy and module references */
	if (oh->ver.minor >= 2) {
		AG_ObjectClassSpec *cs = &oh->cs;
		char *c;

		AG_CopyString(cs->hier, ds, sizeof(cs->hier));
#ifdef AG_ENABLE_DSO
		AG_CopyString(cs->libs, ds, sizeof(cs->libs));
#else
		AG_SkipString(ds);
#endif
		Strlcpy(cs->spec, cs->hier, sizeof(cs->spec));
#ifdef AG_ENABLE_DSO
		if (cs->libs[0] != '\0') {
			Strlcat(cs->spec, "@", sizeof(cs->spec));
			Strlcat(cs->spec, cs->libs, sizeof(cs->spec));
		}
#endif
		if ((c = strrchr(cs->hier, ':')) != NULL && c[1] != '\0') {
			Strlcpy(cs->name, &c[1], sizeof(cs->name));
		} else {
			Strlcpy(cs->name, cs->hier, sizeof(cs->name));
		}
	} else {
		oh->cs.hier[0] = '\0';
		oh->cs.spec[0] = '\0';
		oh->cs.name[0] = '\0';
#ifdef AG_ENABLE_DSO
		oh->cs.libs[0] = '\0';
#endif
	}

	/* Dataset start offset */
	oh->dataOffs = AG_ReadUint32(ds);

	/* Object flags */
	oh->flags = (Uint)AG_ReadUint32(ds);

	return (0);
}

/* Load Object variables. */
int
AG_ObjectLoadVariables(void *p, AG_DataSource *ds)
{
	const AG_Version propTblVer = { 2, 1 };
	AG_Object *ob = p;
	Uint count, i, j;

	/* TODO 2.0 remove this redundant signature */
	if (AG_ReadVersion(ds, "AG_PropTbl", &propTblVer, NULL) == -1)
		return (-1);

	AG_ObjectLock(ob);

	count = (Uint)AG_ReadUint32(ds);
#if AG_MODEL != AG_SMALL
	if (count > AG_OBJECT_MAX_VARIABLES) {
		AG_SetErrorS(_("Too many variables"));
		return (-1);
	}
#endif
	for (i = 0; i < count; i++) {
		char key[64];
		Sint8 code;
		char *s;

		if (AG_CopyString(key, ds, sizeof(key)) >= sizeof(key)) {
			AG_SetErrorV("E8", _("Variable name is too long"));
			goto fail;
		}
		code = (Sint8)AG_ReadSint32(ds);		/* XXX */
#ifdef DEBUG_SERIALIZATION
		Debug(ob, "key \"%s\" = code %d\n", key, code)
#endif
		for (j = 0; j < AG_VARIABLE_TYPE_LAST; j++) {
			if (agVariableTypes[j].code == code)
				break;
		}
		if (j == AG_VARIABLE_TYPE_LAST) {
			AG_SetErrorV("E9", _("Unknown variable type"));
			goto fail;
		}
		switch (agVariableTypes[j].typeTgt) {
#if AG_MODEL == AG_SMALL
		case AG_VARIABLE_UINT:   AG_SetUint(ob,  key, AG_ReadUint16(ds));	break;
		case AG_VARIABLE_INT:    AG_SetInt(ob,   key, AG_ReadSint16(ds));	break;
#else
		case AG_VARIABLE_UINT:   AG_SetUint(ob,  key,  (Uint)AG_ReadUint32(ds)); break;
		case AG_VARIABLE_INT:    AG_SetInt(ob,   key,   (int)AG_ReadSint32(ds)); break;
		case AG_VARIABLE_ULONG:  AG_SetUlong(ob, key, (Ulong)AG_ReadUint64(ds)); break;
		case AG_VARIABLE_LONG:   AG_SetLong(ob,  key,  (long)AG_ReadSint64(ds)); break;
		case AG_VARIABLE_UINT32: AG_SetUint32(ob, key, AG_ReadUint32(ds));	break;
		case AG_VARIABLE_SINT32: AG_SetSint32(ob, key, AG_ReadSint32(ds));	break;
#endif /* MD or LG */
		case AG_VARIABLE_UINT8:  AG_SetUint8(ob,  key, AG_ReadUint8(ds));	break;
		case AG_VARIABLE_SINT8:  AG_SetSint8(ob,  key, AG_ReadSint8(ds));	break;
		case AG_VARIABLE_UINT16: AG_SetUint16(ob, key, AG_ReadUint16(ds));	break;
		case AG_VARIABLE_SINT16: AG_SetSint16(ob, key, AG_ReadSint16(ds));	break;
#ifdef AG_HAVE_64BIT
		case AG_VARIABLE_UINT64: AG_SetUint64(ob, key, AG_ReadUint64(ds));	break;
		case AG_VARIABLE_SINT64: AG_SetSint64(ob, key, AG_ReadSint64(ds));	break;
#endif
#ifdef AG_HAVE_FLOAT
		case AG_VARIABLE_FLOAT:  AG_SetFloat(ob,  key, AG_ReadFloat(ds));	break;
		case AG_VARIABLE_DOUBLE: AG_SetDouble(ob, key, AG_ReadDouble(ds));	break;
#endif
		case AG_VARIABLE_STRING:
			if ((s = AG_ReadString(ds)) != NULL) {
				AG_SetStringNODUP(ob, key, s);
			} else {
				AG_SetString(ob, key, "");
			}
			break;
		default:
			AG_SetErrorV("E10", _("Incompatible variable type"));
			goto fail;
		}
	}
	AG_ObjectUnlock(ob);
	return (0);
fail:
	AG_ObjectUnlock(ob);
	return (-1);
}

/* Save persistent object variables. */
void
AG_ObjectSaveVariables(void *pObj, AG_DataSource *ds)
{
	const AG_Version propTblVer = { 2, 1 };
	AG_Object *ob = pObj;
	AG_Offset countOffs;
	Uint32 count = 0;
	AG_Variable *V;

	/* TODO 2.0 remove this redundant signature */
	AG_WriteVersion(ds, "AG_PropTbl", &propTblVer);
	countOffs = AG_Tell(ds);
	AG_WriteUint32(ds, 0);
	
	AG_ObjectLock(ob);
	TAILQ_FOREACH(V, &ob->vars, vars) {
		const AG_VariableTypeInfo *Vt = &agVariableTypes[V->type];
		void *p;

		if (Vt->code == -1) {
			Verbose("Save: skipping %s (non-persistent)\n", V->name);
			continue;
		}

		AG_LockVariable(V);
		AG_WriteString(ds, (char *)V->name);
		AG_WriteSint32(ds, (Sint32)Vt->code);		/* XXX */

		p = (agVariableTypes[V->type].indirLvl > 0) ?
		    V->data.p : (void *)&V->data;

		switch (AG_VARIABLE_TYPE(V)) {
#if AG_MODEL == AG_SMALL
		case AG_VARIABLE_UINT:	 AG_WriteUint16(ds, *(Uint *)p);	break;
		case AG_VARIABLE_INT:	 AG_WriteSint16(ds, *(int *)p);		break;
#else /* MEDIUM or LARGE */
		case AG_VARIABLE_UINT:	 AG_WriteUint32(ds, (Uint32)*(Uint *)p);	break;
		case AG_VARIABLE_INT:	 AG_WriteSint32(ds, (Sint32)*(int *)p);		break;
		case AG_VARIABLE_ULONG:	 AG_WriteUint64(ds, (Uint64)*(Ulong *)p);	break;
		case AG_VARIABLE_LONG:	 AG_WriteSint64(ds, (Sint64)*(long *)p);	break;
#endif
		case AG_VARIABLE_UINT8:	 AG_WriteUint8(ds, *(Uint8 *)p);		break;
		case AG_VARIABLE_SINT8:	 AG_WriteSint8(ds, *(Sint8 *)p);		break;
		case AG_VARIABLE_UINT16: AG_WriteUint16(ds, *(Uint16 *)p);		break;
		case AG_VARIABLE_SINT16: AG_WriteSint16(ds, *(Sint16 *)p);		break;
#if AG_MODEL != AG_SMALL
		case AG_VARIABLE_UINT32: AG_WriteUint32(ds, *(Uint32 *)p);		break;
		case AG_VARIABLE_SINT32: AG_WriteSint32(ds, *(Sint32 *)p);		break;
#endif
#ifdef AG_HAVE_64BIT
		case AG_VARIABLE_UINT64: AG_WriteUint64(ds, *(Uint64 *)p);		break;
		case AG_VARIABLE_SINT64: AG_WriteSint64(ds, *(Sint64 *)p);		break;
#endif
#ifdef AG_HAVE_FLOAT
		case AG_VARIABLE_FLOAT:  AG_WriteFloat(ds, *(float *)p);		break;
		case AG_VARIABLE_DOUBLE: AG_WriteDouble(ds, *(double *)p);		break;
#endif
		case AG_VARIABLE_STRING: AG_WriteString(ds, V->data.s);			break;
		default: break;
		}
		AG_UnlockVariable(V);

		if (++count > AG_OBJECT_MAX_VARIABLES) {
			AG_FatalErrorV("E34", "Too many variables to save");
		/*	break; */
		}
	}
	AG_ObjectUnlock(ob);
	AG_WriteUint32At(ds, count, countOffs);
}

/*
 * Load an Agar object (or a virtual filesystem of Agar objects) from an
 * archive file.
 *
 * Only the generic part is read, datasets are skipped and dependencies
 * are left unresolved.
 */
int
AG_ObjectLoadGenericFromFile(void *p, const char *pPath)
{
	AG_Object *ob = p;
#if AG_MODEL == AG_SMALL
	AG_ObjectHeader *oh;
#else
	AG_ObjectHeader oh;
#endif
	char path[AG_PATHNAME_MAX];
	AG_DataSource *ds;
	Uint32 count, i;
	
	AG_LockVFS(ob);
	AG_ObjectLock(ob);

	if (pPath != NULL) {
		Strlcpy(path, pPath, sizeof(path));
	} else {
		if (AG_ObjectCopyFilename(ob, path, sizeof(path)) == -1)
			goto fail_unlock;
	}
#ifdef DEBUG_SERIALIZATION
	Debug(ob, "Loading generic data from %s\n", path);
#endif
	if ((ds = AG_OpenFile(path, "rb")) == NULL)
		goto fail_unlock;

	/* Free any resident dataset in order to clear the dependencies. */
	AG_ObjectReset(ob);

#if AG_MODEL == AG_SMALL
	if ((oh = TryMalloc(sizeof(AG_ObjectHeader))) == NULL) {
		goto fail;
	}
	if (AG_ObjectReadHeader(ds, oh) == -1) {
		goto fail;
	}
	ob->flags &= ~(AG_OBJECT_SAVED_FLAGS);
	ob->flags |= oh->flags;
	free(oh);
#else
	if (AG_ObjectReadHeader(ds, &oh) == -1) {
		goto fail;
	}
	ob->flags &= ~(AG_OBJECT_SAVED_FLAGS);
	ob->flags |= oh.flags;
#endif
	/* Skip over legacy (pre-1.6) dependency table */
	count = AG_ReadUint32(ds);
	for (i = 0; i < count; i++)
		AG_SkipString(ds);

	/* Load the set of Variables */
	if (AG_ObjectLoadVariables(ob, ds) == -1)
		goto fail;
	
	/* Load the generic part of the archived child objects. */
	count = AG_ReadUint32(ds);
	for (i = 0; i < count; i++) {
		char cname[AG_OBJECT_NAME_MAX];
		char hier[AG_OBJECT_HIER_MAX];
		AG_Object *chld;
		AG_ObjectClass *C;

	 	/* TODO check that there are no duplicate names. */
		AG_CopyString(cname, ds, sizeof(cname));
		AG_CopyString(hier, ds, sizeof(hier));

		/* Look for an existing object of the given name. */
		if ((chld = AG_ObjectFindChild(ob, cname)) != NULL) {
			if (strcmp(chld->cls->hier, hier) != 0) {
#ifdef AG_VERBOSITY
				AG_SetError(_("Archived object `%s' clashes with "
				              "existing object of incompatible type"),
					      cname);
#else
				AG_SetErrorS("E12");
#endif
				goto fail;
			}
			if (AG_ObjectLoadGeneric(chld) == -1) {
				goto fail;
			}
			continue;
		}

		/* Create a new child object. */
#ifdef AG_ENABLE_DSO
		C = AG_LoadClass(hier);
#else
		C = AG_LookupClass(hier);
#endif
		if (C == NULL) {
#ifdef AG_VERBOSITY
			AG_SetError("%s: %s", ob->name, AG_GetError());
#else
			AG_SetErrorS("E14");
#endif
			if (agObjectIgnoreUnknownObjs) {
#ifdef DEBUG_SERIALIZATION
				Debug(ob, "%s; ignoring\n", AG_GetError());
#endif
				continue;
			} else {
				goto fail;
			}
			goto fail;
		}
		if ((chld = TryMalloc(C->size)) == NULL) {
			goto fail;
		}
		AG_ObjectInit(chld, C);
		AG_ObjectSetNameS(chld, cname);
		AG_ObjectAttach(ob, chld);
		if (AG_ObjectLoadGeneric(chld) == -1)
			goto fail;
	}

	AG_CloseFile(ds);
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (0);
fail:
	AG_ObjectReset(ob);
	AG_CloseFile(ds);
fail_unlock:
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (-1);
}

/* Load an Agar object dataset from an object archive file. */
int
AG_ObjectLoadDataFromFile(void *p, int *dataFound, const char *pPath)
{
	AG_ObjectHeader oh;
	char path[AG_PATHNAME_MAX];
	AG_Object *ob = p;
	AG_DataSource *ds;
	AG_Version ver;
	AG_ObjectClass **hier;
	int i, nHier;

	AG_LockVFS(ob);
	AG_ObjectLock(ob);

	*dataFound = 1;

	/* Open the file. */
	if (pPath != NULL) {
		Strlcpy(path, pPath, sizeof(path));
	} else {
		if (AG_ObjectCopyFilename(ob, path, sizeof(path)) == -1) {
			*dataFound = 0;
			goto fail_unlock;
		}
	}
#ifdef DEBUG_SERIALIZATION
	Debug(ob, "Loading dataset from %s\n", path);
#endif
	if ((ds = AG_OpenFile(path, "rb")) == NULL) {
		*dataFound = 0;
		goto fail_unlock;
	}
	if (AG_ObjectReadHeader(ds, &oh) == -1 ||
	    AG_Seek(ds, oh.dataOffs, AG_SEEK_SET) == -1 ||
	    AG_ReadVersion(ds, ob->cls->name, &ob->cls->ver, &ver) == -1) {
		goto fail;
	}
	if (ob->flags & AG_OBJECT_DEBUG_DATA) {
#ifdef AG_DEBUG
		AG_SetSourceDebug(ds, 1);
#else
		AG_SetErrorV("E15", _("Can't read without DEBUG"));
		goto fail;
#endif
	}
	if (AG_ObjectGetInheritHier(ob, &hier, &nHier) == -1)
		goto fail;

	AG_ObjectReset(ob);

	for (i = 0; i < nHier; i++) {
#ifdef DEBUG_SERIALIZATION
		Debug(ob, "Loading as %s\n", hier[i]->name);
#endif
		if (hier[i]->load == NULL)
			continue;
		if (hier[i]->load(ob, ds, &ver) == -1) {
#ifdef AG_VERBOSITY
			AG_SetError("<0x%x>: %s", (Uint)AG_Tell(ds), AG_GetError());
#else
			AG_SetErrorS("E16");
#endif
			free(hier);
			goto fail;
		}
	}
	free(hier);

	AG_CloseFile(ds);
	AG_PostEvent(ob->root, "object-post-load", "%p,%s", ob, path);
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (0);
fail:
	AG_CloseFile(ds);
fail_unlock:
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (-1);
}

static void
BackupObjectFile(const char *_Nonnull orig)
{
	char path[AG_PATHNAME_MAX];

	if (AG_FileExists(orig)) {
		Strlcpy(path, orig, sizeof(path));
		Strlcat(path, ".bak", sizeof(path));
		rename(orig, path);
	}
}

/* Save the state of an object and its children. */
int
AG_ObjectSaveAll(void *p)
{
	AG_Object *obj = p, *cobj;

	AG_LockVFS(obj);
	AG_ObjectLock(obj);

	if (AG_ObjectSave(obj) == -1) {
		goto fail;
	}
	TAILQ_FOREACH(cobj, &obj->children, cobjs) {
		AG_ObjectLock(cobj);
		if (AG_ObjectSaveAll(cobj) == -1) {
			AG_ObjectUnlock(cobj);
			goto fail;
		}
		AG_ObjectUnlock(cobj);
	}

	AG_ObjectUnlock(obj);
	AG_UnlockVFS(obj);
	return (0);
fail:
	AG_ObjectUnlock(obj);
	AG_UnlockVFS(obj);
	return (-1);
}

/* Serialize an object to an arbitrary AG_DataSource(3). */
int
AG_ObjectSerialize(void *p, AG_DataSource *ds)
{
	AG_Object *ob = p;
	AG_Offset dataOffs;
	AG_ObjectClass **hier;
	int i, nHier;
#ifdef AG_DEBUG
	int debugSave;
#endif
	AG_ObjectLock(ob);
	
	/* Header */
	AG_WriteVersion(ds, agObjectClass.name, &agObjectClass.ver);
	AG_WriteString(ds, ob->cls->hier);
#ifdef AG_ENABLE_DSO
	AG_WriteString(ds, ob->cls->libs);
#else
	AG_WriteString(ds, "");
#endif
	dataOffs = AG_Tell(ds);
	AG_WriteUint32(ds, 0);					/* Data offs */
	AG_WriteUint32(ds, (Uint32)(ob->flags & AG_OBJECT_SAVED_FLAGS));

	/* Legacy (pre-1.6) dependency table. */
	AG_WriteUint32(ds, 0);

	/* Persistent object variables */
	AG_ObjectSaveVariables(ob, ds);
	
	/* Legacy (pre-1.7) child object metadata */
	AG_WriteUint32(ds, 0);

	/* Dataset */
	AG_WriteUint32At(ds, AG_Tell(ds), dataOffs);
	AG_WriteVersion(ds, ob->cls->name, &ob->cls->ver);

#ifdef AG_DEBUG
	if (ob->flags & AG_OBJECT_DEBUG_DATA) {
		debugSave = AG_SetSourceDebug(ds, 1); 
	} else {
		debugSave = 0;
	}
#endif
	if (AG_ObjectGetInheritHier(ob, &hier, &nHier) == -1) {
		goto fail;
	}
	for (i = 0; i < nHier; i++) {
#ifdef DEBUG_SERIALIZATION
		Debug(ob, "Saving as %s\n", hier[i]->name);
#endif
		if (hier[i]->save == NULL)
			continue;
		if (hier[i]->save(ob, ds) == -1) {
			free(hier);
			goto fail;
		}
	}
	free(hier);

#ifdef AG_DEBUG
	if (ob->flags & AG_OBJECT_DEBUG_DATA)
		AG_SetSourceDebug(ds, debugSave);
#endif
	AG_ObjectUnlock(ob);
	return (0);
fail:
#ifdef AG_DEBUG
	if (ob->flags & AG_OBJECT_DEBUG_DATA)
		AG_SetSourceDebug(ds, debugSave);
#endif
	AG_ObjectUnlock(ob);
	return (-1);
}

/*
 * Unserialize single object (not a VFS) from an arbitrary AG_DataSource(3).
 * To unserialize complete virtual filesystems, see AG_ObjectLoadFromFile().
 */
int
AG_ObjectUnserialize(void *p, AG_DataSource *ds)
{
	AG_Object *ob = p;
	AG_ObjectHeader oh;
	AG_Version ver;
	AG_ObjectClass **hier = NULL;
	Uint32 count;
	int i, nHier;
#ifdef AG_DEBUG
	int debugSave;
#endif

	AG_ObjectLock(ob);

	/* Object header */
	if (AG_ObjectReadHeader(ds, &oh) == -1) {
		goto fail;
	}
	ob->flags &= ~(AG_OBJECT_SAVED_FLAGS);
	ob->flags |= oh.flags;

	/* Skip over legacy (pre-1.6) dependency table */
	count = AG_ReadUint32(ds);
	for (i = 0; i < count; i++)
		AG_SkipString(ds);

	/* Load the set of Variables */
	if (AG_ObjectLoadVariables(ob, ds) == -1)
		goto fail;

	/* Table of child objects, expected empty. */
	if (AG_ReadUint32(ds) != 0) {
		AG_SetErrorV("E17", "nChildren != 0");
		goto fail;
	}

	/* Dataset */
	if (AG_ReadVersion(ds, ob->cls->name, &ob->cls->ver, &ver) == -1)
		goto fail;

	if (ob->flags & AG_OBJECT_DEBUG_DATA) {
#ifdef AG_DEBUG
		debugSave = AG_SetSourceDebug(ds, 1);
#else
		AG_SetErrorV("E15", _("Can't read without DEBUG"));
		goto fail;
#endif
	} else {
#ifdef AG_DEBUG
		debugSave = 0;
#endif
	}
	if (AG_ObjectGetInheritHier(ob, &hier, &nHier) == -1) {
		goto fail_dbg;
	}
	for (i = 0; i < nHier; i++) {
#ifdef DEBUG_SERIALIZATION
		Debug(ob, "Loading as %s\n", hier[i]->name);
#endif
		if (hier[i]->load == NULL)
			continue;
		if (hier[i]->load(ob, ds, &ver) == -1) {
#ifdef AG_VERBOSITY
			AG_SetError("<0x%x>: %s", (Uint)AG_Tell(ds), AG_GetError());
#else
			AG_SetErrorS("E18");
#endif
			free(hier);
			goto fail_dbg;
		}
	}
	free(hier);

#ifdef AG_DEBUG
	if (ob->flags & AG_OBJECT_DEBUG_DATA)
		AG_SetSourceDebug(ds, debugSave);
#endif
	AG_ObjectUnlock(ob);
	return (0);
fail_dbg:
#ifdef AG_DEBUG
	if (ob->flags & AG_OBJECT_DEBUG_DATA)
		AG_SetSourceDebug(ds, debugSave);
#endif
fail:
	AG_ObjectReset(ob);
	AG_ObjectUnlock(ob);
	return (-1);
}

/* Archive an object to a file. */
int
AG_ObjectSaveToFile(void *p, const char *pPath)
{
	char dirPath[AG_PATHNAME_MAX];
	char path[AG_PATHNAME_MAX];
	char name[AG_OBJECT_PATH_MAX];
	AG_Object *ob = p;
	AG_DataSource *ds;
	int hasArchivePath;

	AG_LockVFS(ob);
	AG_ObjectLock(ob);

	AG_ObjectCopyName(ob, name, sizeof(name));
	hasArchivePath = AG_Defined(ob, "archive-path");

	if (pPath != NULL) {
		Strlcpy(path, pPath, sizeof(path));
	} else if (!hasArchivePath) {
		/*
		 * Create the save directory if needed (but never do this
		 * if an archive-path is set).
		 */
		if (AG_ConfigGetPath(AG_CONFIG_PATH_DATA, 0, dirPath, sizeof(dirPath)) >= sizeof(dirPath) ||
		    Strlcat(dirPath, name, sizeof(dirPath)) >= sizeof(dirPath)) {
			AG_SetErrorV("E4", _("Path overflow"));
			goto fail_unlock;
		}
		if (AG_FileExists(dirPath) == 0 &&
		    AG_MkPath(dirPath) == -1)
			goto fail_unlock;
	}

	if (pPath == NULL) {
		if (hasArchivePath) {
			AG_GetString(ob, "archive-path", path, sizeof(path));
		} else {
			Strlcpy(path, dirPath, sizeof(path));
			Strlcat(path, AG_PATHSEP, sizeof(path));
			Strlcat(path, ob->name, sizeof(path));
			Strlcat(path, ".", sizeof(path));
			Strlcat(path, ob->cls->name, sizeof(path));
		}
	}
#ifdef DEBUG_SERIALIZATION
	Debug(ob, "Saving object to %s\n", path);
#endif
	if (agObjectBackups) {
		BackupObjectFile(path);
	} else {
		AG_FileDelete(path);
	}
	if ((ds = AG_OpenFile(path, "wb")) == NULL) {
		goto fail_unlock;
	}
	if (AG_ObjectSerialize(ob, ds) == -1) {
		goto fail;
	}
	AG_CloseFile(ds);
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (0);
fail:
	AG_CloseFile(ds);
fail_unlock:
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (-1);
}

/* Shorthand for AG_ObjectSaveToFile() */
int
AG_ObjectSave(void *p)
{
	return AG_ObjectSaveToFile(p, NULL);
}

/* Load an object from an AG_Db database entry. */
int
AG_ObjectLoadFromDB(void *obj, AG_Db *db, const AG_Dbt *key)
{
	AG_DataSource *ds;
	AG_Dbt val;

	if (AG_DbGet(db, key, &val) == -1) {
		return (-1);
	}
	if ((ds = AG_OpenCore(val.data, val.size)) == NULL) {
		return (-1);
	}
	if (AG_ObjectUnserialize(obj, ds) == -1) {
		AG_CloseCore(ds);
		return (-1);
	}
	return (0);
}

/* Archive an object to an AG_Db database entry. */
int
AG_ObjectSaveToDB(void *pObj, AG_Db *db, const AG_Dbt *key)
{
	AG_Object *obj = pObj;
	AG_DataSource *ds;
	AG_Dbt dbKey, dbVal;
	int rv;

	if ((ds = AG_OpenAutoCore()) == NULL)
		return (-1);

	AG_LockVFS(obj);
	AG_ObjectLock(obj);
	rv = AG_ObjectSerialize(obj, ds);
	AG_ObjectUnlock(obj);
	AG_UnlockVFS(obj);

	if (rv == -1)
		goto fail;

	dbKey.data = obj->name;
	dbKey.size = strlen(obj->name)+1;
	dbVal.data = AG_CORE_SOURCE(ds)->data;
	dbVal.size = AG_CORE_SOURCE(ds)->size;
	rv = AG_DbPut(db, &dbKey, &dbVal);
	AG_CloseAutoCore(ds);
	return (rv);
fail:
	AG_CloseAutoCore(ds);
	return (-1);
}

#endif /* AG_SERIALIZATION */

/*
 * Change the name of an object (C string).
 * The parent VFS, if any, must be locked.
 */
void
AG_ObjectSetNameS(void *p, const char *name)
{
	AG_Object *ob = p;
	char *c;

	AG_ObjectLock(ob);
	if (name == NULL) {
		ob->name[0] = '\0';
	} else {
#ifdef AG_DEBUG
		if (Strlcpy(ob->name, name, sizeof(ob->name)) >= sizeof(ob->name))
			Verbose("Truncated object name: \"%s\"", ob->name);
#else
		Strlcpy(ob->name, name, sizeof(ob->name));
#endif
		for (c = &ob->name[0]; *c != '\0'; c++) {
			if (*c == '/' || *c == '\\')		/* Pathname separator */
				*c = '_';
		}
	}
	AG_ObjectUnlock(ob);
}

/*
 * Change the name of an object (format string).
 * The parent VFS, if any, must be locked.
 */
void
AG_ObjectSetName(void *p, const char *fmt, ...)
{
	AG_Object *ob = p;
	va_list ap;
	char *c;

	AG_ObjectLock(ob);
	if (fmt != NULL) {
		va_start(ap, fmt);
		Vsnprintf(ob->name, sizeof(ob->name), fmt, ap);
		va_end(ap);
	} else {
		ob->name[0] = '\0';
	}
	for (c = &ob->name[0]; *c != '\0'; c++) {
		if (*c == '/' || *c == '\\')		/* Pathname separator */
			*c = '_';
	}
	AG_ObjectUnlock(ob);
}

#if AG_MODEL != AG_SMALL
/* Move an object towards the head of its parent's children list. */
void
AG_ObjectMoveUp(void *p)
{
	AG_Object *ob = p, *prev;
	AG_Object *parent = ob->parent;

	AG_LockVFS(parent);
	if (parent != NULL && ob != TAILQ_FIRST(&parent->children)) {
		prev = TAILQ_PREV(ob, ag_objectq, cobjs);
		TAILQ_REMOVE(&parent->children, ob, cobjs);
		TAILQ_INSERT_BEFORE(prev, ob, cobjs);
	}
	AG_UnlockVFS(parent);
}

/* Move an object towards the tail of its parent's children list. */
void
AG_ObjectMoveDown(void *p)
{
	AG_Object *ob = p;
	AG_Object *parent = ob->parent;
	AG_Object *next = TAILQ_NEXT(ob, cobjs);

	AG_LockVFS(parent);
	if (parent != NULL && next != NULL) {
		TAILQ_REMOVE(&parent->children, ob, cobjs);
		TAILQ_INSERT_AFTER(&parent->children, next, ob, cobjs);
	}
	AG_UnlockVFS(parent);
}

/* Move an object to the head of its parent's children list. */
void
AG_ObjectMoveToHead(void *p)
{
	AG_Object *ob = p;
	AG_Object *parent = ob->parent;

	AG_LockVFS(parent);
	if (parent != NULL) {
		TAILQ_REMOVE(&parent->children, ob, cobjs);
		TAILQ_INSERT_HEAD(&parent->children, ob, cobjs);
	}
	AG_UnlockVFS(parent);
}

/* Move an object to the tail of its parent's children list. */
void
AG_ObjectMoveToTail(void *p)
{
	AG_Object *ob = p;
	AG_Object *parent = ob->parent;

	AG_LockVFS(parent);
	if (parent != NULL) {
		TAILQ_REMOVE(&parent->children, ob, cobjs);
		TAILQ_INSERT_TAIL(&parent->children, ob, cobjs);
	}
	AG_UnlockVFS(parent);
}
#endif /* !AG_SMALL */

#ifdef AG_SERIALIZATION
/*
 * Remove the data files of an object and its children.
 * The object's VFS must be locked.
 */
void
AG_ObjectUnlinkDatafiles(void *p)
{
	char path[AG_PATHNAME_MAX];
	AG_Object *ob = p, *cob;

	AG_ObjectLock(ob);
	if (AG_ObjectCopyFilename(ob, path, sizeof(path)) == 0) {
		AG_FileDelete(path);
	}
	TAILQ_FOREACH(cob, &ob->children, cobjs) {
		AG_ObjectUnlinkDatafiles(cob);
	}
	if (AG_ObjectCopyDirname(ob, path, sizeof(path)) == 0) {
		AG_RmDir(path);
	}
	AG_ObjectUnlock(ob);
}

/*
 * Check whether the dataset of the given object or any of its children are
 * different with respect to the last archive. The result is only valid as
 * long as the object and VFS are locked, and this assumes that no other
 * application is concurrently accessing the datafiles.
 */
int
AG_ObjectChangedAll(void *p)
{
	AG_Object *ob = p, *cob;

	AG_LockVFS(ob);
	AG_ObjectLock(ob);

	if (AG_ObjectChanged(ob) == 1) {
		goto changed;
	}
	TAILQ_FOREACH(cob, &ob->children, cobjs) {
		if (AG_ObjectChangedAll(cob) == 1)
			goto changed;
	}

	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (0);
changed:
	AG_ObjectUnlock(ob);
	AG_UnlockVFS(ob);
	return (1);
}

/*
 * Check whether the dataset of the given object is different with respect
 * to its last archive. The result is only valid as long as the object is
 * locked, and this assumes no other application is concurrently accessing
 * the datafiles.
 */
int
AG_ObjectChanged(void *p)
{
#if AG_MODEL == AG_SMALL
	char *bufCur = Malloc(AG_BUFFER_MAX);
	char *bufLast = Malloc(AG_BUFFER_MAX);
#else
	char bufCur[AG_BUFFER_MAX];
	char bufLast[AG_BUFFER_MAX];
#endif
	char pathCur[AG_PATHNAME_MAX];
	char pathLast[AG_PATHNAME_MAX];
	AG_Object *ob = p;
	FILE *fLast, *fCur;
	AG_Size rvLast, rvCur;
	int rv = 0;

	AG_ObjectLock(ob);

	AG_ObjectCopyFilename(ob, pathLast, sizeof(pathLast));
	if ((fLast = fopen(pathLast, "r")) == NULL) {
		rv = 1;
		goto out;
	}
	AG_ConfigGetPath(AG_CONFIG_PATH_TEMP, 0, pathCur, sizeof(pathCur));
	Strlcat(pathCur, AG_PATHSEP, sizeof(pathCur));
	Strlcat(pathCur, "_chg.", sizeof(pathCur));
	Strlcat(pathCur, ob->name, sizeof(pathCur));
	if (AG_ObjectSaveToFile(ob, pathCur) == -1) {
		fclose(fLast);
		rv = 1;
		goto out;
	}
	if ((fCur = fopen(pathCur, "r")) == NULL) {
		fclose(fLast);
		rv = 1;
		goto out;
	}
	for (;;) {
		rvLast = fread(bufLast, 1, sizeof(bufLast), fLast);
		rvCur = fread(bufCur, 1, sizeof(bufCur), fCur);
	
		if (rvLast != rvCur ||
		   (rvLast > 0 && memcmp(bufLast, bufCur, rvLast) != 0)) {
			rv = 1;
			break;
		}
		if (feof(fLast)) {
			if (!feof(fCur)) { rv = 1; }
			break;
		}
		if (feof(fCur)) {
			if (!feof(fLast)) { rv = 1; }
			break;
		}
	}
	AG_FileDelete(pathCur);
	fclose(fCur);
	fclose(fLast);
out:
	AG_ObjectUnlock(ob);
#if AG_MODEL == AG_SMALL
	free(bufCur);
	free(bufLast);
#endif
	return (rv);
}
#endif /* AG_SERIALIZATION */

/*
 * Generate an object name that is unique in the given parent object. The
 * name is only guaranteed to remain unique as long as the VFS and parent
 * object are locked.
 */
void
AG_ObjectGenName(void *p, AG_ObjectClass *C, char *name, AG_Size len)
{
	AG_Object *pobj = p, *chld;
	char *ccBase, *cc, *dBase;
	Uint i;

	if ((ccBase = strchr(C->name, '_')) != NULL) {	/* Skip any prefix */
		ccBase++;
	} else {
		ccBase = C->name;
	}
	if (len < 4) {
		return;
	}
	name[0] = tolower(ccBase[0]);
	len--;
	for (cc = &ccBase[1], dBase = &name[1];
	    *cc != '\0' && len > 0;
	     cc++, dBase++) {
		*dBase = tolower(*cc);
		len--;
	}
	*dBase = '\0';
	i = 0;
tryname:
	StrlcpyUint(dBase, i, len);
	if (pobj != NULL) {
		AG_LockVFS(pobj);
		TAILQ_FOREACH(chld, &pobj->children, cobjs) {
			if (strcmp(chld->name, name) == 0)
				break;
		}
		AG_UnlockVFS(pobj);

		if (chld != NULL) {
			i++;
			goto tryname;
		}
	}
}

#if AG_MODEL != AG_SMALL
/* Generate a unique object name using the specified prefix. */
void
AG_ObjectGenNamePfx(void *p, const char *pfx, char *name, AG_Size len)
{
	AG_Object *pobj = p;
	Uint i = 1;
	AG_Object *ch;

tryname:
	Strlcpy(name, pfx, len);
	StrlcatUint(name, i, len);
	if (pobj != NULL) {
		AG_LockVFS(pobj);
		TAILQ_FOREACH(ch, &pobj->children, cobjs) {
			if (strcmp(ch->name, name) == 0)
				break;
		}
		AG_UnlockVFS(pobj);
		if (ch != NULL) {
			i++;
			goto tryname;
		}
	}
}
#endif /* !AG_SMALL */

#ifdef AG_LEGACY
void
AG_ObjectSetArchivePath(void *obj, const char *path)
{
	AG_SetString(obj, "archive-path", path);
}
#endif /* !AG_LEGACY */

static void
InitClass(AG_ObjectClass *_Nonnull C, const char *_Nonnull hier)
{
	const char *c;
	AG_Size rv;

	if (Strlcpy(C->hier, hier, sizeof(C->hier)) >= sizeof(C->hier))
		goto too_big;

	if ((c = strrchr(hier, ':')) != NULL && c[1] != '\0') {
		rv = Strlcpy(C->name, &c[1], sizeof(C->name));
	} else {
		rv = Strlcpy(C->name, hier, sizeof(C->name));
	}
	if (rv >= sizeof(C->name)) {
		goto too_big;
	}
	TAILQ_INIT(&C->sub);
	return;
too_big:
	AG_FatalError("Class name overflow");
}

/*
 * Initialize the object class description table.
 * Invoked internally by AG_InitCore().
 */
void
AG_InitClassTbl(void)
{
	AG_Variable V;

#ifdef AG_NAMESPACES
	agNamespaceTbl = Malloc(sizeof(AG_Namespace));
	agNamespaceCount = 0;
	AG_RegisterNamespace("Agar", "AG_", "https://libagar.org/");
#endif
#ifdef AG_ENABLE_DSO
	agModuleDirs = Malloc(sizeof(char *));
	agModuleDirCount = 0;
#endif
	/* Initialize the class tree */
	InitClass(&agObjectClass, "AG_Object");
#ifdef AG_ENABLE_DSO
	agObjectClass.libs[0] = '\0';
#endif
	/* Initialize the class table. */
	agClassTbl = AG_TblNew(AG_OBJECT_CLASSTBLSIZE, 0);

	/* AG_Object -> agObjectClass */
	AG_InitPointer(&V, &agObjectClass);
	if (AG_TblInsert(agClassTbl, "AG_Object", &V) == -1)
		AG_FatalError(NULL);

	AG_MutexInitRecursive(&agClassLock);
}

/*
 * Release the object class description table.
 * Invoked internally by AG_Destroy().
 */
void
AG_DestroyClassTbl(void)
{
#ifdef AG_NAMESPACES
	free(agNamespaceTbl);
	agNamespaceTbl = NULL;
	agNamespaceCount = 0;
#endif
#ifdef AG_ENABLE_DSO
	{
		int i;

		for (i = 0; i < agModuleDirCount; i++) {
			free(agModuleDirs[i]);
		}
		free(agModuleDirs);
		agModuleDirs = NULL;
		agModuleDirCount = 0;
	}
#endif
	AG_TblDestroy(agClassTbl);
	free(agClassTbl); agClassTbl = NULL;
	
	AG_MutexDestroy(&agClassLock);
}

#ifdef AG_NAMESPACES
/*
 * Parse a class specification string either in the conventional form
 * "AG_Class1:AG_Class2:...[@lib]", or in "Agar(Class1:Class2:...)[@lib]"
 * format if NAMESPACE is supported.
 */
int
AG_ParseClassSpec(AG_ObjectClassSpec *cs, const char *spec)
{
	char buf[AG_OBJECT_HIER_MAX], *pBuf, *pTok;
	char nsName[AG_OBJECT_HIER_MAX], *pNsName = nsName;
	const char *s, *p, *pOpen = NULL;
	char *c;
	AG_Namespace *ns;
	AG_Size rv;
	int iTok, i=0, len=0;

	cs->hier[0] = '\0';
	cs->name[0] = '\0';
# ifdef AG_ENABLE_DSO
	cs->libs[0] = '\0';
# endif
	*pNsName = '\0';
	for (s = &spec[0]; *s != '\0'; s++) {
		if (++len >= AG_OBJECT_HIER_MAX) {
			AG_SetErrorV("E22", _("Class is too long"));
			return (-1);
		}
		if (s[0] == '(' && s[1] != '\0') {
			if (pOpen || nsName[0] == '\0') {
				AG_SetErrorV("E23", _("Class syntax error"));
				return (-1);
			}
			pOpen = &s[1];
			continue;
		}
		if (pOpen == NULL) {
			if (*s != ':') {
				*pNsName = *s;
				pNsName++;
			}
# ifdef AG_ENABLE_DSO
			if (s[0] == '@' && s[1] != '\0')
				if (Strlcpy(cs->libs, &s[1], sizeof(cs->libs))
				    >= sizeof(cs->libs))
					AG_FatalError("DSO name overflow");
# endif
		}
		if (*s == ')') {
			if ((s - pOpen) == 0) {
				pOpen = NULL;
				continue;
			}
			*pNsName = '\0';
			pNsName = &nsName[0];
			if ((ns = AG_GetNamespace(nsName)) == NULL) {
				AG_SetErrorV("E24", _("No such namespace"));
				return (-1);
			}
			for (p = pOpen, iTok = 0;
			     (p < s) && (iTok < sizeof(buf)-1);
			     p++, iTok++) {
				buf[iTok] = *p;
			}
			buf[iTok] = '\0';
			for (pBuf = buf;
			     (pTok = Strsep(&pBuf, ":")) != NULL; ) {
				i += Strlcpy(&cs->hier[i], ns->pfx, sizeof(cs->hier)-i);
				i += Strlcpy(&cs->hier[i], pTok, sizeof(cs->hier)-i);
				i += Strlcpy(&cs->hier[i], ":", sizeof(cs->hier)-i);
			}
			pOpen = NULL;
			continue;
		}
	}

	/* Fill in the "hier" (full hierarchy) field. */
	if (i > 0 && cs->hier[i-1] == ':') {
		cs->hier[i-1] = '\0';                     /* Strip last ':' */
	}
	if (i == 0) {                                        /* Flat format */
#ifdef AG_DEBUG
		if (Strlcpy(cs->hier, spec, sizeof(cs->hier)) >= sizeof(cs->hier))
			AG_FatalError("Class hierarchy overflow");
#else
		Strlcpy(cs->hier, spec, sizeof(cs->hier));
#endif
	} else {
		cs->hier[i] = '\0';
	}
	if ((c = strrchr(cs->hier, '@')) != NULL)
		*c = '\0';

	/* Fill in the "name" (short name) field. */
	if ((c = strrchr(cs->hier, ':')) != NULL && c[1] != '\0') {
		rv = Strlcpy(cs->name, &c[1], sizeof(cs->name));
	} else {
		rv = Strlcpy(cs->name, cs->hier, sizeof(cs->name));
	}
	if (rv >= sizeof(cs->name)) {
		AG_FatalError("Class name overflow");
	}
	if ((c = strrchr(cs->name, '@')) != NULL) {
		*c = '\0';
	}
	
	/* Fill in the "spec" (full hierarchy + @libs) field. */
	Strlcpy(cs->spec, cs->hier, sizeof(cs->spec));

	/* Fill in the "libs" (DSO modules) field. */
# ifdef AG_ENABLE_DSO
	if (cs->libs[0] != '\0')
		if (Strlcat(cs->spec, cs->libs, sizeof(cs->spec)) >=
		    sizeof(cs->spec))
			AG_FatalError("DSO name overflow");
# endif
	return (0);
}
#else /* !AG_NAMESPACES */
/*
 * Parse a class specification string only in the conventional format
 * "AG_Class1:AG_Class2:...[@lib]" (no namespace support).
 */
int
AG_ParseClassSpec(AG_ObjectClassSpec *cs, const char *spec)
{
	char *c;

	Strlcpy(cs->hier, spec, sizeof(cs->hier));
	Strlcpy(cs->spec, spec, sizeof(cs->spec));
	
	if ((c = strchr(cs->hier, '@')) != NULL) {
# ifdef AG_ENABLE_DSO
		Strlcpy(cs->libs, &c[1], sizeof(cs->libs));
# endif
		*c = '\0';
	}
# ifdef AG_ENABLE_DSO
	else {
		cs->libs[0] = '\0';
	}
# endif
	if ((c = strrchr(spec, ':')) != NULL && c[1] != '\0') {
		Strlcpy(cs->name, &c[1], sizeof(cs->name));
	} else {
		Strlcpy(cs->name, spec, sizeof(cs->name));
	}
	return (0);
}
#endif /* !AG_NAMESPACES */

/* Register object class as described by the given AG_ObjectClass structure. */
void
AG_RegisterClass(void *p)
{
	AG_ObjectClass *C = p;
	AG_ObjectClassSpec cs;
	AG_Variable V;
	char *s;
	
	if (AG_ParseClassSpec(&cs, C->hier) == -1) {
		AG_FatalError(NULL);
	}
	InitClass(C, cs.hier);
#ifdef AG_ENABLE_DSO
	Strlcpy(C->libs, cs.libs, sizeof(C->libs));
#endif
#ifdef AG_DEBUG_CLASSES
	Debug(NULL, "[ Register %s (%s) ]\n", cs.name, cs.hier);
#endif
	AG_MutexLock(&agClassLock);

	/* Insert into the class tree. */
	if ((s = strrchr(cs.hier, ':')) != NULL) {
		*s = '\0';
		if ((C->super = AG_LookupClass(cs.hier)) == NULL)
			AG_FatalError(NULL);
	} else {
		C->super = &agObjectClass;	/* Base AG_Object class */
	}
	TAILQ_INSERT_TAIL(&C->super->sub, C, subclasses);

	/* Insert into the class table. */
	AG_InitPointer(&V, C);
	if (AG_TblInsert(agClassTbl, C->hier, &V) == -1)
		AG_FatalError(NULL);

	AG_MutexUnlock(&agClassLock);
}

/* Unregister an object class. */
void
AG_UnregisterClass(void *p)
{
	AG_ObjectClass *C = p;
	AG_ObjectClass *Csuper = C->super;
	Uint h;

	AG_MutexLock(&agClassLock);
	h = AG_TblHash(agClassTbl, C->hier);
	if (AG_TblExistsHash(agClassTbl, h, C->hier)) {
#ifdef AG_DEBUG_CLASSES
		Debug(NULL, "[ Unregister %s ]\n", C->name);
#endif
		/* Remove from the class tree. */
		TAILQ_REMOVE(&Csuper->sub, C, subclasses);
		C->super = NULL;

		/* Remove from the class table. */
		AG_TblDeleteHash(agClassTbl, h, C->hier);
	}
	AG_MutexUnlock(&agClassLock);
}

#if AG_MODEL != AG_SMALL
/*
 * Allocate, initialize and zero an AG_ObjectClass (or derivative thereof).
 *
 * This gives an alternative to passing a statically-initialized AG_ObjectClass
 * to AG_RegisterClass(). Here we auto-allocate it instead,
 * and the methods can be set using AG_ClassSet{Init,Reset,Destroy,...}().
 */
void *
AG_CreateClass(const char *hier, AG_Size objectSize, AG_Size classSize,
    Uint major, Uint minor)
{
	AG_ObjectClass *C;

	if ((C = TryMalloc(classSize)) == NULL) {
		return (NULL);
	}
	memset(C, 0, classSize);
	if (Strlcpy(C->hier, hier, sizeof(C->hier)) >= sizeof(C->hier)) {
		AG_FatalError("Class hierarchy overflow");
	}
	C->size = objectSize;
	C->ver.major = major;
	C->ver.minor = minor;
	AG_RegisterClass(C);
	return (C);
}

/* Set Object class operations procedurally. */
#define AG_CLASS_SET_FN_BODY(fnName, fnType, op) \
fnType fnName (void *Cp, fnType fn) { \
	AG_ObjectClass *C = (AG_ObjectClass *)Cp; \
	fnType fnOrig = C->op; \
	C->op = fn; \
	return (fnOrig); \
}
AG_CLASS_SET_FN_BODY(AG_ClassSetInit,    AG_ObjectInitFn,    init);
AG_CLASS_SET_FN_BODY(AG_ClassSetReset,   AG_ObjectResetFn,   reset);
AG_CLASS_SET_FN_BODY(AG_ClassSetDestroy, AG_ObjectDestroyFn, destroy);
AG_CLASS_SET_FN_BODY(AG_ClassSetLoad,    AG_ObjectLoadFn,    load);
AG_CLASS_SET_FN_BODY(AG_ClassSetSave,    AG_ObjectSaveFn,    save);
AG_CLASS_SET_FN_BODY(AG_ClassSetEdit,    AG_ObjectEditFn,    edit);
#undef AG_CLASS_SET_FN_BODY

/* Unregister and free an auto-allocated AG_ObjectClass (or derivative thereof) */
void
AG_DestroyClass(void *C)
{
	AG_UnregisterClass(C);
	free(C);
}
#endif /* !AG_SMALL */

/*
 * Lookup information about a registered object class.
 * Return a normalized class description (or NULL if no such class exists).
 */
AG_ObjectClass *
AG_LookupClass(const char *inSpec)
{
	AG_ObjectClassSpec cs;
	AG_Variable *V;

	if (inSpec[0] == '\0' ||
#ifdef AG_NAMESPACES
	    strcmp(inSpec, "Agar(Object)") == 0 ||
#endif
	    strcmp(inSpec, "AG_Object") == 0)
		return (&agObjectClass);

	if (AG_ParseClassSpec(&cs, inSpec) == -1)
		return (NULL);

	/* Look up the class table. */
	AG_MutexLock(&agClassLock);
	if ((V = AG_TblLookup(agClassTbl, cs.hier)) != NULL) {
		AG_MutexUnlock(&agClassLock);
		return ((AG_ObjectClass *)V->data.p);
	}
	AG_MutexUnlock(&agClassLock);

	AG_SetErrorV("E25", _("No such class"));
	return (NULL);
}

#ifdef AG_ENABLE_DSO
/*
 * Transform "PFX_Foo" string to "pfxFooClass".
 */
static int
GetClassSymbol(char *_Nonnull sym, AG_Size len,
    const AG_ObjectClassSpec *_Nonnull cs)
{
	char *d;
	const char *c;
	int inPfx = 1;
	AG_Size l = 0;

	for (c = &cs->name[0], d = &sym[0];
	     *c != '\0';
	     c++) {
		if (*c == '_') {
			inPfx = 0;
			continue;
		}
		if ((l+2) >= len) {
			goto toolong;
		}
		*d = inPfx ? (char) tolower((int) *c) : *c;
		d++;
		l++;
	}
	*d = '\0';
	if (Strlcat(sym, "Class", len) >= len) {
		goto toolong;
	}
	return (0);
toolong:
	AG_SetErrorS(_("Symbol is too long"));
	return (-1);
}

/*
 * Look for a "@libs" string in the class specification and scan module
 * directories for the required libraries. If they are found, bring them
 * into the current process's address space. If successful, look up the
 * "pfxFooClass" symbol and register the class.
 *
 * Multiple libraries can be specified with commas. The "pfxFooClass"
 * symbol is assumed to be defined in the first library in the list.
 */
AG_ObjectClass *
AG_LoadClass(const char *classSpec)
{
	AG_ObjectClassSpec cs;
	AG_ObjectClass *C;
	char *s, *lib;
	char sym[AG_OBJECT_HIER_MAX];
	AG_DSO *dso;
	void *pClass = NULL;
	int i;

	if (AG_ParseClassSpec(&cs, classSpec) == -1)
		return (NULL);
	
	AG_MutexLock(&agClassLock);

	if ((C = AG_LookupClass(cs.hier)) != NULL) {
		AG_MutexUnlock(&agClassLock);
		return (C);                     /* Found a registered class */
	}
	if (cs.libs[0] == '\0') {
		AG_SetError(_("Class " AGSI_BR_CYAN "%s" AGSI_RST " not found."),
		    cs.hier);
		goto fail;
	}
	for (i = 0, s = cs.libs;                 /* Attempt dynamic linking */
	    (lib = Strsep(&s, ", ")) != NULL;
	    i++) {
# ifdef AG_DEBUG_CLASSES
		Debug(NULL, "<%s>: Linking %s...", classSpec, lib);
# endif
		if ((dso = AG_LoadDSO(lib, 0)) == NULL) {
			AG_SetError("DSO(%s): %s", classSpec, AG_GetError());
			goto fail;
		}
		/* Look up "pfxFooClass" in the first library. */
		if (i == 0) {
			if (GetClassSymbol(sym, sizeof(sym), &cs) == -1) {
				goto fail;
			}
			if (AG_SymDSO(dso, sym, &pClass) == -1) {
				AG_UnloadDSO(dso);
				/* XXX TODO undo other DSOs we just loaded */
				goto fail;
			}
		}
# ifdef AG_DEBUG_CLASSES
		Debug(NULL, "OK\n");
# endif
	}
	if (pClass == NULL) {
		AG_SetError(_("<%s>: No library specified"), classSpec);
		goto fail;
	}
	AG_RegisterClass(pClass);

	AG_MutexUnlock(&agClassLock);
	return (pClass);
fail:
# ifdef AG_DEBUG_CLASSES
	Debug(NULL, "%s\n", AG_GetError());
# endif
	AG_MutexUnlock(&agClassLock);
	return (pClass);
}

/*
 * Unregister the given class and decrement the reference count / unload
 * related dynamically-linked libraries.
 */
void
AG_UnloadClass(AG_ObjectClass *C)
{
	char *s, *lib;
	AG_DSO *dso;
	
	AG_UnregisterClass(C);

	for (s = C->libs; (lib = Strsep(&s, ", ")) != NULL; ) {
		if ((dso = AG_LookupDSO(lib)) != NULL)
			AG_UnloadDSO(dso);
	}
}
#endif /* AG_ENABLE_DSO */

#ifdef AG_NAMESPACES
/* Register a new namespace. */
AG_Namespace *
AG_RegisterNamespace(const char *name, const char *pfx, const char *url)
{
	AG_Namespace *ns;

	agNamespaceTbl = Realloc(agNamespaceTbl,
	    (agNamespaceCount+1)*sizeof(AG_Namespace));
	ns = &agNamespaceTbl[agNamespaceCount++];
	ns->name = name;
	ns->pfx = pfx;
	ns->url = url;
	return (ns);
}

/* Unregister a namespace. */
void
AG_UnregisterNamespace(const char *name)
{
	int i;

	for (i = 0; i < agNamespaceCount; i++) {
		if (strcmp(agNamespaceTbl[i].name, name) == 0)
			break;
	}
	if (i < agNamespaceCount) {
		if (i < agNamespaceCount-1) {
			memmove(&agNamespaceTbl[i], &agNamespaceTbl[i+1],
			    (agNamespaceCount-i-1)*sizeof(AG_Namespace));
		}
		agNamespaceCount--;
	}
}
#endif /* AG_NAMESPACES */

#ifdef AG_ENABLE_DSO
/* Register a new module directory path. */
void
AG_RegisterModuleDirectory(const char *path)
{
	char *s, *p;

	agModuleDirs = Realloc(agModuleDirs,
	    (agModuleDirCount+1)*sizeof(char *));
	agModuleDirs[agModuleDirCount++] = s = Strdup(path);
	if (*(p = &s[strlen(s)-1]) == AG_PATHSEPCHAR)
		*p = '\0';
}

/* Unregister a module directory path. */
void
AG_UnregisterModuleDirectory(const char *path)
{
	int i;

	for (i = 0; i < agModuleDirCount; i++) {
		if (strcmp(agModuleDirs[i], path) == 0)
			break;
	}
	if (i < agModuleDirCount) {
		free(agModuleDirs[i]);
		if (i < agModuleDirCount-1) {
			memmove(&agModuleDirs[i], &agModuleDirs[i+1],
			    (agModuleDirCount-i-1)*sizeof(char *));
		}
		agModuleDirCount--;
	}
}
#endif /* AG_ENABLE_DSO */

/* General case fallback for AG_ClassIsNamed() */
int
AG_ClassIsNamedGeneral(const AG_ObjectClass *C, const char *cn)
{
	char cname[AG_OBJECT_HIER_MAX], *cp, *c;
	char nname[AG_OBJECT_HIER_MAX], *np, *s;

	Strlcpy(cname, cn, sizeof(cname));
	Strlcpy(nname, C->hier, sizeof(nname));
	cp = cname;
	np = nname;

	while ((c = Strsep(&cp, ":")) != NULL &&
	       (s = Strsep(&np, ":")) != NULL) {
		if (c[0] == '*' && c[1] == '\0')
			continue;
		if (strcmp(c, s) != 0)
			return (0);
	}
	return (1);
}

/*
 * Return an array of class description pointers ("AG_ObjectClass *") for
 * each class in the inheritance hierarchy of obj. For example:
 *
 *   "AG_Widget:AG_Box:AG_Titlebar" -> { &agWidgetClass,
 *                                       &agBoxClass,
 *                                       &agTitlebarClass }
 *
 * The caller should release the returned array using free() after use.
 */
int
AG_ObjectGetInheritHier(void *obj, AG_ObjectClass ***hier, int *nHier)
{
	char cname[AG_OBJECT_HIER_MAX], *c;
	AG_ObjectClass *C, **pHier;
	int i, stop = 0;

	if (AGOBJECT(obj)->cls->hier[0] == '\0') {
		(*nHier) = 0;
		return (0);
	}
	(*nHier) = 1;
	Strlcpy(cname, AGOBJECT(obj)->cls->hier, sizeof(cname));
	for (c = &cname[0]; *c != '\0'; c++) {
		if (*c == ':')
			(*nHier)++;
	}
	pHier = (*hier) = Malloc((*nHier)*sizeof(AG_ObjectClass *));
	for (c = &cname[0], i = 0; ; c++) {
		if (*c != ':' && *c != '\0') {
			continue;
		}
		if (*c == '\0') {
			stop++;
		} else {
			*c = '\0';
		}
		if ((C = AG_LookupClass(cname)) == NULL) {
			AG_SetError(
			    _("No such class " AGSI_BR_CYAN "%s" AGSI_RST ". "
			      "Missing AG_RegisterClass(3) call?"),
			    AGOBJECT(obj)->cls->hier);
			free(pHier);
			return (-1);
		}
		*c = ':';
		pHier[i++] = C;
		
		if (stop)
			break;
	}
	return (0);
}
