#########################################################################
package AnyData::Storage::SNMP;
#########################################################################

## XXX: TODO:
##   scalar sets?
##   multi-hosts

$AnyData::Storage::VERSION = '5.2.1';
use strict;

use vars qw(@basecols);

@AnyData::Storage::SNMP::basecols = qw(hostname iid);
$AnyData::Storage::SNMP::iidptr = 1; # must match array column of above
@AnyData::Storage::SNMP::basetypes = qw(OCTETSTR OBJECTID);
$AnyData::Storage::SNMP::debug = 0;
$AnyData::Storage::SNMP::debugre = undef;

use Data::Dumper;
use AnyData::Storage::File;
use SNMP;
SNMP::init_snmp("AnyData::SNMP");

sub new {
#    DEBUG("calling AnyData::Storage::SNMP new\n");
#    DEBUG("new storage: ",Dumper(\@_),"\n");
    my $class = shift;
    my $self  = shift || {};
    $self->{open_mode} = 'c';
    return bless $self, $class;
}

sub open_table {
    DEBUG("calling AnyData::Storage::SNMP open_table\n");
    my ($self, $parser, $table, $mode, $tname) = @_;
    $self->{'process_table'} = $tname;
    DEBUG("open_table: ",Dumper(\@_),"\n");
}

sub get_col_names {
    DEBUG("calling AnyData::Storage::SNMP get_col_names\n");
    my ($self, $parser, $tname) = @_;
    DEBUG("get_col_names\n",Dumper(\@_),"\n");
    $tname = $self->{'process_table'} if (!$tname);

    # get cached previous results
    return $self->{col_names}{$tname} if (defined($self->{col_names}{$tname}));

    # table name
    $tname = $self->{'process_table'} if (!$tname);

    # mib node setup
    my $mib = $SNMP::MIB{$tname} || return warn "no such table $tname";
    my $entry = $mib->{'children'}[0];

    # base columns and types
    my @cols = @AnyData::Storage::SNMP::basecols;
    my @types = @AnyData::Storage::SNMP::basetypes;
    my %donecol;
    my $count = $#cols;

    foreach my $index (@{$entry->{'indexes'}}) {
	push @cols, $index;
	push @types, $SNMP::MIB{$index}{type};
	$donecol{$index} = 1;
	$count++;
	if ($SNMP::MIB{$index}{parent}{label} eq $entry->{label}) {
	    # this index is a member of this table
	    $self->{columnmap}[$count] = $index;
	    $self->{coloffset} += 1;
	}
    }

    # search children list
    foreach my $child  ( sort { $a->{'subID'} <=> $b->{'subID'} } @{$entry->{'children'}}) {
	push @{$self->{real_cols}}, $child->{'label'};
	next if ($donecol{$child->{label}});
	push @cols, $child->{'label'};
	push @types, $child->{'type'};
	$count++;
	$self->{columnmap}[$count] = $child->{'label'};
    }

    # save for later.
    $parser->{col_names} = \@cols;
    $self->{col_types} = \@types;
    $self->{col_names} = \@cols;
    map { $self->{col_uc_map}{uc($_)} = $_ } @cols;
    return \@cols;
}

sub set_col_nums {
    DEBUG("calling AnyData::Storage::SNMP set_col_nums\n");
    DEBUG("set_col_nums\n",Dumper(\@_),"\n");
    my ($self, $tname) = @_;
    return $self->{col_nums} if (defined($self->{col_nums}));
    my $mib = $SNMP::MIB{$tname};
    my $entry = $mib->{'children'}[0];
    my (%cols, %mibnodes);
    my $cnt = -1;

    foreach my $i (@{$self->{col_names}}) {
	$cols{$i} = ++$cnt;
    }

    $self->{col_nums} = \%cols;
    return \%cols;
}

# not needed?
sub get_file_handle {
    DEBUG("calling AnyData::Storage::SNMP get_file_handle\n");
    DEBUG("get_file_handle\n",Dumper(\@_),"\n");
    return shift;
}

# not needed?
sub get_file_name {
    DEBUG("calling AnyData::Storage::SNMP get_file_name\n");
    DEBUG("get_file_name\n",Dumper(\@_),"\n");
    my $self = shift;
    return $self->{process_table} || $self->{table_name};
}

sub truncate {
    DEBUG("calling AnyData::Storage::SNMP truncate\n");
    my ($self) = @_;
    DEBUG("trunacte, ", Dumper(@_));

    # We must know how to delete rows or else this is all pointless.
#     return $self->{col_nums}{$tname} if (defined($self->{col_nums}{$tname}));
    my $tablemib = $SNMP::MIB{$self->{process_table}};
    my $entrymib = $tablemib->{'children'}[0];
    my ($delcolumn, $delcolumnname, $delcolumnvalue);

    foreach my $child  (@{$entrymib->{'children'}}) {
	if ($child->{'textualConvention'} eq 'RowStatus') {
	    $delcolumn = $child->{subID};
	    $delcolumnname = $child->{label};
	    $delcolumnvalue = 6; # destroy
	    last;
	}
    }
    if (!$delcolumn) {
	return warn "Can't (or don't know how to) delete from table $self->{process_table}.  Failing.\n";
    }

    # we should have a session, or else something is really wierd but...
    $self->{'sess'} = $self->make_session() if (!$self->{'sess'});

    # for each key left in our cache, delete it
    foreach my $key (keys(%{$self->{'existtest'}})) {
	# xxx: fullyqualified oid better
	my $vblist = new SNMP::VarList([$delcolumnname, $key,
					$delcolumnvalue]);
	DEBUG("truncate $key: \n", Dumper($vblist));
	$self->{'sess'}->set($vblist) || warn $self->{'sess'}->{ErrorStr};
    }
    return;
}

sub make_session {
    DEBUG("calling AnyData::Storage::SNMP make_session\n");
    my $self = shift;
    my @args = @_;
    my @sessions;
    foreach my $key (qw(SecName Version SecLevel AuthPass Community RemotePort Timeout Retries RetryNoSuch SecEngineId ContextEngineId Context AuthProto PrivProto PrivPass)) {
	push @args, $key, $self->{$key} if ($self->{$key});
    }
    foreach my $host (split(/,\s*/,$self->{DestHost})) {
	push @sessions, new SNMP::Session(@args, 'DestHost' => $host);
    }
    return \@sessions;
}

sub file2str {
    DEBUG("calling AnyData::Storage::SNMP file2str\n");
    my ($self, $parser, $uccols) = @_;
    my ($cols, @retcols);
    DEBUG("file2str\n",Dumper(\@_),"\n");
  restart:
    if (!$self->{lastnode}) {
#	my @vbstuff = @{$parser->{'col_names'}};
#	splice (@vbstuff,0,1+$#AnyData::Storage::SNMP::basecols);
#	map { $_ = [ $_ ] } @vbstuff;
#	$self->{lastnode} = new SNMP::VarList(@vbstuff);
#	splice (@$cols,0,1+$#AnyData::Storage::SNMP::basecols);
	map { push @$cols,$self->{col_uc_map}{$_} } @$uccols;
	if ($#$cols == -1) {
	    $cols = $self->{'col_names'};
	    # remove base columns
	    splice (@$cols,0,1+$#AnyData::Storage::SNMP::basecols);
	    # remove not accessible columns
	    foreach my $col (@$cols) {
		my $mib = $SNMP::MIB{$col};
		push @retcols, $col if ($mib->{'access'} =~ /Read|Create/);
	    }
	} else {
	    @retcols = @$cols;
	    # remove base columns
	    foreach my $c (@AnyData::Storage::SNMP::basecols) {
		@retcols = grep(!/^$c$/, @retcols);
	    }
	    # remove non-accessible columns
	    @retcols = grep {$SNMP::MIB{$_}{'access'} =~ /Read|Create/} @retcols;
	}
	map { $_ = [ $_ ] } @retcols;
	$self->{lastnode} = new SNMP::VarList(@retcols);
    }

    if (!$self->{'sess'}) {
	$self->{'sess'} = $self->make_session();
	if ($#{$self->{'sess'}} == 0 && $self->{'silence_single_host'}) {
	    $self->{'hostname'} = '';
	} else {
	    $self->{'hostname'} = $self->{'sess'}[0]{'DestHost'};
	}
    }

    # perform SNMP operation
    my $lastnode = $self->{'lastnode'}[0][0];
    my $result;
    $result = $self->{'sess'}[0]->getnext($self->{lastnode});
    if (!defined($result)) {
	warn " getnext of $self->{lastnode}[0][0] returned undef\n";
    }
    DEBUG(" result: ",Dumper($self->{lastnode}),"\n");

    # XXX: check for holes!

    # need proper oid compare here for all nodes
    if ($self->{'lastnode'}[0][0] ne $lastnode) {
	if ($#{$self->{'sess'}} > 0) {
	    shift @{$self->{'sess'}};
	    delete($self->{'lastnode'});
	    @$cols = ();
	    $self->{'hostname'} = $self->{'sess'}[0]{'DestHost'} if($self->{'hostname'});
	    goto restart;
	}
	return undef;
    }
    
    # add in basecols information:
    my @ret = ($self->{'hostname'}, $self->{'lastnode'}[0][1]);
    DEBUG("Dump row results: ",Dumper($self->{'lastnode'}),"\n");

    # build result array from result varbind contents
    map { $ret[$self->{'col_nums'}{$_->[0]}] = map_data($_); } @{$self->{'lastnode'}};

    # store instance ID for later use if deletion is needed later.
    $self->{'existtest'}{$self->{'lastnode'}[0][1]} = 1;

    DEBUG("Dump row results2: ",Dumper(\@ret),"\n");
    return \@ret;
}

sub map_data {
    if ($_->[3] eq "OBJECTID") {
	$_->[2] = pretty_print_oid(@_);
    }
    return $_->[2];
}

sub pretty_print_oid {
    use NetSNMP::default_store qw(:all);
    netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, 
			   NETSNMP_DS_LIB_DONT_BREAKDOWN_OIDS,0);
    my $new = SNMP::translateObj($_->[2], 0);
    netsnmp_ds_set_boolean(NETSNMP_DS_LIBRARY_ID, 
			   NETSNMP_DS_LIB_DONT_BREAKDOWN_OIDS,1);
    my $new2 = SNMP::translateObj($_->[2], 0);
    if ($new) {
	$_->[2] = $new2 . "$new";
    } else {
	$_->[2] = $_->[0] . $_->[1];
    }
    return $_->[2];
}

sub push_row {
    DEBUG("calling AnyData::Storage::SNMP push_row\n");
    DEBUG("push_row: ",Dumper(\@_),"\n");
    DEBUG("push_row\n");
    my ($self, $values, $parser, $cols) = @_;
    my @callers = caller(3);
    my $mode = $callers[3];
    if ($mode =~ /DELETE/) {
	DEBUG("not deleting $values->[$AnyData::Storage::SNMP::iidptr]\n");
	delete $self->{'existtest'}{$values->[$AnyData::Storage::SNMP::iidptr]};
	return;
    }

    my @origvars;
    if ($#$cols == -1) {
	# no column info passed in.  Update everything (mode probably INSERTS).
#	@origvars = @{$self->{'col_names'}}};
#	splice (@origvars,0,1+$#AnyData::Storage::SNMP::basecols);
	
	map { push @origvars, $_ if $SNMP::MIB{$_}{'access'} =~ /Write|Create/; } @{$self->{'real_cols'}} ;

	DEBUG("set cols: ", Dumper(\@origvars));
    } else {
	# only update the columns in question.  (mode probably UPDATE)
	map { push @origvars, $self->{col_uc_map}{$_} } @$cols;
    }

    my @vars;
    foreach my $var (@origvars) {
	my $access = $SNMP::MIB{$var}{'access'};
	# not in this table, probably (hopefully) an index from another:
	next if ($SNMP::MIB{$var}{'parent'}{'parent'}{'label'} ne 
		 $self->{process_table});
	DEBUG("$var -> $access\n");
	if ($access =~ /(Write|Create)/) {
	    push @vars, $var;
	} elsif ($mode eq 'insert') {
	    DEBUG("XXX: error if not index\n");
	} elsif ($mode eq 'update') {
	    DEBUG("update to non-writable column attempted (SNMP error coming)\n");	
	}
    }

    # generate index OID component if we don't have it.
    if ($values->[$AnyData::Storage::SNMP::iidptr] eq '') {
	$values->[$AnyData::Storage::SNMP::iidptr] = 
	    $self->make_iid($self->{process_table}, $values);
    }

    # add in values to varbind columns passed in from incoming parameters
    my @newvars;
    foreach my $v (@vars) {
	my $num = $self->{'col_nums'}{$v};
	DEBUG("types: $v -> $num -> ", $self->{'col_types'}[$num], 
	      " -> val=", $values->[$num], "\n");
	next if (!defined($values->[$num]));
	# build varbind: column-oid, instance-id, value type, value
	push @newvars, [$v, $values->[1], $values->[$num],
			$self->{'col_types'}[$num]];
    };

    # create the varbindlist
#    print STDERR Dumper(\@newvars);
    my $vblist = new SNMP::VarList(@newvars);

#    print STDERR Dumper($vblist);
    DEBUG("set: ", Dumper($vblist));
    $self->{'sess'} = $self->make_session() if (!$self->{'sess'});
    if (!$self->{'sess'}[0]) {
	warn "couldn't create SNMP session";
    } elsif (!$self->{'sess'}[0]->set($vblist)) {
	my $err = "$self->{process_table}: " . $self->{'sess'}[0]->{ErrorStr};
	if ($self->{'sess'}[0]->{ErrorInd}) {
	    $err = $err . " (at varbind #" 
		. $self->{'sess'}[0]->{ErrorInd}  . " = " ;
	    my $dump = Data::Dumper->new([$vblist->[$self->{'sess'}[0]->{ErrorInd} -1]]);
	    $err .= $dump->Indent(0)->Terse(1)->Dump;
	}
	warn $err;
    }
}

sub seek {
    DEBUG("calling AnyData::Storage::SNMP seek\n");
    my ($self, $parser) = @_;
    DEBUG("seek\n",Dumper(\@_),"\n");
}

sub make_iid {
    DEBUG("calling AnyData::Storage::SNMP make_iid\n");
    my ($self, $tname, $vals) = @_;
    
    # Get indexes
    my $mib = $SNMP::MIB{$tname};
    my $entry = $mib->{'children'}[0];
    my $indexes = $entry->{'indexes'};
    my $iid;

    # XXX: implied

#    print STDERR "INDEXES: ", Dumper($vals),"\n";
    foreach my $index (@$indexes) {
	warn "A null index value was found, which I doubt is correct." if (!defined($vals->[$self->{col_nums}{$index}]));
	my $val = $vals->[$self->{col_nums}{$index}];
	my $type = $SNMP::MIB{$index}->{'type'};
	DEBUG("index type: $index -> $type -> $val -> " . length($val) . "\n");
	if ($type eq "OCTETSTR") {
	    $iid .= "." . length($val) . "." . join(".", unpack("c*", $val));
	} elsif ($type eq "OBJID") {
	    $iid .= "." . (scalar grep(/\./,$val) + 1) . "." . $val;
	} else {
	    # should be only an INTEGER?
	    $iid .= "." . $val;
	}
    }
    DEBUG("made iid: $iid\n");
    return $iid;
}

sub DEBUG {
    my @info = caller(1);
    if ($AnyData::Storage::SNMP::debug
	|| ($AnyData::Storage::SNMP::debugre &&
	    $_[0] =~ /$AnyData::Storage::SNMP::debugre/)) {
	DEBUGIT(\@info, @_);
    }
}

sub DEBUGIT {
    my $info;
    if (ref($_[0]) eq 'ARRAY') {
	$info = shift @_;
    } else {
	my @y;
	my $c=0;
	print STDERR "debug chain: ";
	for(@y = caller($c); $#y > -1; $c++, @y = caller($c)) {
	    print STDERR "  $c: $y[3]\n";
	}
	my @x = caller(1);
	$info = \@x;
    }
    print STDERR "$info->[3]: ";
    print STDERR @_;
}

1;
