package WebPAC::Input;

use warnings;
use strict;

use blib;

use WebPAC::Common;
use base qw/WebPAC::Common/;
use Text::Iconv;
use Data::Dumper;

=head1 NAME

WebPAC::Input - read different file formats into WebPAC

=head1 VERSION

Version 0.05

=cut

our $VERSION = '0.05';

=head1 SYNOPSIS

This module implements input as database which have fixed and known
I<size> while indexing and single unique numeric identifier for database
position ranging from 1 to I<size>.

Simply, something that is indexed by unmber from 1 .. I<size>.

Examples of such databases are CDS/ISIS files, MARC files, lines in
text file, and so on.

Specific file formats are implemented using low-level interface modules,
located in C<WebPAC::Input::*> namespace which export C<open_db>,
C<fetch_rec> and optional C<init> functions.

Perhaps a little code snippet.

    use WebPAC::Input;

    my $db = WebPAC::Input->new(
    	module => 'WebPAC::Input::ISIS',
		config => $config,
		lookup => $lookup_obj,
		low_mem => 1,
    );

    $db->open('/path/to/database');
	print "database size: ",$db->size,"\n";
	while (my $rec = $db->fetch) {
		# do something with $rec
	}



=head1 FUNCTIONS

=head2 new

Create new input database object.

  my $db = new WebPAC::Input(
	module => 'WebPAC::Input::MARC',
	code_page => 'ISO-8859-2',
	low_mem => 1,
	recode => 'char pairs',
	no_progress_bar => 1,
  );

C<module> is low-level file format module. See L<WebPAC::Input::Isis> and
L<WebPAC::Input::MARC>.

Optional parametar C<code_page> specify application code page (which will be
used internally). This should probably be your terminal encoding, and by
default, it C<ISO-8859-2>.

Default is not to use C<low_mem> options (see L<MEMORY USAGE> below).

C<recode> is optional string constisting of character or words pairs that
should be replaced in input stream.

C<no_progress_bar> disables progress bar output on C<STDOUT>

This function will also call low-level C<init> if it exists with same
parametars.

=cut

sub new {
	my $class = shift;
	my $self = {@_};
	bless($self, $class);

	my $log = $self->_get_logger;

	$log->logconfess("specify low-level file format module") unless ($self->{module});
	my $module = $self->{module};
	$module =~ s#::#/#g;
	$module .= '.pm';
	$log->debug("require low-level module $self->{module} from $module");

	require $module;
	#eval $self->{module} .'->import';

	# check if required subclasses are implemented
	foreach my $subclass (qw/open_db fetch_rec init/) {
		my $n = $self->{module} . '::' . $subclass;
		if (! defined &{ $n }) {
			my $missing = "missing $subclass in $self->{module}";
			$self->{$subclass} = sub { $log->logwarn($missing) };
		} else {
			$self->{$subclass} = \&{ $n };
		}
	}

	if ($self->{init}) {
		$log->debug("calling init");
		$self->{init}->($self, @_);
	}

	$self->{'code_page'} ||= 'ISO-8859-2';

	# running with low_mem flag? well, use DBM::Deep then.
	if ($self->{'low_mem'}) {
		$log->info("running with low_mem which impacts performance (<32 Mb memory usage)");

		my $db_file = "data.db";

		if (-e $db_file) {
			unlink $db_file or $log->logdie("can't remove '$db_file' from last run");
			$log->debug("removed '$db_file' from last run");
		}

		require DBM::Deep;

		my $db = new DBM::Deep $db_file;

		$log->logdie("DBM::Deep error: $!") unless ($db);

		if ($db->error()) {
			$log->logdie("can't open '$db_file' under low_mem: ",$db->error());
		} else {
			$log->debug("using file '$db_file' for DBM::Deep");
		}

		$self->{'db'} = $db;
	}

	$self ? return $self : return undef;
}

=head2 open

This function will read whole database in memory and produce lookups.

 $input->open(
 	path => '/path/to/database/file',
	code_page => '852',
	limit => 500,
	offset => 6000,
	lookup => $lookup_obj,
	stats => 1,
 );

By default, C<code_page> is assumed to be C<852>.

C<offset> is optional parametar to position at some offset before reading from database.

C<limit> is optional parametar to read just C<limit> records from database

C<stats> create optional report about usage of fields and subfields

Returns size of database, regardless of C<offset> and C<limit>
parametars, see also C<size>.

=cut

sub open {
	my $self = shift;
	my $arg = {@_};

	my $log = $self->_get_logger();

	$log->logcroak("need path") if (! $arg->{'path'});
	my $code_page = $arg->{'code_page'} || '852';

	# store data in object
	$self->{'input_code_page'} = $code_page;
	foreach my $v (qw/path offset limit/) {
		$self->{$v} = $arg->{$v} if ($arg->{$v});
	}

	# create Text::Iconv object
	$self->{iconv} = Text::Iconv->new($code_page,$self->{'code_page'});

	my $filter_ref;

	if ($self->{recode}) {
		my @r = split(/\s/, $self->{recode});
		if ($#r % 2 != 1) {
			$log->logwarn("recode needs even number of elements (some number of valid pairs)");
		} else {
			my $recode;
			while (@r) {
				my $from = shift @r;
				my $to = shift @r;
				$recode->{$from} = $to;
			}

			my $regex = join '|' => keys %{ $recode };

			$log->debug("using recode regex: $regex");
			
			$filter_ref = sub {
				my $t = shift;
				$t =~ s/($regex)/$recode->{$1}/g;
				return $t;
			};

		}

	}

	my ($db, $size) = $self->{open_db}->( $self, 
		path => $arg->{path},
		filter => $filter_ref,
		%{ $arg },
	);

	unless (defined($db)) {
		$log->logwarn("can't open database $arg->{path}, skipping...");
		return;
	}

	unless ($size) {
		$log->logwarn("no records in database $arg->{path}, skipping...");
		return;
	}

	my $from_rec = 1;
	my $to_rec = $size;

	if (my $s = $self->{offset}) {
		$log->debug("skipping to MFN $s");
		$from_rec = $s;
	} else {
		$self->{offset} = $from_rec;
	}

	if ($self->{limit}) {
		$log->debug("limiting to ",$self->{limit}," records");
		$to_rec = $from_rec + $self->{limit} - 1;
		$to_rec = $size if ($to_rec > $size);
	}

	# store size for later
	$self->{size} = ($to_rec - $from_rec) ? ($to_rec - $from_rec + 1) : 0;

	$log->info("processing $self->{size}/$size records [$from_rec-$to_rec] convert $code_page -> $self->{code_page}", $self->{stats} ? ' [stats]' : '');

	# read database
	for (my $pos = $from_rec; $pos <= $to_rec; $pos++) {

		$log->debug("position: $pos\n");

		my $rec = $self->{fetch_rec}->($self, $db, $pos );

		$log->debug(sub { Dumper($rec) });

		if (! $rec) {
			$log->warn("record $pos empty? skipping...");
			next;
		}

		# store
		if ($self->{low_mem}) {
			$self->{db}->put($pos, $rec);
		} else {
			$self->{data}->{$pos} = $rec;
		}

		# create lookup
		$self->{'lookup'}->add( $rec ) if ($rec && $self->{'lookup'});

		# update counters for statistics
		if ($self->{stats}) {
			map {
				my $fld = $_;
				$self->{_stats}->{fld}->{ $fld }++;
				if (ref($rec->{ $fld }) eq 'ARRAY') {
					map {
						if (ref($_) eq 'HASH') {
							map {
								$self->{_stats}->{sf}->{ $fld }->{ $_ }++;
							} keys %{ $_ };
						} else {
							$self->{_stats}->{repeatable}->{ $fld }++;
						}
					} @{ $rec->{$fld} };
				}
			} keys %{ $rec };
		}

		$self->progress_bar($pos,$to_rec) unless ($self->{no_progress_bar});

	}

	$self->{pos} = -1;
	$self->{last_pcnt} = 0;

	# store max mfn and return it.
	$self->{max_pos} = $to_rec;
	$log->debug("max_pos: $to_rec");

	return $size;
}

=head2 fetch

Fetch next record from database. It will also displays progress bar.

 my $rec = $isis->fetch;

Record from this function should probably go to C<data_structure> for
normalisation.

=cut

sub fetch {
	my $self = shift;

	my $log = $self->_get_logger();

	$log->logconfess("it seems that you didn't load database!") unless ($self->{pos});

	if ($self->{pos} == -1) {
		$self->{pos} = $self->{offset};
	} else {
		$self->{pos}++;
	}

	my $mfn = $self->{pos};

	if ($mfn > $self->{max_pos}) {
		$self->{pos} = $self->{max_pos};
		$log->debug("at EOF");
		return;
	}

	$self->progress_bar($mfn,$self->{max_pos}) unless ($self->{no_progress_bar});

	my $rec;

	if ($self->{low_mem}) {
		$rec = $self->{db}->get($mfn);
	} else {
		$rec = $self->{data}->{$mfn};
	}

	$rec ||= 0E0;
}

=head2 pos

Returns current record number (MFN).

 print $isis->pos;

First record in database has position 1.

=cut

sub pos {
	my $self = shift;
	return $self->{pos};
}


=head2 size

Returns number of records in database

 print $isis->size;

Result from this function can be used to loop through all records

 foreach my $mfn ( 1 ... $isis->size ) { ... }

because it takes into account C<offset> and C<limit>.

=cut

sub size {
	my $self = shift;
	return $self->{size};
}

=head2 seek

Seek to specified MFN in file.

 $isis->seek(42);

First record in database has position 1.

=cut

sub seek {
	my $self = shift;
	my $pos = shift || return;

	my $log = $self->_get_logger();

	if ($pos < 1) {
		$log->warn("seek before first record");
		$pos = 1;
	} elsif ($pos > $self->{max_pos}) {
		$log->warn("seek beyond last record");
		$pos = $self->{max_pos};
	}

	return $self->{pos} = (($pos - 1) || -1);
}

=head2 stats

Dump statistics about field and subfield usage

  print $input->stats;

=cut

sub stats {
	my $self = shift;

	my $log = $self->_get_logger();

	my $s = $self->{_stats};
	if (! $s) {
		$log->warn("called stats, but there is no statistics collected");
		return;
	}

	my $max_fld = 0;

	my $out = join("\n",
		map {
			my $f = $_ || die "no field";
			my $v = $s->{fld}->{$f} || die "no s->{fld}->{$f}";
			$max_fld = $v if ($v > $max_fld);

			my $o = sprintf("%4s %d ~", $f, $v);

			if (defined($s->{sf}->{$f})) {
				map {
					$o .= sprintf(" %s:%d", $_, $s->{sf}->{$f}->{$_});
				} sort keys %{ $s->{sf}->{$f} };
			}

			if (my $v_r = $s->{repeatable}->{$f}) {
				$o .= " ($v_r)" if ($v_r != $v);
			}

			$o;
		} sort { $a cmp $b } keys %{ $s->{fld} }
	);

	$log->debug( sub { Dumper($s) } );

	return $out;
}

=head1 MEMORY USAGE

C<low_mem> options is double-edged sword. If enabled, WebPAC
will run on memory constraint machines (which doesn't have enough
physical RAM to create memory structure for whole source database).

If your machine has 512Mb or more of RAM and database is around 10000 records,
memory shouldn't be an issue. If you don't have enough physical RAM, you
might consider using virtual memory (if your operating system is handling it
well, like on FreeBSD or Linux) instead of dropping to L<DBM::Deep> to handle
parsed structure of ISIS database (this is what C<low_mem> option does).

Hitting swap at end of reading source database is probably o.k. However,
hitting swap before 90% will dramatically decrease performance and you will
be better off with C<low_mem> and using rest of availble memory for
operating system disk cache (Linux is particuallary good about this).
However, every access to database record will require disk access, so
generation phase will be slower 10-100 times.

Parsed structures are essential - you just have option to trade RAM memory
(which is fast) for disk space (which is slow). Be sure to have planty of
disk space if you are using C<low_mem> and thus L<DBM::Deep>.

However, when WebPAC is running on desktop machines (or laptops :-), it's
highly undesireable for system to start swapping. Using C<low_mem> option can
reduce WecPAC memory usage to around 64Mb for same database with lookup
fields and sorted indexes which stay in RAM. Performance will suffer, but
memory usage will really be minimal. It might be also more confortable to
run WebPAC reniced on those machines.


=head1 AUTHOR

Dobrica Pavlinusic, C<< <dpavlin@rot13.org> >>

=head1 COPYRIGHT & LICENSE

Copyright 2005 Dobrica Pavlinusic, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut

1; # End of WebPAC::Input
