#!/usr/bin/perl
#  engine.pl - the CBB 'engine'.
#              This script implements a transaction abstract data type
#              It encapsulates a list a transactions and the functions
#              required to manipulate the transactions.
#
#  Written by Curtis Olson.  Started August 22, 1994.
#
#  Copyright (C) 1994 - 1997  Curtis L. Olson  - curt@sledge.mn.org
#
#  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., 675 Mass Ave, Cambridge, MA 02139, USA.

# $Id: engine.pl,v 2.18 1997/05/06 02:35:14 curt Exp $
# (Log is kept at end of this file)


package CBB;

use strict;   # don't take no guff


# @INC specifies the installed location of the necessary pieces.
# It should already be setup by wrapper.pl

require "common.pl";
require "log.pl";


$| = 1;				# flush buffers after every write

if ( $CBB::logging != 0 && $CBB::logging != 1) {
    # if not specified elsewhere, turn on logging
    $CBB::logging = 1;			# 0 = off,  1 = on
}

if ( $CBB::debug != 0 && $CBB::debug != 1) {
    # if not specified elsewhere, turn off debugging.
    $CBB::debug = 0;			# 0 = off,  1 = on
}

# Global variables

# %CBB::TRANS - an associative array of transactions and transaction keys
# @CBB::KEYS - a sorted list of transaction keys (for traversing the trans list)
# $CBB::sorted_keys - specifies whether the list in @CBB::KEYS is valid
# $CBB::calced - specified whether the transactions have been properly calculated
# $CBB::current - specifies the "current" position in the @CBB::KEYS array
# $CBB::current_file - full name of currently opened transaction file
# %CBB::BALS - an associative array used to store account information
# $CBB::version - version number (set in common.pl)


&init_trans();		   # initialize %CBB::TRANS, @CBB::KEYS, and $CBB::sorted_keys
open(DEBUG, ">debug") if $CBB::debug;


# toggle debugging
sub debug {
    # in: flag
    # out: flag

    my($newdebug) = @_;

    if ($newdebug == 1) {
	# turning debugging on

	if ($CBB::debug == 1) {
	    # already on, do nothing
	} else {
	    $CBB::debug = 1;
	    open(DEBUG, ">debug");
	}
    } else {
	# turning of debugging

	if ($CBB::debug == 0) {
	    # already off, do nothing
	} else {
	    $CBB::debug = 0;
	    close(DEBUG);
	}
    }

    return $CBB::debug;
}
  

# get next available key for a specified date
sub get_next_key {
    # in: date
    # out: key

    my($date) = @_;
    my($count) = 0;

    while ( $CBB::TRANS{"$date-".&pad($count)} ) {
	$count++;
    }

    return "$date-".&pad($count);
}


# set @CBB::KEYS = sorted list of transaction keys
sub sort_keys {
    $CBB::sorted_keys = 1;
    $CBB::current = 0;

    print DEBUG "sort_keys()\n" if $CBB::debug;
    @CBB::KEYS = sort(keys %CBB::TRANS);
}


# recalculate the transactions
sub calc_trans {
    my($total, $ntotal, $stotal, $ctotal) = (0.00, 0.00, 0.00, 0.00);
    my($count, $ncount, $scount, $ccount) = (0, 0, 0, 0);
    my($key);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk);

    my($current_date) = &raw_date();
	
    $CBB::calced = 1;

    print DEBUG "calc_trans()\n" if $CBB::debug;

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    foreach $key (@CBB::KEYS) {
        ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, 
        	$junk) = split(/\t/, $CBB::TRANS{$key});

	$total = $total + $credit - $debit;
	$count++;

	if ( $date <= $current_date ) {
	    $CBB::BALS{"Current"} = $total;
	}

	if ( ($cleared eq "x") || ($cleared eq "X") ) {
	    $ctotal = $ctotal + $credit - $debit;
	    $ccount++;
	} elsif ( $cleared eq "*" ) {
	    $stotal = $stotal + $credit - $debit;
	    $scount++;
	} else {
	    $ntotal = $ntotal + $credit - $debit;
	    $ncount++;
	}

	$CBB::TRANS{$key} = 
	  "$date\t$check\t$desc\t$debit\t$credit\t$cat\t$com\t$cleared\t".
	  	sprintf("%.2f", $total);
    }

    $CBB::BALS{"Amount"} =  $total;
    $CBB::BALS{"Count"} =   $count;

    $CBB::BALS{"Xamount"} = $ctotal;
    $CBB::BALS{"Xcount"} =  $ccount;

    $CBB::BALS{"*amount"} = $stotal;
    $CBB::BALS{"*count"} =  $scount;

    $CBB::BALS{"Namount"} = $ntotal;
    $CBB::BALS{"Ncount"} =  $ncount;
}


# create a transaction (and add to the transaction list)
sub create_trans {
    # in: transaction
    # out: keyed_transaction

    my($trans) = @_;
    my($key);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    &insert_and_update_mem($trans);

    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) =
	split(/\t/, $trans);

    if ( length($date) == 6 ) {
	# for backwards compatibility ... shouldn't be needed now.
	$date = "19$date";
    }

    $key = &get_next_key($date);

    $trans = "$date\t$check\t$desc\t$debit\t$credit\t$cat\t$com\t$cleared\t$total";

    $CBB::TRANS{$key} = "$trans";

    print DEBUG "created:  $key\t$trans\n" if $CBB::debug;

    return "$key\t$trans";
}


# create a transfer transaction in the current file and the transfer to file
sub create_xfer {
    # in: transaction
    # out: keyed_transaction

    my($trans) = @_;
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) =
	split(/\t/, $trans);    
    my($orig_file) = $CBB::current_file;
    my($to_trans, $to_file, $from_cat);
    my($key, $result);
    my($returned_result);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    print DEBUG "(xfer) current_file = $CBB::current_file\n" if $CBB::debug;
    # determine the "from" category
    $from_cat = "[".&file_basename(&file_root($CBB::current_file))."]";

    # determine the "to" file name
    $to_file = $cat;
    chop($to_file);
    $to_file = substr($to_file, 1);
    $to_file = &file_dirname($CBB::current_file)."/$to_file";
    print DEBUG "to file = '$to_file' ($to_file.cbb)\n" if $CBB::debug;
    if ( -e "$to_file.cbb" ) {
	$to_file .= ".cbb";
    } elsif ( -e "$to_file.dir" ) {
	$to_file .= ".dir";
    } else {
	return "error";
    }

    print DEBUG "Transfer to $to_file\n" if $CBB::debug;

    # create the "to" transaction
    $to_trans = "$date\t$check\t$desc\t".$credit."\t".$debit."\t".
		$from_cat."\t$com\t$cleared\t$total";

    # we need special handling here to preserve the .cbb file
    # save the current transactions to a temporary file
    # before loading the "to" account
    $result = &save_trans("$orig_file.$$.tmp");
    return "error" if ( $result eq "error" );
    %CBB::TRANS = ();  # clear out any transactions from the current file

    # open the "to" account
    $result = &load_trans($to_file);
    return "error" if ( $result eq "error" );
    
    $result = &create_trans($to_trans);
    
    $result = &save_trans($to_file);

    $result = &load_cbb_trans("$orig_file.$$.tmp");
    return "error" if ( $result eq "error" );
    unlink("$orig_file.$$.tmp");
    $CBB::current_file = $orig_file;

    # create the "from" transaction
    $returned_result = &create_trans($trans);

    return "$returned_result";
}


# update a transaction (replace in the transaction list)
sub update_trans {
    # in: keyed_transaction
    # out: keyed_transaction

    my($keyed_trans) = @_;
    my($key, $trans, $result);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    ($key, $trans) = split(/\t/, $keyed_trans, 2);

    &delete_trans($key);
    $result = &create_trans($trans);

    print DEBUG "updated:  $key\n" if $CBB::debug;
    print DEBUG "     to:  $result\n" if $CBB::debug;

    return "$result";
}


# update a transfer transaction (replace in the transaction list)
sub update_xfer {
    # in: keyed_transaction
    # out: keyed_transaction

    my($keyed_trans) = @_;
    my($key, $trans, $result);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    ($key, $trans) = split(/\t/, $keyed_trans, 2);

    &delete_xfer($key);
    $result = &create_xfer($trans);

    print DEBUG "updated:  $key\n" if $CBB::debug;
    print DEBUG "     to:  $result\n" if $CBB::debug;

    return "$result";
}


# delete a transaction given the key
sub delete_trans {
    # in: key

    my($key) = @_;
    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    delete $CBB::TRANS{$key};

    if ($CBB::current > 0) {
	--$CBB::current;
    }

    print DEBUG "deleted:  $key\n" if $CBB::debug;

    return "ok";
}

# delete an transfer transaction in the transfer to file
sub delete_xfer {
    # in: key

    my($key) = @_;
    my($orig_file, $orig_current) = ($CBB::current_file, $CBB::current);
    my($count) = 0;

    my($to_file, $from_cat, $found_key, $found_trans);
    my($result);

    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) 
	= split(/\t/, $CBB::TRANS{$key});

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    # determine the "from" category
    $from_cat = "[".&file_basename(&file_root($CBB::current_file))."]";

    # determine the "to" file name
    $to_file = $cat;
    chop($to_file);
    $to_file = substr($to_file, 1);
    $to_file = &file_dirname($CBB::current_file)."/$to_file";
    print DEBUG "to file = '$to_file' ($to_file.cbb)\n" if $CBB::debug;
    if ( -e "$to_file.cbb" ) {
	$to_file .= ".cbb";
    } else {
	return "error";
    }

    print DEBUG "Deleting transfer to $to_file\n" if $CBB::debug;

    # We need special handling here to preserve the .cbb file.  Save
    # the current transactions to a temporary file before loading the
    # "to" account.
    $result = &save_trans("$orig_file.$$.tmp");
    return "error" if ( $result eq "error" );
    
    # open the "to" account
    $result = &load_trans($to_file);
    return "error" if ( $result eq "error" );

    # now search for the transaction
    while ( $found_trans = $CBB::TRANS{"$date-".&pad($count)} ) {
	my($found_date, $found_check, $found_desc, $found_debit, 
	      $found_credit, $found_cat, $found_com, $found_cleared, 
	      $found_total) = split(/\t/, $found_trans);

	last if (($found_check eq $check) && 
		 ($found_desc eq $desc) &&
		 ($found_debit == $credit) && 
		 ($found_credit == $debit) &&
		 ($found_com eq $com) && 
		 ($found_cat eq $from_cat) && 
		 ($found_key = "$date-".&pad($count)) );

	$count++;
    }

    print DEBUG "Found key: $found_key\n" if $CBB::debug;

    if ( $found_key ) {
	delete $CBB::TRANS{$found_key};
    
	$CBB::calced = 0;
	$CBB::sorted_keys = 0;
    } else {
	print DEBUG "Transaction not found in $to_file\n" if $CBB::debug;
    }

    # now save the "to" account
    $result = &save_trans($to_file);

    # revert to orig account
    $result = &load_cbb_trans("$orig_file.$$.tmp");
    return "error" if ( $result eq "error" );
    unlink("$orig_file.$$.tmp");

    # restore global variables
    $CBB::current_file = $orig_file;
    $CBB::current = $orig_current;
    $CBB::calced = 0;
    $CBB::sorted_keys = 0;

    delete $CBB::TRANS{$key};

    if ($CBB::current > 0) {
	--$CBB::current;
    }

    print DEBUG "deleted:  $key\n" if $CBB::debug;

    return "ok";
}


# return the next transaction
sub next_trans {
    my($trans);

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    ++$CBB::current;
    $trans = $CBB::TRANS{$CBB::KEYS[$CBB::current]};
    if ( $trans ) {
        return "$CBB::KEYS[$CBB::current]\t$trans";
    } else {
        return "none";
    }
}


# return the transaction specified by a key
sub find_trans {
    # uses a binary search so that we can keep $CBB::current current.   
    # Yeeeks! I have to think for a change.
    # Hmmm, maybe I should rethink my data structures ... nah. :)

    my($key) = @_;
    my($left, $middle, $right) = (0, 0, $#CBB::KEYS);
    my($trans);

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    $trans = "";

    while ( $left <= $right ) {
	$middle = int( ($left + $right) / 2 );
        print DEBUG "$left < $middle < $right\n" if $CBB::debug;
	if ( $CBB::KEYS[$middle] lt $key ) {
	    $left = $middle + 1;
	    print DEBUG "  left = middle + 1\n" if $CBB::debug;
        } elsif ( $CBB::KEYS[$middle] gt $key ) {
	    $right = $middle - 1;
	    print DEBUG "  right = middle - 1\n" if $CBB::debug;
        } else {
	    # we found it, set $trans to what we want and force an exit of
	    # the while loop
	    $trans = $CBB::TRANS{$CBB::KEYS[$middle]};
	    print DEBUG "  found it: $trans\n" if $CBB::debug;
	    $CBB::current = $middle;
	    $left = $right + 1;
        }
    }

    print DEBUG "found:  $key\t$trans\n" if $CBB::debug;

    if ( $trans ) {
        return "$key\t$trans";
    } else {
        return "none";
    }
}


# returns the current index
sub get_current_index {
    return ($CBB::current + 1);
}


# return the first transaction
sub first_trans {
    my($trans);

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    $CBB::current = 0;
    $trans = $CBB::TRANS{$CBB::KEYS[$CBB::current]};
    if ( $trans ) {
        return "$CBB::KEYS[$CBB::current]\t$trans";
    } else {
        return "none";
    }
}


# returns the entire transaction list in one big chunk.
sub all_trans {
    # in: date
    # out: result

    my($date_fmt) = @_;
    my($key, $nicecat, $cutcom, $cutdesc, $cutcheck, $nicedate, $checklen);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total);
    my($day, $month, $year);

    $| = 0;				# turn off buffer flushing

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    foreach $key (@CBB::KEYS) {
	# print ("$key\t$CBB::TRANS{$key}\n");
        ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) =
	    split(/\t/, $CBB::TRANS{$key});

        if ( length($date) == 6 ) {
	    # for backwards compatibility ... shouldn't be needed now.
            ($year, $month, $day) = $date =~ /(\d\d)(\d\d)(\d\d)/;
	    $year = "19" . $year;
        } else {
            ($year, $month, $day) = $date =~ /(\d\d\d\d)(\d\d)(\d\d)/
        }

        $checklen = length($check);
        if ( $checklen > 5 ) {
            $cutcheck = substr($check, $checklen - 5, 5);
        } else {
            $cutcheck = $check;
        }

        if ( $date_fmt == 1 ) {
            $nicedate = "$month/$day/" . substr($year, 2, 2);
        } else {
            $nicedate = "$day.$month." . substr($year, 2, 2);
        }

        $cutdesc = substr($desc, 0, 15);
        $cutcom = substr($com, 0, 15);
        if ( $cat =~ m/\|/ ) {
            $nicecat = "-Splits-";
        } else {
            $nicecat = $cat;
        }
	$nicecat = substr($nicecat, 0, 9);

	printf("%5s  %-8s  %-15s  %9.2f  %9.2f  %-1s %10.2f %14s\n",
	       $cutcheck, $nicedate, $cutdesc, $debit, $credit, $cleared, 
	       $total, $key);
        printf("%5s  %-8s  %-15s  %-9s %39s\n", "", "", $cutcom, $nicecat, 
	       $key);
    }

    $| = 1;				# turn buffer flushing back on

    return "none";
}

# returns part of the transaction list in one big chunk. (since a date)
sub part_trans {
    # in: date
    # out: result

    my($sdate_fmt) = @_;
    my($left, $middle, $right) = (0, 0, $#CBB::KEYS);
    my($date_fmt, $sdate);
    my($key, $nicecat, $cutcom, $cutdesc, $cutcheck, $nicedate, $checklen);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total);
    my($day, $month, $year);

    # two arguments: data_format and start date
    ($date_fmt, $sdate) = split(" ", $sdate_fmt, 2);

    $| = 0;				# turn off buffer flushing

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    # look for first key past starting with sdate (borrowed from find_trans)
    $sdate = "$sdate-".&pad(0);

    while ( $left <= $right ) {
	$middle = int( ($left + $right) / 2 );
	if ( $CBB::KEYS[$middle] lt $sdate ) {
	    $left = $middle + 1;
        } elsif ( $CBB::KEYS[$middle] gt $sdate ) {
	    $right = $middle - 1;
        } else {
	    # we found it, force an exit of the while loop
	    $left = $right + 1;
        }
    }
    if ($CBB::KEYS[$middle] != $sdate) {
	 # we found the first past sdate
	 $middle = $left;
    }

    for (; $middle <= $#CBB::KEYS ; ++$middle) {
	$key=$CBB::KEYS[$middle];

	# print ("$key\t$CBB::TRANS{$key}\n");
        ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) =
	    split(/\t/, $CBB::TRANS{$key});

        if ( length($date) == 6 ) {
	    # for backwards compatibility ... shouldn't be needed now.
            ($year, $month, $day) = $date =~ /(\d\d)(\d\d)(\d\d)/;
	    $year = "19" . $year;
        } else {
            ($year, $month, $day) = $date =~ /(\d\d\d\d)(\d\d)(\d\d)/
        }

        $checklen = length($check);
        if ( $checklen > 5 ) {
            $cutcheck = substr($check, $checklen - 5, 5);
        } else {
            $cutcheck = $check;
        }

        if ( $date_fmt == 1 ) {
            $nicedate = "$month/$day/" . substr($year, 2, 2);
        } else {
            $nicedate = "$day.$month." . substr($year, 2, 2);
        }

        $cutdesc = substr($desc, 0, 15);
        $cutcom = substr($com, 0, 15);
        if ( $cat =~ m/\|/ ) {
            $nicecat = "-Splits-";
        } else {
            $nicecat = $cat;
        }
	$nicecat = substr($nicecat, 0, 9);

	printf("%5s  %-8s  %-15s  %9.2f  %9.2f  %-1s %10.2f %14s\n",
	       $cutcheck, $nicedate, $cutdesc, $debit, $credit, $cleared, 
	       $total, $key);
        printf("%5s  %-8s  %-15s  %-9s %39s\n", "", "", $cutcom, $nicecat, 
	       $key);
    }

    $| = 1;				# turn buffer flushing back on

    return "none";
}

# return the first uncleared transaction
sub first_uncleared_trans {
    my($trans);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk);
    
    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    $CBB::current = 0;
    $trans = $CBB::TRANS{$CBB::KEYS[$CBB::current]};
    ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk) = 
    	    split(/\t/, $trans);
    while ( $cleared eq "x" ) {
        ++$CBB::current;
        $trans = $CBB::TRANS{$CBB::KEYS[$CBB::current]};
        ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk) = 
    	        split(/\t/, $trans);
    }

    if ( $trans ) {
        return "$CBB::KEYS[$CBB::current]\t$trans";
    } else {
        return "none";
    }
}


# return the next uncleared transaction
sub next_uncleared_trans {
    my($trans);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk);

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    ++$CBB::current;
    $trans = $CBB::TRANS{$CBB::KEYS[$CBB::current]};
    ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk) = 
    	    split(/\t/, $trans);
    while ( $cleared eq "x" ) {
        ++$CBB::current;
        $trans = $CBB::TRANS{$CBB::KEYS[$CBB::current]};
        ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $junk) = 
    	        split(/\t/, $trans);
    }

    if ( $trans ) {
        return "$CBB::KEYS[$CBB::current]\t$trans";
    } else {
        return "none";
    }
}


# select transaction -- primes a transaction for future clearing
sub select_trans {
    # in: key
    # out: keyed_transaction

    my($key) = @_;
    my($trans);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    $trans = $CBB::TRANS{$key};
    ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) = 
    	    split(/\t/, $trans);

    $cleared = "*";

    $CBB::TRANS{$key} = 
	  "$date\t$check\t$desc\t$debit\t$credit\t$cat\t$com\t$cleared\t$total";

    print DEBUG "selected:  $key to be cleared\n" if $CBB::debug;

    return "$key\t$CBB::TRANS{$key}";
}


# select transaction -- primes a transaction for future clearing
sub unselect_trans {
    # in: key
    # out: keyed_transaction

    my($key) = @_;
    my($trans);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    $trans = $CBB::TRANS{$key};
    ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) = 
    	    split(/\t/, $trans);

    $cleared = "";

    $CBB::TRANS{$key} = 
	  "$date\t$check\t$desc\t$debit\t$credit\t$cat\t$com\t$cleared\t$total";

    print DEBUG "unselected:  $key will not be cleared\n" if $CBB::debug;

    return "$key\t$CBB::TRANS{$key}";
}


# clear all selected transactions
sub clear_trans {
    my($key, $trans);
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total);

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    foreach $key (@CBB::KEYS) {
        $trans = $CBB::TRANS{$key};
        ($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) = 
    	        split(/\t/, $trans);

	if ( $cleared eq "*" ) {
            $cleared = "x";

            $CBB::TRANS{$key} = 
	          "$date\t$check\t$desc\t$debit\t$credit\t$cat\t$com\t$cleared\t$total";
        }
    }
}


# return the cleared balance (this should be the last statement ending bal)
sub get_cleared_bal {
    return sprintf("%.2f", $CBB::BALS{"Xamount"});
}


# initialize the transactions data structure
sub init_trans {
    # out: result

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;
    @CBB::KEYS = ();

    return "ok";
}


# make a new account
sub make_acct {
    # in: acct-name acct-desc acct-type
    # out: result
    
    my($name, $desc) = split(/ /, $_[0], 2);
    my($pos, $short_name);

    print DEBUG "Make account $name - $desc\n" if $CBB::debug;
    # print "Make account $name - $desc\n";

    print DEBUG "Making cbb account\n" if $CBB::debug;

    open(SAVE, ">$name.cbb.new");
    close(SAVE);
    unlink("$name.cbb.bak");
    rename("$name.cbb", "$name.cbb.bak");
    rename("$name.cbb.new", "$name.cbb");
    
    $CBB::current_file = "$name.cbb";

    # Assume we have category already open ... :| ??? :(

    # strip leading path from $name
    &insert_cat("[".&file_basename($name)."]\t$desc\t");

    # save the categories file before it gets toasted
    &save_cats(&file_dirname($name) . "/categories");

    return "ok";
}


# determine the file type and call the correct load routine
sub load_trans {
    # in: file base
    # out: result

    my($file) = @_;
    my($ext) = &file_extension($file);

    # print "$ext\n";
    # print &file_root($file) . "\n";

    print DEBUG "file extension is: $ext\n" if $CBB::debug;

    if ($CBB::cache) {
    	no strict 'vars';	# necessary for this special hack
    	no strict 'refs';

	# save current data to cache
	my($hname) = "ACC_" . &file_basename($CBB::current_file);
    print DEBUG "$hname $CBB::current_file\n" if $CBB::debug;
	%$hname = %CBB::TRANS;

	# test if new table already in cache
	$hname = "ACC_" . &file_basename($file);
    print DEBUG "$hname\n" if $CBB::debug;
	if (scalar (%$hname) ) {
		print DEBUG "$hname defined , load from cache\n" if $CBB::debug;

		$CBB::sorted_keys = 0;
		$CBB::calced = 0;
		
		%CBB::TRANS = %$hname;	# take values from the cache
		&calc_trans();

		$CBB::current_file = $file;

		return "ok";
	}
    }

    return &load_cbb_trans($file);
}


# load the data from a cbb file
sub load_cbb_trans {
    # in: file name (including .cbb extension)
    # out: result

    my($file) = @_;
    my($file_version) = "";
    my($junk);

    $CBB::sorted_keys = 0;
    $CBB::calced = 0;

    print DEBUG "Loading the cbb format file: $file\n" if $CBB::debug;

    if ( $CBB::decrypt ne "" ) {
	open(LOAD, "$CBB::decrypt < $file|") || return "error";
    } else {
    	open(LOAD, "<$file") || return "error";
    }

    %CBB::TRANS = ();	# clear out any transactions from the previous file

    while ( <LOAD> ) {
	if ( m/^#/ ) {
	    # toss the comment (but first check for any goodies.)
	    if ( m/version/i ) {
		($junk, $junk, $junk, $file_version) = split;
		print DEBUG "Data file version = $file_version\n" if $CBB::debug;
	    }
	} else {
	    if ( $file_version eq "") {
		print DEBUG "no data file version, file encrypted ?" if $CBB::debug;
		close(LOAD);
		return "error";
	    }
            chop;
	    if ( ! m/\t/ ) {
		s/:/\t/g;
		$_ = &fix_splits($_);
	    }
            &create_trans($_);
	}
    }

    close(LOAD);

    &calc_trans();

    $CBB::current_file = $file;

    return "ok";
}


sub fix_splits {
    # in: transaction with old two field per record splits
    # out: transaction with new three field per record splits

    my($line) = @_;
    my($date, $check, $desc, $debit, $credit, $cat, $com, $cleared, $total) =
	split(/\t/, $line);
    my(@cats, $i, $max, $newcat);

    if ( $cat =~ m/\|/ ) {
        @cats = split(/\|/, $cat);

        $i = 0;
        $max = ($#cats - 1) / 2;
        $newcat = "|";

        while ( $i < $max ) {
    	    $newcat .= $cats[$i * 2 + 1] . "||" . 
		       $cats[$i * 2 + 2] . "|";
	    $i++;
        }
    } else {
	$newcat = $cat;
    }

    return "$date\t$check\t$desc\t$debit\t$credit\t$newcat\t$com\t$cleared\t$total";
}


# load the data from a dbm file
sub load_dbm_trans {
    # in: file base name
    # out: result

    my($file) = @_;
    print DEBUG "Loading the dbm format file: $file\n" if $CBB::debug;

    if ( -e "$file" ) {
	$CBB::current_file = $file;
	$CBB::sorted_keys = 0;
	$CBB::calced = 0;

	dbmclose(%CBB::TRANS);
	dbmopen(%CBB::TRANS, &file_root($file), 0666) || return "error";
	
	# test to see if this file is <tab> delimited
	&sort_keys();
	# never ever call calc_trans() at this point (or call something that
	# calls it
	if (defined($CBB::TRANS{$CBB::KEYS[0]}) && 
	    !($CBB::TRANS{$CBB::KEYS[0]} =~ m/\t/) ) {
	    print DEBUG "'$CBB::TRANS{$CBB::KEYS[0]}' = old version of CBB dbm file\n"
		if $CBB::debug;
	    return "error - old version of CBB dbm file";
	} else {
	    print DEBUG "valid txn: '$CBB::TRANS{$CBB::KEYS[0]}'\n" 
		if $CBB::debug;
        }

	return "ok";
    } else {
	return "error";
    }
}


# save all the precious data to a file
sub save_trans {
    # in: file name (including .cbb extension)
    # out: result

    my($file) = @_;
    my($auto_save_file, $key);
    my(@trans);

    print DEBUG "Saving the cbb format file: $file\n" if $CBB::debug;

    if ($CBB::calced == 0) {
	&calc_trans();
    }

    if ($CBB::sorted_keys == 0) {
	&sort_keys();
    }

    if ( $CBB::encrypt ne "" ) {
	open(SAVE, "|$CBB::encrypt > $file.new") || return "error";
    } else {
    	open(SAVE, ">$file.new") || return "error";
    }

    # Print some header stuff
    print (SAVE "# CBB Data File -- $file\n");
    print (SAVE "#\n");
    print (SAVE "# CBB Version = $CBB::version_num\n");
    printf (SAVE "# Current Balance = %.2f\n", $CBB::BALS{Current});
    printf (SAVE "# Ending Balance = %.2f\n", $CBB::BALS{Amount});
    print (SAVE "# Transaction Count = $CBB::BALS{Count}\n");
    printf (SAVE "# Cleared Balance = %.2f\n", $CBB::BALS{Xamount});
    print (SAVE "# Cleared Txn Count = $CBB::BALS{Xcount}\n");
    print (SAVE "# Saved on (US Date Fmt) " . &nice_date("1") . " ");
    print (SAVE "by $CBB::user_name\n");
    print (SAVE "#\n");
    print (SAVE "# date  check  desc  debit  credit  cat  com  cleared\n");
    print (SAVE "# ---------------------------------------------------\n");

    foreach $key (@CBB::KEYS) {
	# strip off last total
	@trans = split(/\t/, $CBB::TRANS{$key});
	print SAVE join ("\t", @trans[0..7]) . "\n";
    }

    close(SAVE);

    unlink("$file.bak");
    rename("$file", "$file.bak");
    rename("$file.new", "$file");

    $auto_save_file = &file_dirname($file) . "#" . &file_basename($file) . "#";
    print DEBUG "auto_save_file = $auto_save_file\n" if $CBB::debug;
    if ( -e $auto_save_file ) {
	unlink("$auto_save_file");
	unlink("$auto_save_file.bak");
    }

    return "ok";
}


1;

# ----------------------------------------------------------------------------
# $Log: engine.pl,v $
# Revision 2.18  1997/05/06 02:35:14  curt
# Removed an extranious my()
#
# Revision 2.17  1997/05/06 01:00:27  curt
# Added patches contributed by Martin Schenk <schenkm@ping.at>
# - Default to umask of 066 so .CBB files get created rw by owner only
# - Added support for pgp encrypting data files
# - Added support for displaying only recent parts of files (avoids
#   waiting to load in lots of old txns you don't currently need.)
# - Added a feature to "cache" whole accounts in the perl engine so
#   that switching between accounts can be faster.
# - The above options can be turned on/off via the preferrences menu.
#
# Revision 2.16  1997/04/12 01:15:24  curt
# Display current balance rather than ending balance in the account list
# box.  This makes a difference if you like to insert future transactions.
#
# Revision 2.15  1997/04/11 20:24:01  curt
# Automatically insert new transactions into the memorized list.
#
# Revision 2.14  1997/04/04 18:41:35  curt
# Fixed a small bug in editing transfer transactions.
#
# Revision 2.13  1997/03/04 03:23:00  curt
# Fixed bug which caused a transfer transaction to not show up in the list
# box even though it had been correctly inserted.
#
# Revision 2.12  1997/01/18 03:28:42  curt
# Added "use strict" pragma to enforce good scoping habits.
#
# Revision 2.11  1997/01/10 22:03:30  curt
# Transfer fixups and a few other misc. fixes contributed by
#   Lionel Mallet <Lionel.Mallet@sophia.inria.fr>
#
# Revision 2.10  1997/01/02 04:38:32  curt
# Changes over the 1996 holidays:
#   - Converted listbox to text widget.  This allows us to do nice
#     things with alternating background colors, highliting, red
#     negative numbers, etc.
#   - Negative transactions are now drawn in red.
#   - Added a Goto <Today> option.
#   - <Home> & <End> were double bound.  Now, listbox can be traversed with
#     <Meta-Home> and <Meta-End>
#
# Revision 2.9  1996/12/17 20:15:43  curt
# Version incremented to 0.70.
# No longer save running total in .cbb files.
# Miscellaneous tweaks.
#
# Revision 2.8  1996/12/17 14:53:54  curt
# Updated copyright date.
#
# Revision 2.7  1996/12/11 18:33:32  curt
# Ran a spell checker.
#
# Revision 2.6  1996/10/02 19:37:19  curt
# Replaced instances of hardcoded century (19) with a variable.  We need to
# know the current century in cases where it is not provided and it is
# assumed to be the current century.  Someday I need to figure out how
# to determine the current century, but I have a couple of years to do it. :-)
#
# I still need to fix conf-reports and reports.pl
#
# Revision 2.5  1996/09/26 19:48:44  curt
# Fixed some problems with the newly revamped tab completion code.
#
# Revision 2.4  1996/07/30 14:35:33  curt
# Fixed a typo introduced in previous (field width change).
#
# Revision 2.3  1996/07/24 20:17:14  curt
# Added Arlindo M. L. Oliveira's "total" field with fix for handling higher
# numbers.
#
# Revision 2.2  1996/07/13 02:57:41  curt
# Version 0.65
# Packing Changes
# Documentation changes
# Changes to handle a value in both debit and credit fields.
#
# Revision 2.1  1996/02/27  05:35:40  curt
# Just stumbling around a bit with cvs ... :-(
#
# Revision 2.0  1996/02/27  04:41:53  curt
# Initial 2.0 revision.  (See "Log" files for old history.)
