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

__PROTO_BEGIN__
//-----------------------------------------------------------------------------
// function	ftr_new
//
// purpose	Create a new instance of file tree recursion for a specified
//		filesystem object name (usually a directory).
//
// arguments	1 (const char *) filesystem object name
//
// returns	(FTR) pointer to new instance
//-----------------------------------------------------------------------------
FTR
ftr_new (
    const char *	arg_name
)
__PROTO_END__
{
    FTR				this_ftr	;
    size_t			name_len	;
    int				save_cwd_fd	;

    static char			dot_name	[2] = ".";


    //-----------------------------------------------
    // Allocate and initialize the FTR object struct.
    //-----------------------------------------------
    this_ftr = malloc( sizeof (struct ftr_s) );
    if ( ! this_ftr )
	goto ftr_new_error_malloc_struct;

    this_ftr->count_objects	= 0;
    this_ftr->count_returns	= 0;

    this_ftr->next_ftr		= NULL;
    this_ftr->curr_dir		= NULL;
    this_ftr->cmp_fun_p		= NULL;

    this_ftr->depth_limit	= INT_MAX - 1;
    this_ftr->depth_curr	= 0;
    this_ftr->depth_peak	= 0;

    this_ftr->file_type		= '?';

    //------------------------------------------------------------------------
    // Allocate name working space.  The amount to allocate is initially TWICE
    // the size of the maximum allowed by the system for a full path name.
    // This allows room for both the current directory and the specified name
    // to each be as long as the maximum size the system permits.  FTR can
    // handle names longer than this maximum when the current directory and
    // the specified name each fit within the limit.  A future version of FTR
    // may even relax that limitation.
    //------------------------------------------------------------------------
    this_ftr->name_size = PATH_MAX * 2;
    this_ftr->name_root = malloc( this_ftr->name_size );
    if ( ! this_ftr->name_root ) goto ftr_new_error_malloc_name;

    //------------------------------------------------------------------
    // Keep a handle on the original current directory.  This is used to
    // restore the current directory when returning from this function.
    // Also save a reference for later restore when this FTR is done.
    //------------------------------------------------------------------
    save_cwd_fd = open( ".", O_RDONLY | O_DIRECTORY );
    if ( save_cwd_fd < 0 ) goto ftr_new_error_save_cwd;
    this_ftr->rdir_orig = save_cwd_fd;

    //---------------------------------------------------------
    // If the specified name is NULL or empty, use "." instead.
    //---------------------------------------------------------
    if ( ! arg_name || ! arg_name[0] ) arg_name = dot_name;

    //------------------------------------------------------------
    // The full reference directory will be the original current
    // working directory, unless the specified name begins with a
    // '/' in which case it will be the system root directory and
    // the current working directory will also be changed to this.
    //------------------------------------------------------------
    this_ftr->rdir_full = this_ftr->rdir_orig;
    if ( arg_name[0] == '/' ) {

	//-- Open the root directory and save it as full reference.
	this_ftr->rdir_full = open( "/", O_RDONLY | O_DIRECTORY );
	if ( this_ftr->rdir_full < 0 ) goto ftr_new_error_open_root;

	//-- Change the current directory to the root directory.
	if ( fchdir( this_ftr->rdir_full ) < 0 ) goto ftr_new_error_chdir_root;

	//-- Trim all but one leading '/' from the specified name.
	while ( arg_name[0] == '/' ) ++ arg_name;
	-- arg_name;
    }

    //------------------------------------------------------------
    // Get the current working directory path into the name space.
    //------------------------------------------------------------
    if ( ! getcwd( this_ftr->name_root, this_ftr->name_size ) )
	goto ftr_new_error_getcwd;

    //-----------------------------------------------------------------
    // Verify that the length of the remaining specified name is valid.
    //-----------------------------------------------------------------
    name_len = strlen( arg_name );
    if ( name_len > ( PATH_MAX - 1 ) ) goto ftr_new_error_name_too_long;

    //---------------------------------------
    // Verify that the specified name exists.
    //---------------------------------------
    if ( lstat( arg_name, & this_ftr->stat_buf ) < 0 )
	goto ftr_new_error_lstat_arg;

    //----------------------------------------------
    // If the specified name is ".", whether passed
    // or substituted, then it will not be appended.
    //----------------------------------------------
    if ( arg_name[0] == '.' && arg_name[1] == 0 )
	this_ftr->name_full = NULL;

    //------------------------------------------
    // Else the specified name will be appended.
    //------------------------------------------
    else {

	//-------------------------------------------------------------------
	// If the reference directory is "/" (it can be that way because the
	// specified name began with '/' or because the current directory was
	// already at "/") then the place where the specified name is placed
	// depends on whether that name begins with '/' or not.  If it does,
	// then use [0] and let it overwrite the "/" with its own '/', else
	// use [1] and to append the specified name after the '/'.
	//-------------------------------------------------------------------
	if ( this_ftr->name_root[0] == '/' && this_ftr->name_root[1] == 0 ) {
	    this_ftr->name_full = this_ftr->name_root;
	    if ( arg_name[0] != '/' ) ++ this_ftr->name_full;
	}

	//------------------------------------------------------
	// Else append a '/' character as a directory separator,
	// followed by the specified name.
	//------------------------------------------------------
	else if ( arg_name[0] != '.' || arg_name[1] != 0 ) {
	    this_ftr->name_full = this_ftr->name_root;
	    while ( * this_ftr->name_full ) ++ this_ftr->name_full;
	    * this_ftr->name_full ++ = '/';
	}

	//-----------------------------------------------------
	// Append the specified name at the indicated position.
	//-----------------------------------------------------
	memcpy( this_ftr->name_full, arg_name, name_len + 1 );
    }

    //----------------------------------------------------------
    // Find the place to aim pointers name_last and name_end,
    // as well as establish a reference directory for name_last.
    // Just cheat for the simple case of "/".
    //----------------------------------------------------------
    this_ftr->name_last = this_ftr->name_root;
    this_ftr->name_end = this_ftr->name_root;
    this_ftr->rdir_last = this_ftr->rdir_full;

    //-- If the name is "/" this is a simple case.
    if ( this_ftr->name_root[0] == '/' && this_ftr->name_root[1] == 0 )
	++ this_ftr->name_end;

    //-- Else find the end of the name string.
    else {
	while ( * this_ftr->name_end ) {
	    if ( * this_ftr->name_end == '/' ) {
		this_ftr->name_last = this_ftr->name_end;
	    }
	    ++ this_ftr->name_end;
	}
	if ( this_ftr->name_last > this_ftr->name_root ) {
	    * this_ftr->name_last = 0;
	    this_ftr->rdir_last =
		open( ( this_ftr->name_full && this_ftr->name_last > this_ftr->name_full
			? this_ftr->name_full
			: this_ftr->name_root
		      ), O_RDONLY | O_DIRECTORY );
	    * this_ftr->name_last = '/';
	    if ( this_ftr->rdir_last < 0 ) goto ftr_new_error_open_last;
	    ++ this_ftr->name_last;
	}
    }

    //--------------------------------------------------
    // If this is a directory, then set name_part as "."
    // and open this directory as a reference to it.
    // Otherwise leave name_part and rdir_part unset.
    //--------------------------------------------------
    this_ftr->name_part = NULL;
    this_ftr->rdir_part = -1;

    //----------------------------------------------
    // Restore the current directory for the caller.
    //----------------------------------------------
    // DO NOT close this descriptor since the FTR
    // object has a copy of it in ->rdir_orig.
    //----------------------------------------------
    if ( fchdir( save_cwd_fd ) < 0 ) goto ftr_new_error_fchdir_restore;

    //-------------------------------------------------------
    // And finally, at last, what we've all been waiting for.
    //-------------------------------------------------------
    return this_ftr;

    //-----------------------------------------------------------
    // Here are error cleanups in reverse order to undo the mess.
    //-----------------------------------------------------------
 ftr_new_error_open_last:
 ftr_new_error_name_too_long:
 ftr_new_error_lstat_arg:
 ftr_new_error_getcwd:
    fchdir( save_cwd_fd );

 ftr_new_error_fchdir_restore:
 ftr_new_error_chdir_root:
    if ( this_ftr->rdir_full != save_cwd_fd )
	close( this_ftr->rdir_full );

 ftr_new_error_open_root:
    close( save_cwd_fd );

 ftr_new_error_save_cwd:
    free( this_ftr->name_root );

 ftr_new_error_malloc_name:
    free( this_ftr );

 ftr_new_error_malloc_struct:
    return NULL;
}

