# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
#####################################################
#
#  xCAT plugin package to handle various commands that work with the 
#     xCAT tables
#
#   Supported commands:
#         nodeadd
#         nodels
#         nodech
#         tabdump
#         tabrestore
#         noderm
#   To be implemented:
#         gettab
#         chtab
#         tabls
#         getnodecfg (?? this doesn't seem much different from gettab)
#         nr
#   These were xCAT 1.2 commands.  Are they still useful in xCAT 1.3?
#         addattr
#         delattr
#         chtype
#
#####################################################
package xCAT_plugin::tabutils;
use xCAT::Table;
use xCAT::Schema;
use Data::Dumper;
use xCAT::NodeRange;
use xCAT::Schema; #noderm will need to build list of tables..
#use Getopt::Long qw(GetOptionsFromArray);

1;

#some quick aliases to table/value
my %shortnames = (
  groups => [qw(nodelist groups)],
  tags => [qw(nodelist groups)],
  mgt => [qw(nodehm mgt)],
  switch => [qw(switch switch)],
);

#####################################################
# Return list of commands handled by this plugin
#####################################################
sub handled_commands {
  return {
    gettab => "tabutils",
    tabdump => "tabutils",
    tabrestore => "tabutils",
    tabch => "tabutils",
    nodech => "tabutils",
    nodeadd => "tabutils",
    noderm => "tabutils",
    tabls => "tabutils",
    nodels => "tabutils",
    getnodecfg => "tabutils",
    addattr => "tabutils",
    delattr => "tabutils",
    chtype => "tabutils",
    nr => "tabutils"
  }
}

my %usage = (
    nodech => "Usage: nodech <noderange> [table.column=value] [table.column=value] ...",
    nodeadd => "Usage: nodeadd <noderange> [table.column=value] [table.column=value] ...",
    noderm => "Usage: noderm <noderange>",
    tabdump => "Usage: tabdump <tablename>\n   where <tablename> is one of the following:\n     ".join("\n     ",keys %xCAT::Schema::tabspec),
    tabrestore => "Usage: tabrestore <tablename>.csv",
);

#####################################################
# Process the command
#####################################################
sub gettab {
    my $req = shift;
    my $callback = shift;
    my $keyspec = shift @{$req->{arg}};
    my @keypairs = split /,/,$keyspec;
    my %keyhash;
    foreach (@keypairs) {
        (my $key, my $value) = split /=/,$_;
        $keyhash{$key}=$value;
    }
    my %tabhash;
    foreach my $tabvalue (@{$req->{arg}}) {
        (my $table,my $column) = split /\./,$tabvalue;
        $tabhash{$table}->{$column} = 1;
    }
    foreach my $tabn (keys %tabhash) {
        my $tab = xCAT::Table->new($tabn);
        (my $ent) = $tab->getAttribs(\%keyhash,keys %{$tabhash{$tabn}});
        foreach my $coln (keys %{$tabhash{$tabn}}) {
            $callback->({data=>["$tabn.$coln:".$ent->{$coln}]});
        }
        $tab->close;
    }
}

sub process_request {
  use Getopt::Long;
  Getopt::Long::Configure("bundling");
  Getopt::Long::Configure("pass_through");
  
  my $request = shift;
  my $callback = shift;
  my $nodes = $request->{node};
  my $command = $request->{command}->[0];
  my $args = $request->{arg};
  unless ($args or $nodes or $request->{data}) {
      if ($usage{$command}) {
          $callback->({data=>[$usage{$command}]});
          return;
      }
  }

  if ($command eq "nodels") {
    return nodels($nodes,$args,$callback,$request->{noderange}->[0]);
  } elsif ($command eq "noderm" or $command eq "rmnode") {
    return noderm($nodes,$args,$callback);
  } elsif ($command eq "nodeadd" or $command eq "addnode") {
    return chnode($nodes,$args,$callback,1);
  } elsif ($command eq "nodech" or $command eq "chnode") {
    return chnode($nodes,$args,$callback,0);
  } elsif ($command eq "tabrestore") {
    return tabrestore($request,$callback);
  } elsif ($command eq "tabdump") {
    return tabdump($args,$callback);
  } elsif ($command eq "gettab") {
      return gettab($request,$callback);
  } else {
    print "$command not implemented yet\n";
    return (1,"$command not written yet");
  }

}


sub noderm {
  my $nodes = shift;
  my $args = shift;
  my $cb = shift;
  my @tablist = ("-d");
  foreach (keys %{xCAT::Schema::tabspec}) {
    if (grep /^node$/,@{$xCAT::Schema::tabspec{$_}->{cols}}) {
      push @tablist,$_; 
    }
  }
  chnode($nodes,\@tablist,$cb,0);
}
sub tabrestore {
#request->{data} is an array of CSV formatted lines
  my $request = shift;
  my $cb = shift;
  my $table = $request->{table}->[0];
  my $linenumber = 1;
  my $tab = xCAT::Table->new($table,-create =>1,-autocommit=>0);
  unless ($tab) {
    $cb->({error=>"Unable to open $table"});
    return;
  }
  $tab->delEntries(); #Yes, delete *all* entries
  my $header = shift @{$request->{data}};
  $header =~ s/"//g; #Strip " from overzealous CSV apps
  $header =~ s/^#//;
  $header =~ s/\s+$//;
  my @colns = split(/,/,$header);
  my $line;
  my $rollback=0;
  LINE: foreach $line (@{$request->{data}}) {
    $linenumber++;
    $line =~ s/\s+$//;
    my $origline = $line; #save for error reporting
    my %record;
    my $col;
    foreach $col (@colns) {
      if ($line =~ /^,/ or $line eq "") { #Match empty, or end of line that is empty
        #TODO: should we detect when there weren't enough CSV fields on a line to match colums? 
        $record{$col}=undef;
        $line =~ s/^,//;
      } elsif ($line =~ /^[^,]*"/) { # We have stuff in quotes... pain...
        #I don't know what I'm doing, so I'll do it a hard way....
        if ($line !~ /^"/) {
          $rollback = 1;
          $cb->({error=>"CSV missing opening \" for record with \" characters on line $linenumber, character ".index($origline,$line).": $origline"});
          next LINE;
        }
        my $offset=1;
        my $nextchar;
        my $ent = "";
        while (not $ent) {
          $offset = index($line,'"',$offset);
          $offset++;
          if ($offset <=0) {
            #MALFORMED CSV, request rollback, report an error
            $rollback=1;
            $cb->({error=>"CSV unmatched \" in record on line $linenumber, character ".index($origline,$line).": $origline"});
            next LINE;
          }
          $nextchar = substr($line,$offset,1);
          if ($nextchar eq '"') {
            $offset++;
          } elsif ($offset eq length($line) or $nextchar eq ',') {
            $ent = substr($line,0,$offset,'');
            $line =~ s/^,//;
            chop $ent;
            $ent = substr($ent,1);
            $ent =~ s/""/"/g;
            $record{$col}=$ent;
          } else {
            $cb->({error=>"CSV unescaped \" in record on line $linenumber, character ".index($origline,$line).": $origline"});
            $rollback=1;
            next LINE;
          }
        }
      } elsif ($line =~ /^([^,]+)/) { #easiest case, no Text::Balanced needed..
        $record{$col} = $1;
        $line =~ s/^([^,]+)(,|$)//;
      }
    }
    if ($line) {
      $rollback = 1;
      $cb->({error=>"Too many fields on line $linenumber: $origline | $line"});
      next LINE;
    }
    #TODO: check for error from DB and rollback
    my @rc =$tab->setAttribs(\%record,\%record);
    if (not defined($rc[0])) {
      $rollback = 1;
      $cb->({error=>"DB error ".$rc[1]. " with line $linenumber: ".$origline});
    }
  }
  if ($rollback) {
    $tab->rollback();
    $tab->close;
    undef $tab;
    return;
  } else {
    $tab->commit; #Made it all the way here, commit
  }
}
sub tabdump {
#TODO: need to return header for not-yet existing, but schemad tabs
#TODO: schema defined column order.
  my $args = shift;
  my $cb = shift;
  my $table="";
  foreach (@$args) {
    unless (/^-/) {
      if ($table) {
        return 1; #TODO: Error, usage
      }
      $table=$_;
    }
  }
  my $tabh = xCAT::Table->new($table);
  my %rsp;
  unless ($tabh) {
    if (defined($xCAT::Schema::tabspec{$table})) {
      my $header = join ",",@{$xCAT::Schema::tabspec{$table}->{cols}};
      $header="#".$header;
      push @{$rsp{data}},$header;
      $cb->(\%rsp);
      return;
    }
    $cb->({error=>"No such table: $table"});
    return 1;
  }
  my $recs=$tabh->getAllEntries();
  my $rec;
  my $firstline = 1;
  unless (@$recs) {
    if (defined($xCAT::Schema::tabspec{$table})) {
      my $header = join ",",@{$xCAT::Schema::tabspec{$table}->{cols}};
      $header="#".$header;
      push @{$rsp{data}},$header;
      $cb->(\%rsp);
      return;
    }
  }

  foreach $rec (@$recs) {
    my $line ='';
    if ($firstline) {
      $firstline = 0;
      $line = join ",",@{$tabh->{colnames}};
      $line =  "#".$line;
      push @{$rsp{data}},$line;
      $line = '';
    }
    foreach (@{$tabh->{colnames}}) {
      $rec->{$_} =~ s/"/""/g;
      if (defined $rec->{$_}) {
        $line = $line . '"' . $rec->{$_} . '",';
      } else {
        $line .= ',';
      }
    }
    $line =~ s/,$//;
    $line = $line . $lineappend;
    push @{$rsp{data}},$line;
  }
  $cb->(\%rsp);
}



sub chnode {
  my $nodes=shift;
  my $args=shift;
  my $callback=shift;
  my $addmode=shift;
  print $addmode;
  if ($addmode) {
    my $idx=0;
    foreach my $arg (@$args) {
      unless ($arg =~ /^-/) {
        $nodes = [noderange($arg,0)];
        splice(@$args,$idx,1);
        last;
      }
      $idx++;
    }
    unless ($nodes) {
      $callback->({error=>"No range to add detected\n"});
      return;
    }
  }
  my $column;
  my $value;
  my $temp;
  my %tables;
  my $tab;
  my $deletemode;

  #No GetOptionsFromArray... 
  #GetOptionsFromArray($args,"d|delete" => \$deletemode);
  #print Dumper($deletemode);
  foreach (@$args) {
    if (m/^-/) {  #A quick and dirty option parser in lieu of lacking Getoptinos
      if (m/^--/) {
        if (m/--delete/) {
          $deletemode=1;
          next;
        } else {
          $callback->({data=>["ERROR: Malformed argument $_ ignored"]});
          next;
        }
      } else {
        if (m/^-d$/) {
          $deletemode=1;
          next;
        } else {
          $callback->({data=>["ERROR: Malformed argument $_ ignored"]});
          next;
        }
      }
    }

    if ($deletemode) {
      if (m/[=\.]/) {
        $callback->({data=>["ERROR: . and = not valid in delete mode"]});
        next;
      }
      $tables{$_} = 1;
      next;
    }
    unless (m/=/) {
      $callback->({data=>["ERROR: Malformed argument $_ ignored"]});
      next;
    }
    ($temp,$value)=split('=',$_,2);
    my $op='=';
    if ($temp =~ /,$/) {
      $op=',=';
      chop($temp);
    } elsif ($temp =~ /\^$/) {
      $op='^=';
      chop($temp);
    }

    if ($shortnames{$temp}) {
      ($table,$column) = @{$shortnames{$temp}};
    } else {
      ($table,$column) = split('\.',$temp,2);
    }
    $tables{$table}->{$column}=[$value,$op];
  }
  foreach $tab (keys %tables) {
    my $tabhdl = xCAT::Table->new($tab,-create => 1,-autocommit => 0);
    if ($tabhdl) {
      foreach (@$nodes) {
        if ($deletemode) {
          $tabhdl->delEntries({'node'=>$_});
        } else {
          #$tabhdl->setNodeAttribs($_,$tables{$tab});
          my %uhsh;
          my $node = $_;
          foreach (keys %{$tables{$tab}}) {
            my $op = $tables{$tab}->{$_}->[1];
            my $val =  $tables{$tab}->{$_}->[0];
            my $key = $_;
            if ($op eq '=') {
              $uhsh{$key}=$val;
            } elsif ($op eq ',=') { #splice assignment
              my $cent = $tabhdl->getNodeAttribs($node,[$key]);
              my $curval;
              if ($cent) { $curval = $cent->{$key}; }
              if ($curval) { 
                my @vals = split(/,/,$curval);
                unless (grep /^$val$/,@vals) {
                  @vals=(@vals,$val);
                  my $newval = join(',',@vals);
                  $uhsh{$key}=$newval;
                }
              } else {
                $uhsh{$key}=$val;
              }
            } elsif ($op eq '^=') {
              my $cent = $tabhdl->getNodeAttribs($node,[$key]);
              my $curval;
              if ($cent) { $curval = $cent->{$key}; }
              if ($curval) { 
                my @vals = split(/,/,$curval);
                if (grep /^$val$/,@vals) { #only bother if there
                  @vals = grep(!/^$val$/,@vals);
                  my $newval = join(',',@vals);
                  $uhsh{$key}=$newval;
                }
              } #else, what they asked for is the case alredy


            }
          }
          if (keys %uhsh) {
            $tabhdl->setNodeAttribs($node,\%uhsh);
          }
        }
      }
      $tabhdl->commit;
    } else {
      $callback->({data=>["ERROR: Unable to open table $tab in configuration"]});
    }
  }
}


    

#####################################################
#  nodels command
#####################################################
sub nodels {
  my $nodes=shift;
  my $args=shift;
  my $callback=shift;
  my $noderange=shift;

  my $VERSION;
  my $HELP;

  sub nodels_usage {
     my %rsp;
     $rsp->{data}->[0]= "Usage:";
     $rsp->{data}->[1]= "  nodels [-?|-h|--help] ";
     $rsp->{data}->[2]= "  nodels [-v|--version] ";
     $rsp->{data}->[3]= "  nodels [noderange] ";
#####  xcat 1.2 nodels usage: 
#     $rsp->{data}->[1]= "  nodels [noderange] [group|pos|type|rg|install|hm|all]";
#     $rsp->{data}->[2]= " ";
#     $rsp->{data}->[3]= "  nodels [noderange] hm.{power|reset|cad|vitals|inv|cons}"; 
#     $rsp->{data}->[4]= "                     hm.{bioscons|eventlogs|getmacs|netboot}";
#     $rsp->{data}->[5]= "                     hm.{eth0|gcons|serialbios|beacon}";
#     $rsp->{data}->[6]= "                     hm.{bootseq|serialbps|all}";
#     $rsp->{data}->[7]= " ";
#     $rsp->{data}->[8]= "  nodels [noderange] rg.{tftp|nfs_install|install_dir|serial}";
#     $rsp->{data}->[9]= "                     rg.{usenis|install_roll|acct|gm|pbs}";
#     $rsp->{data}->[10]="                     rg.{access|gpfs|netdevice|prinic|all}";
     $callback->($rsp);
  }

  @ARGV=@{$args};
  if ( !GetOptions(
                    'h|?|help'   => \$HELP,
                    'v|version'  => \$VERSION,
                   ) ) {
        &nodels_usage;
  }

  # Help
  if ($HELP) { 
     &nodels_usage; 
     return; 
  }

  # Version
  if ($VERSION) { 
    my %rsp;
    $rsp->{data}->[0]= "1.3";
    $callback->($rsp);
    return;
  }

  # TODO -- Parse command arguments
#  my $opt;
#  my %attrs;
#  foreach $opt (@ARGV) {
#     if ($opt =~ /^group/) { 
#     }
#  }
  my $argc = @ARGV;


  if (@$nodes > 0 or $noderange) { #Make sure that there are zero nodes *and* that a noderange wasn't requested
  # TODO - gather data for each node
  #        for now just return the flattened list of nodes)
     my %rsp; #build up fewer requests, be less chatty
     if ($argc) {
       my %tables;
       foreach (@ARGV) {
         my $table;
         my $column;
         my $temp=$_;
         if ($shortnames{$temp}) {
           ($table,$column) = @{$shortnames{$temp}};
         } else {
           ($table,$column) = split('\.',$temp,2);
         }
         unless (grep /^$column$/,@{$tables{$table}}) {
          push @{$tables{$table}},[$column,$temp]; #Mark this as something to get
         }
       }
       my $tab;
       my %noderecs;
       foreach $tab (keys %tables) {
         my $tabh = xCAT::Table->new($tab);
         unless ($tabh) { next; }
         #print Dumper($tables{$tab});
         my $node;
         foreach $node (@$nodes) {
           my @cols;
           my %labels;
           foreach (@{$tables{$tab}}) {
             push @cols,$_->[0];
             $labels{$_->[0]}=$_->[1];
           }
           my $rec=$tabh->getNodeAttribs($node,\@cols);
           foreach (keys %$rec) {
             my %datseg;
             $datseg{data}->[0]->{desc} = [$labels{$_}];
             $datseg{data}->[0]->{contents} = [$rec->{$_}];
             $datseg{name} = [$node]; #{}->{contents} = [$rec->{$_}];
             push @{$noderecs{$node}},\%datseg;
           }
         }
         #$rsp->{node}->[0]->{data}->[0]->{desc}->[0] = $_;
         #$rsp->{node}->[0]->{data}->[0]->{contents}->[0] = $_;
         $tabh->close();
         undef $tabh;
       }
       foreach (sort (keys %noderecs)) {
         push @{$rsp->{"node"}},@{$noderecs{$_}};
       }
    } else { 
      foreach (@$nodes) {
        my $noderec;
        $noderec->{name}->[0]=($_);
        push @{$rsp->{node}},$noderec;
      }
    }
    $callback->($rsp);
  } else {
  # no noderange specified on command line, return list of all nodes
     my $nodelisttab;
     if ($nodelisttab=xCAT::Table->new("nodelist")) {
        my @attribs=("node");
        my @ents=$nodelisttab->getAllAttribs(@attribs);
        foreach (@ents) {
           my %rsp;
           if ($_->{node}) {
              $rsp->{node}->[0]->{name}->[0]=($_->{node});
#              $rsp->{node}->[0]->{data}->[0]->{contents}->[0]="$_->{node} node contents";
#              $rsp->{node}->[0]->{data}->[0]->{desc}->[0]="$_->{node} node desc";
              $callback->($rsp);
           }
        }
     }
  }

  return 0;
}