//-----------------------------------------------------------------------------
// Copyright © 2006 - Philip Howard - All rights reserved
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//-----------------------------------------------------------------------------
// package	libh/ftr
// homepage	http://libh.slashusr.org/
//-----------------------------------------------------------------------------
// author	Philip Howard
// email	libh at ipal dot org
// homepage	http://phil.ipal.org/
//-----------------------------------------------------------------------------
// This file is best viewed using a fixed spaced font such as Courier
// and in a display at least 120 columns wide.
//-----------------------------------------------------------------------------

#define _GNU_SOURCE

#include "ftr_lib.h"

#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <libh/io.h>

//-----------------------------------------------------------------------------
// function	ftr_cmp_def
//
// purpose	Do a default compare of the name structs of two ftr_node
//		structs.  This will be used for collating keys in an AVL
//		tree keyed by name.
//
// arguments	1 (avl_link *) struct ftr_node 1
//		2 (avl_link *) struct ftr_node 2
//		3 (AVL *) ignored
//
// returns	(int)  < 0 : name 1  < name 2
//		(int) == 0 : name 1 == name 2
//		(int)  > 0 : name 1  > name 2
//-----------------------------------------------------------------------------
static
int
ftr_cmp_def (
    avl_link *		arg_node_1
    ,
    avl_link *		arg_node_2
    ,
    AVL *		arg_root
    )
{
    return strcmp( ( (struct ftr_node *) arg_node_1 ) -> name,
		   ( (struct ftr_node *) arg_node_2 ) -> name );
}

//-----------------------------------------------------------------------------
// function	ftr_cmp_ref
//
// purpose	Pass key comparison on to a caller supplied function that
//		compares two (const char *).
//
// arguments	1 (avl_link *) struct ftr_node 1
//		2 (avl_link *) struct ftr_node 2
//		3 (AVL *) AVL tree root pointer is also (struct ftr_dir *)
//
// returns	(int)  < 0 : name 1  < name 2
//		(int) == 0 : name 1 == name 2
//		(int)  > 0 : name 1  > name 2
//-----------------------------------------------------------------------------
static
int
ftr_cmp_ref (
    avl_link *		arg_node_1
    ,
    avl_link *		arg_node_2
    ,
    AVL *		arg_root
    )
{
    //--------------------------------------------
    // Does this look like a function call to you?
    //--------------------------------------------
    return ( * ( ( (struct ftr_dir *) arg_root ) -> cmp_fun_p ) )
	( ( (struct ftr_node *) arg_node_1 ) -> name,
	  ( (struct ftr_node *) arg_node_2 ) -> name );
}

__PROTO_BEGIN__
//-----------------------------------------------------------------------------
// function	ftr_get
//
// purpose	Get the next name object in a file tree recursion.
//
// arguments	1 (FTR) reference to which file tree recursion.
//
// returns	(int) return status
//		(int) ==  -1 : error
//		(int) ==   0 : end of recursion, no more names
//		(int) == '-' : no status available for this name
//		(int) == '?' : unrecognized file type
//		(int) == 'd' : directory before descending
//		(int) == 'a' : directory after ascending back
//		(int) == 'e' : directory not descended (cannot read)
//		(int) == 'm' : directory not descended (maximum depth)
//		(int) == 'f' : regular file
//		(int) == 'l' : symlink
//		(int) == 'b' : block device
//		(int) == 'c' : character device
//		(int) == 's' : socket
//		(int) == 'p' : pipe/fifo
//
// note		All character code return values are positive integers so a
//		valid loop could be coded like:
//
//		while ( ftr_get( this_ftr ) > 0 ) ...
//-----------------------------------------------------------------------------
int
ftr_get (
    FTR			arg_ftr
    )
__PROTO_END__
{
    FTR			next_ftr	;
    struct ftr_dir *	this_dir	;
    struct ftr_dir *	next_dir	;
    struct ftr_node *	this_node	;
    DIR *		dir_stream	;
    struct dirent *	dir_entry	;
    size_t		name_len	;
    size_t		need_size	;
    int			save_cwd_fd	;

    static char		dot_name	[2] = ".";


    //-----------------------------------------------
    // Return now if a permanent error or end exists.
    //-----------------------------------------------
    if ( ! arg_ftr ) goto ftr_get_error_return;
    if ( arg_ftr->file_type <= 0 ) goto ftr_get_return;

    //------------------------------------
    // Save the current working directory.
    //------------------------------------
    save_cwd_fd = open( ".", O_RDONLY | O_DIRECTORY );
    if ( save_cwd_fd < 0 ) goto ftr_get_error_return;

    //---------------------------------------------------------------------
    // If there is a directory node here, then use the next name within it.
    //---------------------------------------------------------------------
    if ( arg_ftr->curr_dir ) {
	this_dir = arg_ftr->curr_dir;

	//------------------------------------------------------------
	// If this is the first time at this level, then finish setup.
	//------------------------------------------------------------
	if ( arg_ftr->file_type == 'd' ) {

	    //-- Back up the name state to be restored when ascending.
	    this_dir->save_rdir_last = arg_ftr->rdir_last;
	    this_dir->save_name_last = arg_ftr->name_last;
	    this_dir->save_name_end = arg_ftr->name_end;

	    //-- Increment depth number and check against peak.
	    ++ arg_ftr->depth_curr;
	    if ( arg_ftr->depth_curr > arg_ftr->depth_peak ) {
		arg_ftr->depth_peak = arg_ftr->depth_curr;
	    }
	    
	    //-- Use the directory descriptor set up in descending.
	    arg_ftr->rdir_last = this_dir->rdir_last;
	    
	    //-- Set up the last name to append new names.
	    arg_ftr->name_last = arg_ftr->name_end;
	    if ( arg_ftr->name_last - arg_ftr->name_root > 1 ) {
		* arg_ftr->name_last = '/';
		++ arg_ftr->name_last;
	    }
	}
	
	//-----------------------------------------------------------------------
	// If there is no name available in the list, then ascend this directory.
	//-----------------------------------------------------------------------
	if ( ! ( this_node = avl_first( & this_dir->name_tree ) ) ) {
	    
	    //-- If not used elsewhere, close the descriptor to this directory.
	    if ( arg_ftr->rdir_last != arg_ftr->rdir_part &&
		 arg_ftr->rdir_last != arg_ftr->rdir_full &&
		 arg_ftr->rdir_last != arg_ftr->rdir_orig ) {
		close( arg_ftr->rdir_last );
	    }
	    
	    //-- Restore the state saved for this directory.
	    arg_ftr->rdir_last = this_dir->save_rdir_last;
	    arg_ftr->name_last = this_dir->save_name_last;
	    arg_ftr->name_end = this_dir->save_name_end;
	    * arg_ftr->name_end = 0;
	    -- arg_ftr->depth_curr;
	    
	    //-- Remove this directory node.
	    arg_ftr->curr_dir = this_dir->parent_dir;
	    free( this_dir );
	    
	    //-- If back at the top, fix up name_part to be dot.
	    if ( ! arg_ftr->curr_dir ) {
		arg_ftr->name_part = arg_ftr->name_end + 1;
		arg_ftr->name_part[0] = '.';
		arg_ftr->name_part[1] = 0;
	    }
	    
	    //-- Get the file status of the parent that becomes current.
	    if ( fchdir( arg_ftr->rdir_last ) < 0 )
		goto ftr_get_error_fchdir;
	    if ( lstat( arg_ftr->name_last, & arg_ftr->stat_buf ) < 0 )
		goto ftr_get_error_lstat;
	    
	    arg_ftr->file_type = 'a';
	    goto ftr_get_return_ascend;
	}
	
	//------------------------------------------------
	// Otherwise there is a name available, so use it.
	//------------------------------------------------
	avl_delete( & this_dir->name_tree, this_node );
	name_len = strlen( this_node->name );
	
	//----------------------------------------------------
	// If more name space is needed, expand the space now.
	//----------------------------------------------------
	need_size = ( arg_ftr->name_last - arg_ftr->name_root ) + name_len + 1;
	if ( need_size > arg_ftr->name_size ) {
	    char *	ptr	;
	    ssize_t	diff	;
	    
	    //-- Increase size in powers of 2.
	    while ( need_size > arg_ftr->name_size ) arg_ftr->name_size <<= 1;
	    
	    //-- Expand the memory allocation.
	    ptr = realloc( arg_ftr->name_root, need_size );
	    if ( ! ptr ) goto ftr_get_error_realloc_name;
	    
	    //-- Relocate the pointers.
	    diff = ptr - arg_ftr->name_root;
	    arg_ftr->name_root = ptr;
	    if ( arg_ftr->name_full != dot_name ) arg_ftr->name_full += diff;
	    if ( arg_ftr->name_part != dot_name ) arg_ftr->name_part += diff;
	    if ( arg_ftr->name_last != dot_name ) arg_ftr->name_last += diff;
	    arg_ftr->name_end  += diff;
	}
	
	//---------------------------------------------------------
	// Append the new name to the current last directory point.
	//---------------------------------------------------------
	memcpy( arg_ftr->name_last, this_node->name, name_len + 1 );
	arg_ftr->name_end = arg_ftr->name_last + name_len;
	
	//---------------
	// Free the node.
	//---------------
	free( this_node );
	
	//---------------------------------------------------
	// Change names that are "." to be the appended name.
	//---------------------------------------------------
	if ( arg_ftr->name_full == dot_name ) arg_ftr->name_full = arg_ftr->name_last;
	if ( arg_ftr->name_part == dot_name ) arg_ftr->name_part = arg_ftr->name_last;
    }
    
    //---------------------------------------------------------------
    // Else with no directory, if this is not the first, we are done.
    //---------------------------------------------------------------
    else if ( arg_ftr->count_objects > 0 ) {
	
	//---------------------------------------------
	// This FTR is done, so do partial cleanup now.
	//---------------------------------------------
	if ( arg_ftr->rdir_full != -1 ) {
	    close( arg_ftr->rdir_full );
	    arg_ftr->rdir_full = -1;
	}
	if ( arg_ftr->rdir_part != -1 ) {
	    close( arg_ftr->rdir_part );
	    arg_ftr->rdir_part = -1;
	}
	if ( arg_ftr->rdir_last != -1 ) {
	    close( arg_ftr->rdir_last );
	    arg_ftr->rdir_last = -1;
	}
	if ( arg_ftr->name_root ) {
	    free( arg_ftr->name_root );
	    arg_ftr->name_root = NULL;
	    arg_ftr->name_full = NULL;
	    arg_ftr->name_part = NULL;
	    arg_ftr->name_last = NULL;
	    arg_ftr->name_end  = NULL;
	}
	
	//-------------------------------------------------
	// If this is the last FTR in the queue, finish up.
	//-------------------------------------------------
	if ( ! arg_ftr->next_ftr ) {
	    arg_ftr->file_type = 0;
	    close( save_cwd_fd );
	    return 0;
	}
	
	//------------------------------------------------
	// Pop the stack to the next FTR in the queue.
	// Keep the same pointer so this is transparent to
	// the caller.  Clean up what is here now and copy
	// stuff from the next one which will be freed.
	//------------------------------------------------
	next_ftr = arg_ftr->next_ftr;
	arg_ftr->next_ftr  = next_ftr->next_ftr;
	arg_ftr->name_root = next_ftr->name_root;
	arg_ftr->name_full = next_ftr->name_full;
	arg_ftr->name_part = next_ftr->name_part;
	arg_ftr->name_last = next_ftr->name_last;
	arg_ftr->name_end  = next_ftr->name_end;
	arg_ftr->name_size = next_ftr->name_size;
	arg_ftr->file_type = next_ftr->file_type;
	arg_ftr->rdir_full = next_ftr->rdir_full;
	arg_ftr->rdir_part = next_ftr->rdir_part;
	arg_ftr->rdir_last = next_ftr->rdir_last;
	free( next_ftr );
    }

    else /* fall on down below in all cases */;
    
    //--------------------------------------------------------------
    // If the status for this name is unavailable, then return that.
    //--------------------------------------------------------------
    if ( fchdir( arg_ftr->rdir_last ) < 0 ) goto ftr_get_error_fchdir;
    if ( lstat( arg_ftr->name_last, & arg_ftr->stat_buf ) < 0 ) {
	arg_ftr->file_type = '-';
	goto ftr_get_return_object;
    }

    //---------------------------------------------------
    // If this is a directory, then descend into it.
    //---------------------------------------------------
    if ( S_ISDIR( arg_ftr->stat_buf.st_mode ) ) {

	//-- Check for depth limit reached.
	if ( arg_ftr->depth_curr >= arg_ftr->depth_limit ) {
	    arg_ftr->file_type = 'm';
	    goto ftr_get_return_object;
	}

	//-- Change the current directory to the new directory.
	if ( chdir( arg_ftr->name_last ) < 0 ) {
	    arg_ftr->file_type = 'e';
	    goto ftr_get_return_object;
	}

	//-- If name_full is not yet set, make it be ".".
	if ( ! arg_ftr->name_full ) {
	    arg_ftr->name_full = dot_name;
	}

	//-- If name_part is not yet set, make it be ".".
	if ( ! arg_ftr->name_part ) {
	    arg_ftr->name_part = dot_name;
	    arg_ftr->rdir_part = open( ".", O_RDONLY | O_DIRECTORY );
	    if ( ! arg_ftr->rdir_part ) {
		arg_ftr->file_type = 'e';
		goto ftr_get_return_object;
	    }
	}

	//-- Allocate a new directory node.
	next_dir = malloc( sizeof (struct ftr_dir) );
	if ( ! next_dir ) goto ftr_get_error_malloc_dir;

	//-- The new directory will be the parent of the last name.
	if ( ( next_dir->rdir_last = open( ".", O_RDONLY | O_DIRECTORY ) ) < 0 ) {
	    free( next_dir );
	    arg_ftr->file_type = 'e';
	    goto ftr_get_return_object;
	}

	//-- Open a stream to read names from the new directory.
	if ( ! ( dir_stream = opendir( "." ) ) ) {
	    close( next_dir->rdir_last );
	    free( next_dir );
	    arg_ftr->file_type = 'e';
	    goto ftr_get_return_object;
	}

	//-- Link new directory node onto top of stack.
	next_dir->parent_dir = arg_ftr->curr_dir;
	arg_ftr->curr_dir = next_dir;

	//-- Initialize the AVL tree name list with the preferred sort function.
	next_dir->cmp_fun_p = arg_ftr->cmp_fun_p;
	avl_init( & next_dir->name_tree,
		  struct ftr_node, link,
		  arg_ftr->cmp_fun_p ? ftr_cmp_ref : ftr_cmp_def );

	//-- Fill in the AVL tree name list from the directory.
	while ( ( dir_entry = readdir( dir_stream ) ) ) {
	    if ( dir_entry->d_name[0] == '.' &&			// skip
		 ( dir_entry->d_name[1] == 0 ||			// "."
		   ( dir_entry->d_name[1] == '.' &&		// and
		     dir_entry->d_name[2] == 0 ) ) ) continue;	// ".."
	    name_len = strlen( dir_entry->d_name ) + 1;
	    this_node = malloc( offsetof( struct ftr_node, name ) + name_len );
	    if ( ! this_node ) {
		closedir( dir_stream );
		close( next_dir->rdir_last );
		avl_empty( & next_dir->name_tree, free );
		free( next_dir );
		goto ftr_get_error_malloc_node;
	    }
	    memcpy( this_node->name, dir_entry->d_name, name_len );
	    avl_put( & next_dir->name_tree, this_node, NULL, 0 );
	}
	closedir( dir_stream );

	//-- Return the status of a descending directory.
	arg_ftr->file_type = 'd';
	goto ftr_get_return_object;
    }

    //-------------------------------------------
    // Else check for other file types to return.
    //-------------------------------------------
    else if ( S_ISREG(  arg_ftr->stat_buf.st_mode ) ) arg_ftr->file_type = 'f';
    else if ( S_ISLNK(  arg_ftr->stat_buf.st_mode ) ) arg_ftr->file_type = 'l';
    else if ( S_ISBLK(  arg_ftr->stat_buf.st_mode ) ) arg_ftr->file_type = 'b';
    else if ( S_ISCHR(  arg_ftr->stat_buf.st_mode ) ) arg_ftr->file_type = 'c';
    else if ( S_ISSOCK( arg_ftr->stat_buf.st_mode ) ) arg_ftr->file_type = 's';
    else if ( S_ISFIFO( arg_ftr->stat_buf.st_mode ) ) arg_ftr->file_type = 'p';
    else arg_ftr->file_type = '?';

 ftr_get_return_object:
    ++ arg_ftr->count_objects;

 ftr_get_return_ascend:
    if ( fchdir( save_cwd_fd ) < 0 ) arg_ftr->file_type = -1;

    close( save_cwd_fd );
    ++ arg_ftr->count_returns;

 ftr_get_return:
    return arg_ftr->file_type;

    //-----------------------------------------------------
    // Do error cleanups in reverse order to undo the mess.
    //-----------------------------------------------------
 ftr_get_error_malloc_node:
    closedir( dir_stream );

 ftr_get_error_malloc_dir:
 ftr_get_error_lstat:
 ftr_get_error_realloc_name:
    fchdir( save_cwd_fd );
    close( save_cwd_fd );

 ftr_get_error_fchdir:
    arg_ftr->file_type = -1;

 ftr_get_error_return:
    return -1;
}

