#! /usr/bin/perl -- -*- perl -*-

### recordsadmin.in
###
### $Id: recordsadmin.in,v 1.11 1999/04/14 17:16:51 ashvin Exp $
###
### Copyright (C) 1996 by Ashvin Goel
###
### This file is under the Gnu Public License.

require 5.000;

use Getopt::Std;
use File::Basename;

# global - should be set by makefile
$records_init_file="$ENV{HOME}/.emacs-records";

$records_emacs_init_file="$ENV{HOME}/.emacs";

# external global variables read from .emacs-records
# with their documentation
# initialized here to sane defaults 

@records_external_vars = (
    "records_directory", 1, 'Directory under which all records are stored.',
    "records_index_file", 1, 'File name in which records subject index is stored.
Make sure that the file is within the records directory.',
    "records_dindex_file", 1, 'File name in which records date index is stored.
Make sure that the file is within the records directory.',
    "records_directory_structure", 0, 'The directory structure for records files.
Its values can be 
0 => all records are stored in records_directory. 
1 => records are stored by year within records_directory.
2 => records are stored by year and month within records_directory.',
    "records_day_order", 0, 'A records file name is composed of a day, month and year.
This variable determines the order of the day in date. 
For example, day = 0, if you want a records date to be in (day, month, year)
format and day = 2, if you want a records date to be (year, month, day), etc.
Valid values for day are 0, 1 or 2 only.',
    "records_month_order", 0, 'A records file name is composed of a day, month and year.
This variable determines the order of the month in date. 
For example, month = 1, if you want a records date to be in (day, month, year)
format and month = 0, if you want a records date to be (month, day, year), etc.
Valid values for month are 0, 1 or 2 only.',
    "records_year_order", 0, 'A records file name is composed of a day, month and year.
This variable determines the order of the year in date. 
For example, year = 2, if you want a records date to be in (day, month, year)
format and year = 0, if you want a records date to be (year, month, day), etc.
Valid values are 0, 1 or 2 only.',
    "records_year_length", 0, 'The length of a records file year. 
This can be 4 (example 1996) or 2 (96).
Even if it is 2, records will work correctly after the turn of the century.
Valid values are 2 or 4 only.',
);

$records_directory = "$ENV{HOME}/records";
$records_index_file = "${records_directory}/index";
$records_dindex_file = "${records_directory}/dindex";
$records_directory_structure = 1;
$records_day_order = 0;
$records_month_order = 1;
$records_year_order = 2;
$records_year_length = 4;

# internal global variables
# initialized in &records_initialize_vars
$records_date_length = 0;
@records_date_order = ();
@records_date = ();
@records_glob_regexp = ();

# other global variables
$records_day_length = 0;
$records_month_length = 0;
%records_raw_index = ();
%records_date_index = ();
$alternate = 1; # alternate set of variables

$records_init_mesg = ";;; AUTOMATICALLY GENERATED: DO NOT ALTER OR DELETE FROM THIS LINE ONWARDS";

$records_more_init_mesg = <<MORE_MESG

;;; This part of the file is also read by perl.
;;; Add any emacs customization before this comment.

;commented by tv\@debian.org - this shouldn't be necessary on Debian.
;(let ((f (expand-file-name "/tmp/ff/o/records-1.4.3")))
;  (if (not (member f load-path))
;      (setq load-path (cons f load-path))))

(autoload 'records-mode "records" "records mode" t)
(autoload 'records-goto-today "records" "Go to today's records" t)
(autoload 'records-underline-line "records" "underine a title" t)
(autoload 'records-index-mode "records-index" "records index mode" t)

(setq auto-mode-alist
      (cons (cons "[0-9][0-9][0-9][0-9][0-9][0-9].?.?\$" 'records-mode)
	    auto-mode-alist))

MORE_MESG
;

#
# Start the bang bang '
#
 
if (!getopts('hdvicn')) {
    &usage();
}

# testing
# &records_initialize_vars(1);
# &records_query_vars($alternate);
# &records_initialize_vars(0, $alternate);
# &records_update_links("/home/asgoel/records/96/961209", $alternate);
# exit(0);

&usage() if ($opt_h);
$debug = 1 if ($opt_d);
$verbose = 1 if ($opt_v);

if ($opt_i) {
    &records_install_init_file();
}

if ($opt_c) {
    &records_convert_format();
}

if (!$opt_n) {
   print "Indexing all records...\n";
   &records_recreate_indexes();
}

print "If you already have an emacs running with records mode loaded, run
M-x records-initialize (C-c C-z) in a record buffer\n";

exit(0);

sub usage {
    print STDERR "Usage: $0 [-options ...]

This program will recreate the records indexes and modifed records files
to reflect the new indexes. With more options, it will also
install records defaults, convert date and directory formats, etc.

where options are:
	[-h]	this usage message
	[-i]	install records defaults and then reindexes records
	[-c]	converts one date and directory format to another and reindex
	[-n]	do not reindex records (useful with -i or -c)
	[-d]	run in debug mode
	[-v]	run in verbose mode
";
    exit(1);
}

# make directory hierarchy
sub mkdirhier {
    local($dir, $mode) = @_;
    local($pdir) = dirname($dir);

    if (!(-d $pdir)) {
	&mkdirhier($pdir, $mode);
    }
    mkdir($dir, $mode) ||
	die "Couldn't make directory $dir: $!\n";
}

sub records_write_file {
    local($file, *records_file) = @_;

    print "$file is modified.\n" if ($verbose);
    # rewrite the new file
    if (!rename("$file", "$file~")) {
	warn("Could not rename $file: $!\n");
	return;
    }
    if (!open(OUT, ">$file")) {
	warn("Could not open $file for writing: $!\n");
	# get back old file
	rename("$file~", "$file");
	return;
    }
    print OUT @records_file;
    close (OUT);
}

# returns 1 if the init file has the generated stuff in it or else 0
sub records_read_init_file {
    local($var);
    local($init_msg_seen) = 0;

    if (!open(IN, "< $records_init_file")) {
        return 0;
    }
    while(<IN>) {
	if (!$init_msg_seen) {
	    next unless /$records_init_mesg/;
	    $init_msg_seen = 1;
	}
	next if /^\s*$/;
	next if /^\s*;/;
	if (/\s*\(\s*setq\s+([A-Za-z-]+)\s+([0-9]+)\s*\)/) {
	    # numeric quantity found
	    $var = $1;
	    $val = $2;
	    $var =~ s/-/_/g;
	    ${$var} = $val;
	    next;
        }
	if (/\s*\(\s*setq\s+([A-Za-z-]+)\s+\"(.*)\"\s*\)/) {
	    # string quantity found
	    $var = $1;
	    $val = $2;
	    $var =~ s/-/_/g;
	    ${$var} = $val;
	    next;
	}
    }
    close(IN);
    return $init_msg_seen;
}

# will take alternate set of variables
sub records_write_init_file {
    local($init, $val) = @_;
    local($len) = $#records_external_vars; # length of the array
    local($i, $var, $value, $str);


    # if init file exists 
    # we have to remove the generated stuff from it.
    if ($init) {
	if (!rename("$records_init_file", "$records_init_file~")) {
	    warn("Could not rename $records_init_file: $!\n");
	    return;
	}
	if (!open(IN, "< $records_init_file~")) {
	    die "Couldn't open $records_init_file~: $!\n";
	}
    }
    if (!open(AP, ">> $records_init_file")) {
	die "Couldn't write to $records_init_file: $!\n";
    }
    if ($init) {
	# get stuff from $records_init_file~
	while(<IN>) {
	    if (!/$records_init_mesg/) {
		print AP;
		next;
	    }
	    last;
	}
	close(IN);	
    }
    print AP $records_init_mesg;
    print AP $records_more_init_mesg;
    for ($i = 0; $i < $len; $i += 3) {
	$var = $records_external_vars[$i];
	$str = $records_external_vars[$i+1];
	$value = ${$var . $val};
	$var =~ s/_/-/g;
	if ($str) {
	    print AP "(setq ", $var, " ", "\"",  $value, "\"", ")\n";
        } else {
	    print AP "(setq ", $var, " ", $value, ")\n";
        }
    }
    close(AP);
}

# will take alternate set of variables
# returns 1 if the validate is unsuccessful or else 0
sub records_validate_vars {
    local($val) = @_;
    local($ret) = 0;

    local(*records_directory_structure_a)
	= \${"records_directory_structure_a" . $val};
    local(*records_day_order_a) = \${"records_day_order" . $val};
    local(*records_month_order_a) = \${"records_month_order" . $val};
    local(*records_year_order_a) = \${"records_year_order" . $val};
    local(*records_year_length_a) = \${"records_year_length" . $val};

    if ($records_directory_structure_a != 0 && 
	$records_directory_structure_a != 1 &&
	$records_directory_structure_a != 2) {
	print "records_directory_structure: should have a value of 0, 1 or 2.\n";
	$ret = 1;
    }
    if ($records_day_order_a != 0 &&
	$records_day_order_a != 1 &&
	$records_day_order_a != 2) {
	print "records_day_order: should have a value of 0, 1 or 2.\n";
	$ret = 1;
    }
    if ($records_month_order_a != 0 &&
	$records_month_order_a != 1 &&
	$records_month_order_a != 2) {
	print "records_month_order: should have a value of 0, 1 or 2.\n";
	$ret = 1;
    }
    if ($records_year_order_a != 0 && 
	$records_year_order_a != 1 &&
	$records_year_order_a != 2) {
	print "records_year_order: should have a value of 0, 1 or 2.\n";
	$ret = 1;
    }
    if (($records_day_order_a == $records_month_order_a) ||
	($records_month_order_a == $records_year_order_a) ||
	($records_year_order_a == $records_day_order_a)) {
	print "records_{day|month|year}_order: should have different values.\n";
	$ret = 1;
    }
    if (!($records_year_length_a == 2 || $records_year_length_a == 4)) {
	print "records_year_length: should have a value of 2 or 4.\n";
    }
    return $ret;
}

# will take alternate set of variables
sub records_initialize_vars {
    local($read_init, $val) = @_;
    local($i, $dmy, $dmylen);
    local($dir_regexp, $file_regexp);

    local(*records_day_length_a) = \${"records_day_length" . $val};
    local(*records_month_length_a) = \${"records_month_length" . $val};
    local(*records_year_length_a) = \${"records_year_length" . $val};

    local(*records_day_order_a) = \${"records_day_order" . $val};
    local(*records_month_order_a) = \${"records_month_order" . $val};
    local(*records_year_order_a) = \${"records_year_order" . $val};

    local(*records_date_length_a) = \${"records_date_length" . $val};
    local(*records_date_order_a) = \@{"records_date_order" . $val};
    local(*records_date_a) = \@{"records_date" . $val};

    local(*records_directory_structure_a)
	= \${"records_directory_structure" . $val};
    local(*records_glob_regexp_a) = \@{"records_glob_regexp" . $val};

    if ($read_init) {
	&records_read_init_file();
    }

    $records_day_length_a = 2;
    $records_month_length_a = 2;
    $records_date_length_a = 0;
    @records_date_a = (['year', 0, 0], ['month', 0, 0], ['day', 0, 0]); 

    # initialize records_date_order
    $records_date_order_a[$records_day_order_a] = 'day';
    $records_date_order_a[$records_month_order_a] = 'month';
    $records_date_order_a[$records_year_order_a] = 'year';
    # initialize records_date
    foreach $dmy (@records_date_order_a) {
	for ($i = 0; $i < 3; $i++) {
	    if ($records_date_a[$i][0] eq $dmy) {
		$dmylen = ${"records_" . $dmy . "_length_a"};
		$records_date_a[$i][1] = $records_date_length_a;
		$records_date_a[$i][2] = $dmylen;
		$records_date_length_a += $dmylen;
	    }
	}
    }

    # initialize records_glob_regexp 
    # directory structure + date length considered
    if ($records_directory_structure_a == 0) {
	$dir_regexp = '';
    } elsif ($records_directory_structure_a == 1) {
	for ($i = 0; $i < $records_year_length_a; $i++) {
	    $dir_regexp .= "[0-9]";
	}
	$dir_regexp .= "/";	
    } elsif ($records_directory_structure_a == 2) {
	for ($i = 0; $i < $records_year_length_a; $i++) {
	    $dir_regexp .= "[0-9]";
	}
	$dir_regexp .= "/[0-9][0-9]/";	
    }
    for ($i = 0; $i < $records_date_length_a; $i++) {
	$file_regexp .= "[0-9]";
    }
    @records_glob_regexp_a = ($dir_regexp, $file_regexp);
}

# will take alternate set of variables
# query the user for values
sub records_query_vars {
    local($val) = @_;

    local($len) = $#records_external_vars; # length of the array
    local($i);
    local($var, $doc, $value);
    local($do_validate) = 1;

    while ($do_validate) {
	for ($i = 0; $i < $len; $i += 3) {
	    $var = $records_external_vars[$i];
	    $doc = $records_external_vars[$i+2];
	    if ($val) {
		${$var . $val} = ${$var};
	    }
	    print $doc, "\n";
	    print $var, " (", ${$var}, "): "; 
            chop($value = <>);
	    if ($value ne "") {
		${$var . $val} = $value;
	    }
	    print "\n";
	}
	# validate input
	$do_validate = &records_validate_vars($val);
	if ($do_validate) {
	    print "You have input bad values. Try again.\n\n";
	}
    }
}

sub records_find_records {
    local(@files);
    local($i);
    local($dir_regexp);
    local($file_regexp);
    local($regexp);

    chdir $records_directory ||
	die "Can't change directory to $records_directory: $!\n";
    @files = glob($records_glob_regexp[0] . $records_glob_regexp[1]);
    return @files;
}

sub records_normalize_date {
    local($date) = @_;
    local($i);
    local(@date);
    
    for ($i = 0; $i < 3; $i++) {
	$date[$i] = substr($date, $records_date[$i][1], $records_date[$i][2]);
    }
    if (length($date[0]) == 2) {
	# normalize year
	substr($date[0], 2, 4) = substr($date[0], 0, 2);
	if ($date[0] > 90) {
	    substr($date[0], 0, 2) = "19";
	} else {
	    substr($date[0], 0, 2) = "20";
	}
    }
    # year month day
    return $date[0] . $date[1] . $date[2];
}

# will take alternate set of variables
sub records_denormalize_date {
    local($ndate, $val) = @_;
    local(*records_date_length_a) = \${"records_date_length" . $val};
    local(*records_date_a) = \@{"records_date" . $val};

    local($date) = sprintf("%${records_date_length_a}s", " ");
    if ($records_date_a[0][2] == 2) {
	# denormalize year
	substr($date, $records_date_a[0][1], $records_date_a[0][2]) = 
	    substr($ndate, 2, 2);
    } else {
	substr($date, $records_date_a[0][1], $records_date_a[0][2]) = 
	    substr($ndate, 0, 4);
    }
    substr($date, $records_date_a[1][1], $records_date_a[1][2])
	= substr($ndate, 4, 2);
    substr($date, $records_date_a[2][1], $records_date_a[2][2])
	= substr($ndate, 6, 2);
    return $date;
}

# will take alternate set of variables
# absolute =  0 => relative path from a records file
#	   =  1 => full absolute path
#	   = -1 => relative path from the records directory
sub records_directory_path {
    local($date, $absolute, $val) = @_;
    local(*records_date_a) = \@{"records_date" . $val};
    local(*records_directory_a) = \${"records_directory" . $val};
    local(*records_directory_structure_a) = 
	\${"records_directory_structure" . $val};

    if ($records_directory_structure_a == 0) {
	return ($absolute >= 1) ? $records_directory_a : '';
    } elsif ($records_directory_structure_a == 1) {
	return (($absolute >= 1) ? $records_directory_a . "/" : 
		($absolute <= -1) ? '' : "../") .
		    substr($date, $records_date_a[0][1], $records_date_a[0][2]); 
    } elsif ($records_directory_structure_a == 2) {
	return (($absolute >= 1) ? $records_directory_a . "/" : 
		($absolute <= -1) ? '' : "../../") .
		    substr($date, $records_date_a[0][1], $records_date_a[0][2]) .
			"/" . substr($date, $records_date_a[1][1], 
				     $records_date_a[1][2]); 
    } else {
	die "records_directory_path: bad value\n";
    }
}

sub records_update_file {
    local($file) = @_;
    local($date) = basename($file);
    local($subject);
    local($file_modified);
    local(@records_file);

    ## variables in the finite state machine
    local($regexp, $a_link_regexp, $link_regexp);
    local($a_link_found, $link_found);
    local($subject_found, $full_subject_found);
    local($tag, $tag_generated);
    local($state) = 1;

    # TODO: finite state machine diagram.

    if (!open(IN, "< $file")) {
        warn("$0: cannot open $file.  Skipped.\n");
        return;
    }

    while ($state) {
	if ($state == 1) {
	    if (!($_ = <IN>)) {
		# eof detected
		$state = 0;
		next;
	    }
	    chop;
	    if (!$a_link_found) {
		$state = 2;
	    } else {
		$state = 3;
	    }
	    next;
	} elsif ($state == 2) {
	    # look for subject
	    if (!$subject_found) {
		if (/^\* (.*)\s*$/) {
		    # partial subject was found
		    # get potential subject name
		    $subject = $1;
		    $subject_found = 1;
		}
		$state = 4;
		next;
	    }
	    if (!$full_subject_found) {
		$regexp = "^-{" . sprintf("%d", length($subject) + 2) . "}";
		if (/$regexp/) {
		    $full_subject_found = 1;
		} else {
		    # subject is bogus
		    $subject_found = 0;
		}
		$state = 4;
		next;
	    }
	    # found a records subject
	    $state = 3;
	    next;
	} elsif ($state == 3) {
	    if (!$tag_generated) {
		$tag = records_add_record_to_rawindex($subject, $date);
		$tag_generated = 1;
	    }
	    # check loose link match
	    $a_link_regexp = "^(link|prev|next): <.*>";
	    if (/$a_link_regexp/) {
		$a_link_found = 1;
		$link_regexp = "^link: <.*\/" . $date . "#" . $tag . "\\* " . 
		    $subject . ">";
		if (/$link_regexp/) {
		    $link_found = 1;
		    $state = 4;
		} else {
		    $file_modified = 1 unless ($file_modified);
		    $state = 1;
		}
		next;
	    } else {
		if (!$link_found) {
		    $file_modified = 1 unless ($file_modified);
		    push(@records_file, "link: <" . &records_directory_path($date)
			 . "/" . $date . "#" . $tag . "* " . $subject . ">\n");
		}
		$a_link_found = 0;
		$link_found = 0;
		$subject_found = 0;
		$full_subject_found = 0;
		$tag_generated = 0;
		$state = 2;
		next;
	    }
	} elsif ($state == 4) {
	    push(@records_file, $_ . "\n");
	    $state = 1;
	    next;
	}
    }
    close (IN);
    if ($file_modified) {
	&records_write_file($file, *records_file);
    } else {
	print "$file unchanged.\n" if ($verbose);
    }
}

# TODO: finish writing it
# will take alternate set of variables
# after format conversion, update the links in file. 
sub records_update_links {
    local($file, $val) = @_;
    local(@records_file, $file_modified);
    local($newdate, $newdir);

    local($link_regexp) = "<(.*)" . $records_glob_regexp[0] . "("
	. $records_glob_regexp[1] . ")(#[0-9]*\\* .*)>";

    if (!open(IN, "< $file")) {
        warn("$0: cannot open $file.  Skipped.\n");
        return;
    }
    while (<IN>) {
	if (!/$link_regexp/) {
	    push(@records_file, $_);
	} else {
	    $file_modified = 1 unless ($file_modified);
	    $newdate = &records_denormalize_date(&records_normalize_date($2),
					       $val);
	    $newdir = &records_directory_path($newdate, 0, $val);
	    # TODO: broken
	    push(@records_file, $` . "<" . $newdir . "/" . $newdate 
		 . $3 . ">" . $');
	}
    }
    close(IN);
    if ($file_modified) {
	&records_write_file($file, *records_file);
    } else {
	print "$file unchanged.\n" if ($verbose);
    }
}

sub records_add_record_to_rawindex {
    local($subject, $date) = @_;
    local($ndate) = &records_normalize_date($date);
    local($key) = $subject . $ndate;

    # add the date and count of number of subjects to the date index
    if (!exists($records_date_index{$ndate})) {
	$records_date_index{$ndate} = 1;
    } else {
	$records_date_index{$ndate}++;
    }
    if (!exists($records_raw_index{$key})) {
	$records_raw_index{$key} = 0;
	return '';
    } else {
	return sprintf("%d", ++$records_raw_index{$key});
    }
}
    
sub records_make_index {
    local($subject_ndate);
    local($subject, $old_subject);
    local($ndate, $date);
    local($tags, $i);
 
    if ( -T "$records_index_file" ) {
	rename("$records_index_file", "$records_index_file~") ||
	    die("Could not rename $records_index_file: $!\n");
    }
    open(INDEX, ">$records_index_file") || 
	die("Could not open $records_index_file for writing: $!\n");
    # index start up 
    print INDEX "-*- records-index -*- ";
    foreach $subject_ndate (sort keys(%records_raw_index)) {
	$subject = substr($subject_ndate, 0, -8);
	$ndate = substr($subject_ndate, -8);
	$date = &records_denormalize_date($ndate);
	$tags = $records_raw_index{$subject_ndate};
	if ($subject ne $old_subject) {
	    # new subject. write it out
	    print INDEX "\n", $subject, ": ";
	    $old_subject = $subject;
	}
	# write out the first tag
	print INDEX $date . " ";
	# write out other tags if they exist
	for ($i = 1; $i <= $tags; $i++) {
	    print INDEX $date . "#" . sprintf("%d", $i) . " ";
	}
    }
    print INDEX "\n";
    close(INDEX);
}

sub records_make_dindex {
    local($date, $ndate);

    if ( -T "$records_dindex_file" ) {
	rename("$records_dindex_file", "$records_dindex_file~") ||
	    die("Could not rename $records_dindex_file: $!\n");
    }
    open(DINDEX, ">$records_dindex_file") || 
	die("Could not open $records_dindex_file for writing: $!\n");
    print DINDEX " "; 
    foreach $ndate (sort keys (%records_date_index)) {
	$date = &records_denormalize_date($ndate);
	print DINDEX $date, "#", $records_date_index{$ndate}, " ";
    }
    close (DINDEX);
}

sub records_install_init_file {
    local($init) = &records_read_init_file();
    local($val);
    if ($init) {
	print "You have a valid $records_init_file.
I will use its values to initialize your records.
Unless you know what you are doing, do not change these values.
If you wish to change the date or directory format, use 
$0 -c\n\n";
	print "Do you still want to re-initialize your init file (y or n) ";
	chop($val= <>);
	if ($val eq "n") {
	    return;
	}
    }

    print "The default values of the variables are shown in parenthesis.
If you want the default value, press return.\n\n";

    # query the user for values
    &records_query_vars();
    # write out the init file
    &records_write_init_file($init);
    # Add stuff to emacs init file
    open(EIT, ">>$records_emacs_init_file") ||
	die "Could not open $records_emacs_init_file for writing: $!\n";
    print EIT "(load \"$records_init_file\")\n";
    close EIT;
    &mkdirhier($records_directory, 0700) unless ( -d $records_directory);

}

sub records_convert_format {
    local($file, @records_files, $newfile, $newdir);
    local($date, $ndate, $newdate);
    
    if (!&records_read_init_file()) {
	print "Records init file has not been initialized.\n";
	print "Run $0 -i to initialize the init file before
attempting to convert records formats.\n\n";
	exit(1);
    }
    # original set of variables has been read and initialized
    &records_initialize_vars(1);
    # read the new set of vars.
    print "You must input the new values of the records variables.\n\n";
    &records_query_vars($alternate);
    &records_initialize_vars(0, $alternate);
    @records_files = &records_find_records();
    foreach $file (@records_files) {
	$date = basename($file);
	$ndate = &records_normalize_date($date);
	$newdate = &records_denormalize_date($ndate, $alternate);
	$newdir = &records_directory_path($newdate, 1, $alternate);
	$newfile =  $newdir . "/" . $newdate;
	if ($file eq $newfile) {
	    die "The old and new variable settings are the same.
We will not go any further!!\n";
	}
	&mkdirhier($newdir, 0700) unless ( -d $newdir);
	rename($file, $newfile) ||
	    warn "Couldn't rename $file to $newfile: $!\n";
	&records_update_links($newfile, $alternate);
    }
    &records_write_init_file(1, $alternate);
}

sub records_recreate_indexes {
    local(@records_files);

    &records_initialize_vars(1);
    @records_files = &records_find_records();

    foreach (@records_files) {
	&records_update_file($_);
    }
    &records_make_index();
    &records_make_dindex();
}

