#!/usr/bin/perl -w
#
# __copy1__
# __copy2__
#
# finds the oldest (or newest) file in the given directories and/or filelist
#
use strict;
use File::Basename;

my $CMD		= "oldest";
my $CMDVER	= "1.17";
my $CMDSIG	= "$CMD v$CMDVER (2025-12-15)";

my $OLDEST;
my $OLDESTTIME;
my $NEWEST;
my $NEWESTTIME;
my $IGNORE_LE;
my $IGNORE_GT;

my $F_NEWEST	= 0;
my $F_OLDEST	= 1;
my $F_ZERO	= 0;
my $F_STDEXL	= 1;
my $VERBOSE	= 1;
my $Terse	= 0;
my $Debug	= 0;

my $regexp;
my $pathre;
my $xregexp;
my $xpathre;

my @StdFilesExl		= qw( Thumbs.db );
my $StdFilesExlRe	= '\.tmp$|\.temp$';
my @StdDirsExl		= qw( tmp temp .streams .sums );
my $StdDirsExlRe	= '^@|^cache|^\.cache';

my @args;


# argument parsing
#
PARMS: while (@ARGV) {
    CASE: {
    	$_ = shift(@ARGV);

    	if ($_ eq "-n" || $_ eq "--newest")	{ $F_OLDEST = 0; $F_NEWEST = 1; last CASE; }
	if ($_ eq "-v" || $_ eq "--verbose")	{ $VERBOSE = 1; last CASE; }
	if ($_ eq "-t" || $_ eq "--terse")	{ $Terse = 1; last CASE; }
	if ($_ eq "-q" || $_ eq "--quiet")	{ $VERBOSE = 0; last CASE; }
	if ($_ eq "-0" || $_ eq "--zero")	{ $F_ZERO = 1; last CASE; }
	if ($_ eq "-D" || $_ eq "--debug")	{ $Debug = 1; $VERBOSE = 0; last CASE; }
    	if ($_ eq "--re") {
		usage()	if (!scalar @ARGV);
		$regexp = shift(@ARGV);
		last CASE;
	}
    	if ($_ eq "--pe") {
		usage()	if (!scalar @ARGV);
		$pathre = shift(@ARGV);
		last CASE;
	}
    	if ($_ eq "--rx") {
		usage()	if (!scalar @ARGV);
		$xregexp = shift(@ARGV);
		last CASE;
	}
    	if ($_ eq "--px") {
		usage()	if (!scalar @ARGV);
		$xpathre = shift(@ARGV);
		last CASE;
	}
    	if ($_ eq "--xle") {
		usage()	if (!scalar @ARGV);
		$IGNORE_LE = shift(@ARGV);
		$IGNORE_LE = `date --date "$IGNORE_LE" '+%s'` or die( "\n" );
		chomp($IGNORE_LE);
		last CASE;
	}
	if ($_ eq "-a" || $_ eq "--all") {
		$F_STDEXL = 0;
		last CASE;
	}
	if ($_ eq "--") { last PARMS; }
	if ($_ =~ /^-/) { usage(); }
	push( @args, $_ );
    }
}
push( @args, @ARGV );

usage()	if (!@args);


$! = 0	if ($VERBOSE);	# unbuffered output


my %OLDESTS;
my %NEWESTS;
my $Prog	= 0;

my $path;
my @files;
my $fname;

for $path (@args) {
	if (-d $path) {
		do_search( $path );
		echocl();
	} else {
		$fname	= $path;
		$fname	=~ s#.*/##;

		if ($path =~ /\//) {
			next	if (defined $pathre && $path !~ /$pathre/i);
			next	if (defined $xpathre && $path =~ /$xpathre/i);
		}
		next	if (defined $regexp && $fname !~ /$regexp/i);
		next	if (defined $xregexp && $fname =~ /$xregexp/i);
		# filenames from command line are grouped like they are in the same dir
		push( @files, $path );
	}
}

if (scalar @files) {
	do_search( @files );
}

if ($Terse) {
	if ($F_OLDEST) {
		map { print $OLDESTS{$_}, "\n"; }	sort( keys( %OLDESTS ) );
	} else {
		map { print $NEWESTS{$_}, "\n"; }	sort( keys( %NEWESTS ) );
	}
} else {
	if ($F_OLDEST) {
		map { print( list( $OLDESTS{$_} ) ); }	sort( keys( %OLDESTS ) );
	} else {
		map { print( list( $NEWESTS{$_} ) ); }	sort( keys( %NEWESTS ) );
	}
}
exit( 0 );









sub usage
{
	print( STDERR "
== $CMDSIG - finds oldest (or newest) file in the given directories ==

usage: $CMD [options] dir(s) and/or file(s) ...

options:
 -t|--terse	shows filenames only, instead of dates + filenames
 -q|--quiet	be quiet
 -v|--verbose	be verbose (progress status)
 -n|--newest	invert logic (find newest file/dir)
 -0|--zero	consider files with epoch-date, 1970-01-01 00:00:00
 	        (seconds=0, default is ignore them)
 -D|--debug	print debug messages on stderr (implies -q)

 -a|--all)	don't use standard exclusions

 --re regexp	consider only files matching 'regexp'
 --pe regexp	consider only full paths matching 'regexp'

 --rx regexp	excludes files matching 'regexp'
 --px regexp	excludes full paths matching 'regexp'

 --xle date	exclude files with date <= 'date'
 --xgt date	exclude files with date >= 'date'

notes:
 . ignorecase is always used on regexp
 . regexp parms can be used only once, but since extended regexp are used,
   you can combine multiple with pipe char (see egrep manual)
" );
	print( "\nstandard exclusions:\n" );
	printf( " dirs:  %s\n", join( " ", @StdDirsExl ) );
	printf( " (re):  %s\n", $StdDirsExlRe );
	printf( " files: %s\n", join( " ", @StdFilesExl ) );
	printf( " (re):  %s\n", $StdFilesExlRe );

	die( "\n" );
}


# brutal hack to display progress lines
sub echocl {
	print( STDERR "\r\e[K", @_ )	if ($VERBOSE);
	return 1;
}

sub pdebug {
	return 1	if (!$Debug);
	if (!scalar @_) {
		print( STDERR "\n" );
	} else {
		my $fmt = shift;
		print( STDERR "D# " . sprintf( $fmt, @_ ) );
	}
	return 1;
}

sub do_search {
	my ($key);

	$OLDEST		= "";
	$OLDESTTIME	= 9999999999999;
	$NEWEST		= "";
	$NEWESTTIME	= 0;

	if (-d $_[0]) {
		pdebug( "  scandir() on do_search(%s)\n", join( ", ", @_ ) );
		scandir( $_[0] );
	} else {
		pdebug( "  scanfiles() on do_search(%s)\n", join( ", ", @_ ) );
		scanfiles( @_ );
	}

	if ($OLDEST ne "") {
		$Prog++;
		$key			= $OLDESTTIME . sprintf( "-%04d", $Prog );
		$OLDESTS{ $key }	= $OLDEST;
		pdebug( "   OLDEST{%s}=%s\n", $key, $OLDEST );
	}
	if ($NEWEST ne "") {
		$Prog++;
		$key			= $NEWESTTIME . sprintf( "-%04d", $Prog );
		$NEWESTS{ $key }	= $NEWEST;
		pdebug( "   NEWEST{%s}=%s\n", $key, $NEWEST );
	}
	return 1;
}


sub scandir {
	my ($dir)	= @_;
	my $DIR		= $dir;
	my @files;
	my @dirs;

	return	if (! -d $dir);

	if ($F_STDEXL) {
		if (basename($dir) =~ $StdDirsExlRe) {
			pdebug( "  skip %s due ExlRe\n", basename($dir) );
			return;
		}
		if (in_list( basename($dir), @StdDirsExl )) {
			pdebug( "  skip %s due inlist Exl\n", basename($dir) );
			return;
		}
	}

	pdebug();
	pdebug( "   scandir( %s )\n", join( ", ", @_ ) );
	echocl( sprintf( " %-.78s", "scanning: $dir" ) );

	opendir( DIR, $dir ) or do {
		echocl( " can't read dir $dir: $!\n" );
		return 1;
	};
	while ($_ = readdir( DIR ) ) {
		next	if ($_ eq "." || $_ eq "..");
		next	if (-l "$dir/$_");

		if (-d "$dir/$_") {
			push( @dirs, "$dir/$_" );
		} else {
			next	if (defined $regexp && $_ !~ /$regexp/i);
			next	if (defined $pathre && "$dir/$_" !~ /$pathre/i);
			next	if (defined $xregexp && $_ =~ /$xregexp/i);
			next	if (defined $xpathre && "$dir/$_" =~ /$xpathre/i);
			push( @files, "$dir/$_" );
		}
	}
	closedir( DIR );

	return 0	if (!@files && !@dirs);

	scanfiles( @files );

	foreach $_ (@dirs) {
		scandir($_);
	}

	return 1;
}


sub scanfiles
{
	my ($key,$changed);
  	my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks);

	pdebug( "   scanfiles( %s )\n", join( ", ", @_ ) );
	foreach $_ (@_) {
		if ($F_STDEXL) {
			if (basename($_) =~ $StdFilesExlRe) {
				pdebug( "   skip %s due ExlRe\n", basename($_) );
				next;
			}
			if (in_list( basename($_), @StdFilesExl )) {
				pdebug( "   skip %s due in_list Exl\n", basename($_) );
				next;
			}
		}

  		($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
                        $atime,$mtime,$ctime,$blksize,$blocks)
	                          = stat($_);

		if (!defined $mtime) {
			printf( STDERR " skip non-readable file '%s'\n", $_ )	if ($VERBOSE || $Debug);
			next;
		}
		next	if ($mtime == 0 && !$F_ZERO);
		next	if (defined $IGNORE_LE && $mtime <= $IGNORE_LE);
		next	if (defined $IGNORE_GT && $mtime >= $IGNORE_GT);

		if (defined $mtime && $mtime > $NEWESTTIME) {
			$NEWEST		= $_;
			$NEWESTTIME	= $mtime;
		}
		if (defined $mtime && $mtime < $OLDESTTIME) {
			$OLDEST		= $_;
			$OLDESTTIME	= $mtime;
		}
	}

	return 1;
}


sub list
{
	my ($file) = @_;
	my @tmp	= stat( $file );
	my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($tmp[9]);

	$file	=~ s/^\.\///;

	return sprintf( "%4d-%02d-%02d %02d:%02d:%02d %s\n",
		$year + 1900, $mon + 1, $mday, $hour, $min, $sec, $file );
}

sub in_list
{
	my $val	= shift;
	my $str;

	foreach $str (@_) {
		return 1	if ($str eq $val);
	}
	return 0;
}
