#include <exec/memory.h>
#include <proto/dos.h>
#include <proto/exec.h>
#include <proto/utility.h>
#include <string.h>


/* Information about a directory */
struct DirNode
{
	struct Node	Link;
	struct DirNode	*Parent;	/* Parent dirnode or NULL */
	BPTR		Dir;		/* Lock on directory */
};


/* Information about a file. ln_Name holds the name of the file */
struct FileNode
{
	struct Node	Link;
	BPTR		Dir;		/* Pointer to a directory lock in a DirNode */
};


/* Information about a MultiAssign node */
struct maData
{
	struct DevProc	*DevProc;
	BPTR		Dir;
	BPTR		OldDir;
	STRPTR		Path;
	STRPTR		Rest;
};


/* Return string after first colon, or NULL if there is no colon */
static STRPTR
AfterColon( STRPTR name )
{
	for( ; *name; ++name )
	{
		if( *name == ':' )
		{
			return( name + 1 );
		}
	}

	return( NULL );
}


/* Free a MultiAssign data node, allocated by InitMA */
static VOID
FreeMA( struct maData *data )
{
	if( data )
	{
		FreeDeviceProc( data->DevProc );
		CurrentDir( data->OldDir );
		UnLock( data->Dir );
		FreeVec( data );
	}
}


/* Prepare for MultiAssign processing. Returns NULL if there is no
 * MultiAssign for this path or there was an error. IoErr() will
 * be 0 if all OK, non-zero otherwise
 */
static struct maData *
InitMA( STRPTR path )
{
	struct maData	*data = NULL;
	STRPTR	rest;

	SetIoErr( 0 );

	if( rest = AfterColon( path ) )
	{
		/* Colon in the path. Might be a multi-assign. */
		if( data = AllocVec( sizeof( struct maData ), MEMF_ANY | MEMF_CLEAR ) )
		{
			CurrentDir( data->OldDir = CurrentDir( 0 ) );
			data->Path = path;
			data->Rest = rest;

			/* Get the DevProc for the initial assign */
			if( data->DevProc = GetDeviceProc( path, NULL ) )
			{
				/* All ok */
				SetIoErr( 0 );
			}
			else
			{
				FreeMA( data );
				return( NULL );
			}
		}
	}

	return( data );
}


/* Handle multi assigns. Call this after a drawer haven been processed, to
 * see if any more drawer needs processing.
 *
 * Returns 1 if a new drawer has been found. We have then CD'd to that
 * drawer. Returns 0 if everything is done, and -1 in case of errors. Note
 * that all errors won't be caught, due to a bug in GetDeviceProc (it
 * doesn't set error codes as documented).
 *
 * Based on an AmigaMail example.
 */
LONG
HandleMA( struct maData *data )
{
	LONG	rc = 0;

	/* Process the next DevProc, if any */
	if( data && ( data->DevProc = GetDeviceProc( data->Path, data->DevProc ) ) )
	{
		struct MsgPort	*oldFS;
		BPTR	dir;

		/* In case dp->dvp_Lock is NULL, we set the new file
		 * system task as the current one.
		 */
		oldFS = SetFileSysTask( data->DevProc->dvp_Port );

		/* Lock() locks relative to the current directory and falls
		 * back to the root of the current file system if dp->dvp_Lock
		 * is NULL.
		 */
		CurrentDir( data->DevProc->dvp_Lock );

		if( dir = Lock( data->Rest, SHARED_LOCK ) )
		{
			/* Set up for more processing */
			CurrentDir( dir );
			UnLock( data->Dir );
			data->Dir = dir;
			rc = 1;
		}
		else
		{
			CurrentDir( data->OldDir );
			rc = -1;
		}

		/* Reset the process' default filesystem port. */
		SetFileSysTask( oldFS );
	}

	/* We can't check for errors here, since GetDeviceProc() doesn't
	 * set IoErr() as documented.
	 */

	return( rc );
}


/* Return the DirEntryType field of the FileInfoBlock for the specified
 * file. Returns 0 in case of errors.
 */
static LONG
FileEntryType( STRPTR name )
{
	struct FileInfoBlock	*fib;
	BPTR	lock;
        LONG	rc = 0;

	if( fib = AllocDosObject( DOS_FIB, NULL ) )
	{
		if( lock = Lock( name, SHARED_LOCK ) )
		{
			if( Examine( lock, fib ) )
			{
				rc = fib->fib_DirEntryType;
			}

			UnLock( lock );
		}

		FreeDosObject( DOS_FIB, fib );
	}

	return( rc );
}


/* Free a directory node, allocated by AllocDirNode */
static VOID
FreeDirNode( struct DirNode *node )
{
	UnLock( node->Dir );
	FreeVec( node );
}


/* Allocate a directory node, used to keep track of all visited
 * directories
 */
static struct DirNode *
AllocDirNode( struct List *list, BPTR dir, struct DirNode *parent )
{
	struct DirNode	*node;

	if( node = AllocVec( sizeof( struct DirNode ), MEMF_ANY ) )
	{
		if( node->Dir = DupLock( dir ) )
		{
			node->Parent = parent;
			AddTail( list, ( struct Node * ) node );
		}
		else
		{
			FreeVec( node );
			node = NULL;
		}
	}

	return( node );
}


/* Free a file node, allocated by AllocFileNode */
static VOID
FreeFileNode( struct FileNode *node )
{
	FreeVec( node );
}


/* Allocate a file node, with the specified name in dir, and add it to the
 * list
 */
static struct FileNode *
AllocFileNode( struct List *list, STRPTR name, BPTR dir )
{
	struct FileNode	*node;

	if( node = AllocVec( ( ULONG ) sizeof( struct FileNode ) + strlen( name ) + 1, MEMF_ANY ) )
	{
		node->Link.ln_Name = ( STRPTR ) ( node + 1 );
		strcpy( node->Link.ln_Name, name );
		node->Dir = dir;
		AddTail( list, ( struct Node * ) node );
	}

	return( node );
}


/* Process the pattern, collect all files, and then process each file found. */
static LONG
CollectProcessPattern( STRPTR name, LONG all, struct Hook *hook )
{
	struct List		files, dirs;
	struct DirNode		*current = NULL;
	struct FileNode		*file;
	struct AnchorPath	*ap;
	BPTR	dir = ( BPTR ) -1;
	LONG	err, warn = 0, rc = RETURN_OK;
	BOOL	enter = FALSE;

	NewList( &files );
	NewList( &dirs );

	/* Set up for MatchFirst/MatchNext */
	if( !( ap = AllocVec( sizeof( struct AnchorPath ), MEMF_ANY | MEMF_CLEAR ) ) )
	{
		return( RETURN_FAIL );
	}

	ap->ap_BreakBits = SIGBREAKF_CTRL_C;
	ap->ap_Flags = ( BYTE ) ( APF_FollowHLinks );
	err = MatchFirst( name, ap );

	if( !err )
	{
		/* If no errors, get a dir node for the "parent" */
		if( !( current = AllocDirNode( &dirs, ap->ap_Current->an_Lock, NULL ) ) )
		{
			rc = RETURN_FAIL;
		}

		/* CD to the dir in question */
		dir = CurrentDir( ap->ap_Current->an_Lock );
	}

	while( !err && ( rc <= RETURN_WARN ) )
	{
		CurrentDir( ap->ap_Current->an_Lock );

		if( enter )
		{
			if( !( current = AllocDirNode( &dirs, ap->ap_Current->an_Lock, current ) ) )
			{
				rc = RETURN_FAIL;
			}

			enter = FALSE;
		}

		if( ap->ap_Info.fib_DirEntryType == ST_SOFTLINK )
		{
			/* Soft link, so try to get real entry type */
			ap->ap_Info.fib_DirEntryType = FileEntryType( ap->ap_Info.fib_FileName );
		}

		/* Enter drawers if requested */
		if( ( ap->ap_Info.fib_DirEntryType > 0 ) && all )
		{
			if( !( ap->ap_Flags & APF_DIDDIR ) )
			{
				ap->ap_Flags |= APF_DODIR;
				enter = TRUE;
			}
			else if( current->Parent )
			{
				current = current->Parent;
			}
			else
			{
				/* There better be a parent... */
				rc = RETURN_FAIL;
			}

			ap->ap_Flags &= ~APF_DIDDIR;
		}
		else if( ap->ap_Info.fib_DirEntryType < 0 )
		{
			if( !( AllocFileNode( &files, ap->ap_Info.fib_FileName, current->Dir ) ) )
			{
				rc = RETURN_FAIL;
			}
		}

		if( rc <= RETURN_WARN )
		{
			err = MatchNext( ap );
		}
	}

	if( dir != -1 )
	{
		CurrentDir( dir );
	}

	MatchEnd( ap );
	FreeVec( ap );

	{
		APTR	next;

		/* If no fatal errors occured, process the files */
		if( ( rc == RETURN_OK ) && ( !IoErr() || ( IoErr() == ERROR_NO_MORE_ENTRIES ) ) )
		{
			for( file = ( struct FileNode * ) files.lh_Head;
				( next = file->Link.ln_Succ ) && ( rc <= RETURN_WARN );
				file = next )
			{
				CurrentDir( file->Dir );
				rc = CallHookPkt( hook, NULL, file->Link.ln_Name );
				warn |= ( rc == RETURN_WARN );

				if( CheckSignal( SIGBREAKF_CTRL_C ) )
				{
					rc = RETURN_ERROR;
					SetIoErr( ERROR_BREAK );
				}
			}
		}

		/* Free all file nodes */
		for( file = ( struct FileNode * ) files.lh_Head;
			next = ( struct FileNode * ) file->Link.ln_Succ;
			file = next )
		{
			FreeFileNode( file );
		}

		/* Free all dir nodes */
		for( current = ( struct DirNode * ) dirs.lh_Head;
			next = ( struct DirNode * ) current->Link.ln_Succ;
			current = next )
		{
			FreeDirNode( current );
		}
	}

	return( ( ( rc == RETURN_OK ) && warn ) ? RETURN_WARN : rc );
}


/* Process all files, entering directories if all is true, and call hook
 * for all files. The hook should return a DOS RETURN_#? code, and the
 * loop will break if more than a warning is returned. Returns
 * RETURN_OK if all hook calls returned RETURN_OK, RETURN_WARN if one or
 * more hook calls returned RETURN_WARN, or the return code from a failed
 * hook call (rc > RETURN_WARN).
 */
LONG
ProcessFiles( STRPTR *files, LONG all, struct Hook *hook )
{
	struct maData	*ma;
	LONG	warn = 0, rc = 0;

	if( !files )
	{
		return( 0 );
	}

	for( ; *files && ( rc <= RETURN_WARN ); ++files )
	{
		ma = InitMA( *files );

		if( IoErr() )
		{
			return( -1 );
		}

		do
		{
			rc = CollectProcessPattern( *files, all, hook );
			warn |= ( rc == RETURN_WARN );
		} while( ( rc <= RETURN_WARN ) && ( HandleMA( ma ) > 0 ) );

		FreeMA( ma );
	}

	return( ( ( rc == RETURN_OK ) && warn ) ? RETURN_WARN : rc );
}
