package A2B::Table;

use TSV::Reader;
use TSV::Writer;

use strict;
use vars qw($WARN_NO_INDEX);

$WARN_NO_INDEX = 1;

my $class_gen = 0;

sub new {
	my $package = shift;
	my %args = @_;
	my $self = bless {
		field_names => [],
		fields => {},	# field name -> numerical index
		rows => [],
		indexes => [],
		index_index => {},
		name => $package,
	}, $package;

	if (my $file = delete $args{file}) {
		$self->read_tsv($file);
	} elsif (my $fields = delete $args{fields}) {
		$self->_fields($fields);
	}

	%args and die "invalid args @{[%args]}";
	
	return $self;
}

use overload '""' => \&name;

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

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

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

sub _fields {
	my $self = shift;
	my $fields = shift;
	$self->{field_names} = $fields;
	for (0..$#$fields) {
		$self->{fields}{$fields->[$_]} = $_;
	}
	my $package = ref $self;
	no strict 'refs';
	my $element_type = ${"${package}::ELEMENT_TYPE"} || "A2B::Table::Element";
	use strict 'refs';
	my $element_type_real = $element_type."::".$class_gen++;

	$self->{element_type} = $element_type_real;
	
	eval qq{
		package $element_type_real;
		
		use strict;
		use vars qw(\@ISA \$TABLE \$FIELDS);

		\@ISA = qw($element_type);

		\$TABLE = \$self;
		\$FIELDS = \$self->{fields};
	} . join "", map qq{
		sub $_ {
			my \$self = shift;
			return \$self->[$self->{fields}{$_}];
		}
	}, @$fields;
	die "internal error" if $@;
}

sub read_tsv {
	my $self = shift;
	my $file = shift;
	($self->{name} = $file) =~ s|.*/||;
	my $reader = TSV::Reader->new($file);
	my $fields = $reader->read or
		die "supposed tsv file is empty";
	$self->_fields($fields);
	while (defined (my $row = $reader->read)) {
		$self->add_row($row);
	}
}

sub write_tsv {
	my $self = shift;
	my $file = shift;

	my $writer = TSV::Writer->new($file);
	$writer->write($self->{field_names});
	for my $row (@{$self->{rows}}) {
		$writer->write($row);
	}
	$writer->close;
}

sub add_row {
	my $self = shift;
	my $package = ref $self;
	my $row = shift;

	bless $row, $self->{element_type};

	push @{$self->{rows}}, $row;
	for my $index (@{$self->{indexes}}) {
		$index->add_row($row);
	}
}

sub _add_index {
	my $self = shift;
	my $index = shift;
	push @{$self->{indexes}}, $index;
	for my $row (@{$self->{rows}}) {
		$index->add_row($row);
	}
	my $index_name = $index->name;
	do {
		my $sorted_index_name = join '-', sort split /-/, $index_name;
		$self->{index_index}{$sorted_index_name} = $index;
	} while $index_name =~ s/-[^-]+$// && !exists $self->{index_index}{$index_name};
}

sub index {
	my $self = shift;
	my @fields = sort @_;
	my $index_name = join '-', @fields;
	return $self->{index_index}{$index_name};
}

sub values {
	my $self = shift;

	my $field = shift;
	if (my $index = $self->index($field)) {
		return $index->keys(1);
	}

	my %values;
	
	warn "warning - no index for $field" if $WARN_NO_INDEX;

	defined (my $field_i = $self->{fields}{$field}) or
		die "bad field `$field'";

	for my $row (@{$self->{rows}}) {
		$values{$row->[$field_i]} = undef;
	}
	
	return [keys %values];
}

sub query {
	my $self = shift;
	my %args = @_;
	my @field_names = sort keys %args;

	if (my $index = $self->index(@field_names)) {
		# hooray, we can use an index
		my @row; $row[$self->{fields}{$_}] = $args{$_} for keys %args;
		return $index->query(\@row);
	}
	
	# bummer, no index
	# TODO automatically make needed indexes?
	# TODO use partial indexes?

	warn "warning - no index for [@field_names]" if $WARN_NO_INDEX;

	# do an exhaustive search

	my $looking_for = join "\0", CORE::values %args;
	my @fields = @{$self->{fields}}{keys %args};
	
	my @list;
	for my $row (@{$self->{rows}}) {
		if ($looking_for eq join "\0", @$row[@fields]) {
			push @list, $row;
		}
	}
	return \@list;
}

sub bind_foreign_key {
	my ($self, $field, $foreign_table, $foreign_key) = @_;
	my $field_i = $self->{fields}{$field};

	for my $row (@{$self->{rows}}) {
		my $value = $row->[$field_i];
		my $results = $foreign_table->query($foreign_key => $value);
		my $n = @$results;
#		$n > 1 and die "bind_foreign_key: `$foreign_key' is not a unique key to `$foreign_table': $value occurs $n times";
		$n == 0 and die "bind_foreign_key: value `$value' does not occur in `$foreign_table:$foreign_key'";
		$row->[$field_i] = $results->[0];
	}
}

1
