The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
########################################
# 030.tree ----- translate. many kinds of tree structures
#   arity      - fanout, eg, 2 for binary
#   link_type  - starlike   idtypes connect all nodes at each level
#                chainlike  each idtype connects parent to one child
#   db_type    - as usual   staggered or binary data pattern
#   skip_pairs - don't do all pairs
########################################
use t::lib;
use t::utilBabel;
use Carp;
use Test::More;
use Test::Deep;
use File::Spec;
use Text::Abbrev;
use List::Util qw(min);
use Graph::Directed;
use Class::AutoDB;
use Data::Babel;
use Data::Babel::Config;
use strict;

my($db_type,$link_type,$arity,$num_maptables,$skip_pairs)=@ARGV;
my %db_type=abbrev qw(staggered binary);
$db_type=$db_type{$db_type} || 'staggered';
my %link_type=abbrev qw(starlike chainlike);
$link_type=$link_type{$link_type} || 'starlike';
defined $arity or $arity=2;
defined $num_maptables or $num_maptables=10;
defined $skip_pairs or $skip_pairs=0;
my $last_maptable=$num_maptables-1;

# diag "\$db_type=$db_type, \$link_type=$link_type, \$arity=$arity, \$num_maptables=$num_maptables, \$skip_pairs=$skip_pairs";

# create AutoDB database
my $autodb=new Class::AutoDB(database=>'test',create=>1); 
isa_ok($autodb,'Class::AutoDB','sanity test - $autodb');
Data::Babel->autodb($autodb);
my $dbh=$autodb->dbh;

# make graph to guide schema construction. each node will generate a maptable
my $graph=new Graph::Directed;
my $root=0;			# root is node 0
$graph->add_vertex($root);
my $more=$num_maptables-1;	# number of nodes remaining
my @roots=$root;		# queue of nodes to root subtrees

while ($more) {
  my $root=shift @roots;
  for (1..min($arity,$more)) {
    my $kid=$num_maptables-$more--;
    $graph->add_edge($root,$kid);
    push(@roots,$kid);
  }
}

# make component objects and Babel
# 
# 'link' IdTypes connect MapTables.
# 'leaf' IdTypes are private to each MapTable
#
# make explicit Masters for even-numbered leafs
#
my $sql_type='VARCHAR(255)';
my(@idtypes,@masters,@maptables);
for (0..$last_maptable) {	  # make leaf IdTypes & Masters
  my $idtype_name='leaf_'.sprintf('%03d',$_);
  push(@idtypes,new Data::Babel::IdType(name=>$idtype_name,sql_type=>$sql_type));
  push(@masters,new Data::Babel::Master(name=>$idtype_name.'_master')) unless $_%2;
}
for ($graph->vertices) {	  # make link IdTypes
  my @kids=$graph->successors($_);
  next unless @kids;
  my $idtype_name='link_'.sprintf('%03d',$_);
  if ($link_type eq 'starlike') { # 1 link per level connecting parent to all kids
    push(@idtypes,new Data::Babel::IdType(name=>$idtype_name,sql_type=>$sql_type));
  } else {			  # each link connects parent to one child
    for (@kids) {
      push(@idtypes,new Data::Babel::IdType(name=>$idtype_name.'_'.sprintf('%03d',$_),
					    sql_type=>$sql_type));
    }}}
for ($graph->vertices) {	  # make MapTables
  my $maptable_num=sprintf('%03d',$_);
  my $maptable_name="maptable_$maptable_num";
  my ($parent)=$graph->predecessors($_);
  my @kids=$graph->successors($_);
  my @idtypes;
  if ($link_type eq 'starlike') { # 1 link per level connecting parent to all kids
    push(@idtypes,'link_'.sprintf('%03d',$parent)) if defined $parent;
    push(@idtypes,"link_$maptable_num") if @kids;
  } else {			  # each link connects parent to one child
    push(@idtypes,join('_','link',sprintf('%03d',$parent),$maptable_num)) if defined $parent;
    push(@idtypes,map {join('_','link',$maptable_num,sprintf('%03d',$_))} @kids);
  }
  @idtypes=sort @idtypes;
  push(@idtypes,"leaf_$maptable_num");
  push(@maptables,new Data::Babel::MapTable(name=>$maptable_name,idtypes=>\@idtypes));
}

my $babel=new Data::Babel
  (name=>'test',idtypes=>\@idtypes,masters=>\@masters,maptables=>\@maptables);
isa_ok($babel,'Data::Babel','sanity test - $babel');
my @errstrs=$babel->check_schema;
ok(!@errstrs,'sanity test - check_schema');
diag(join("\n",@errstrs)) if @errstrs;

# print $graph,"\n";
# for my $node (sort {$a <=> $b} $graph->vertices) {
#   my @parents=$graph->predecessors($node); # should be at most !
#   my @kids=sort {$a <=> $b} $graph->successors($node);
#   print "\$node=$node: \@parents=@parents; \@kids=@kids\n";
# }
# $babel->show;
# exit(1);


# setup the database
#
for (0..$last_maptable) {
  my $data=maptable_data($_);
  my $maptable_name='maptable_'.sprintf('%03d',$_);
  load_maptable($babel,$maptable_name,$data);
}
for my $master (@{$babel->masters}) {
  my $data=($master->explicit)? master_data($master): undef;
  my $master_name=$master->name;
  load_master($babel,$master_name,$data);
}
load_ur($babel,'ur');

# run the queries. 
#
my @idtypes=sort {$a->name cmp $b->name} @{$babel->idtypes};
my @leafs=grep {$_->name=~/^leaf/} @idtypes;
my @links=grep {$_->name=~/^link/} @idtypes;
@idtypes=(@leafs,@links);

# iterate over all idtypes
for my $input_idtype (@idtypes) {
  my $ok=1;
  $ok&=doit($input_idtype,[],__FILE__,__LINE__);                                  # no outputs
  $ok&=doit($input_idtype,[@idtypes[0..$#idtypes]],__FILE__,__LINE__);            # all
  $ok&=doit($input_idtype,[@leafs[0,int($#leafs/2),$#leafs]],__FILE__,__LINE__);  # triple leafs
  $ok&=doit($input_idtype,[@links[0,int($#links/2),$#links]],__FILE__,__LINE__);  # triple links
  $ok&=doit($input_idtype,[@leafs[1,int($#leafs/2)+1],@links[0,$#links]],__FILE__,__LINE__); 
				                                                  # mixed quad
  for my $j0 (0..$#idtypes-1) {
    $ok&=doit($input_idtype,[@idtypes[$j0]],__FILE__,__LINE__);		          # single
    next if $skip_pairs;
    for my $j1 ($j0+1..$#idtypes) {
      $ok&=doit($input_idtype,[@idtypes[$j0,$j1]],__FILE__,__LINE__);	          # pair
    }}
  report_pass($ok,"input=".$input_idtype->name);
}

cleanup_ur($babel);		# clean up intermediate files
done_testing();

# args are idtypes
sub doit {
  my($input_idtype,$output_idtypes,$file,$line)=@_;
  my $input_ids=idtype2ids($input_idtype);

  my $correct=select_ur
    (babel=>$babel,
     input_idtype=>$input_idtype,input_ids=>$input_ids,output_idtypes=>$output_idtypes);
  my $actual=$babel->translate
    (input_idtype=>$input_idtype,input_ids=>$input_ids,output_idtypes=>$output_idtypes);
  my $label="input_idtype=".$input_idtype->name.
    ', output_idtypes='.join(' ',map {$_->name} @$output_idtypes);
  report_fail(scalar @$correct,"BAD NEWS: \$correct empty. $label",$file,$line);
  cmp_table_quietly($actual,$correct,$label,$file,$line);
#   unless (@$correct && cmp_table($actual,$correct,$label,$file,$line)) {
#     print "break here\n";
#   }
  # NG 11-01-21: added 'translate all'
    my $correct=select_ur
    (babel=>$babel,
     input_idtype=>$input_idtype,input_ids_all=>1,output_idtypes=>$output_idtypes);
  my $actual=$babel->translate
    (input_idtype=>$input_idtype,input_ids_all=>1,output_idtypes=>$output_idtypes);
  $label.=', input_ids_all';
  report_fail(scalar @$correct,"BAD NEWS: \$correct empty. $label",$file,$line);
  cmp_table_quietly($actual,$correct,$label,$file,$line);
}

# arg is maptable number
sub maptable_data {
  my($i)=@_;
  my $maptable=$babel->name2maptable('maptable_'.sprintf('%03d',$i));
  my @series=$db_type eq 'staggered'? staggered_series($i): binary_series($i);
  my @idtype_names=map {$_->name} @{$maptable->idtypes};
  # for each value in series, create a row
  my @data;
  for my $val (@series) {
    push(@data,[map {"$_/$val"} @idtype_names]);
  }
  # add in 'multi' rows: links are 'multi','multi'; leafs are 'multi_000','multi_001']
  push(@data,[map {/^leaf/? "$_/multi_000": "$_/multi"} @idtype_names]);
  push(@data,[map {/^leaf/? "$_/multi_001": "$_/multi"} @idtype_names]);
  \@data;
}
# arg can be leaf number or Master object
sub master_data {
  my $leaf=ref $_[0]? $_[0]->idtype->name: 'leaf_'.sprintf('%03d',$_[0]);
  my @series=$db_type eq 'staggered'? staggered_series(): binary_series();
  my @data=((map {"${leaf}/$_"} @series),"${leaf}/multi_000","${leaf}/multi_001");
  \@data;
}
# generate input ids for IN clause. many don't match anything.
# arg is IdType
sub idtype2ids {
  my($idtype)=@_;
  my $name=$idtype->name;
  my @series=$db_type eq 'staggered'? staggered_series(): binary_series();
  my @data=((map {"${name}/$_"} @series),"${name}/multi","${name}/multi_000","${name}/multi_001");
  \@data;
}

# generate series of raw values for use in maptables, masters, and IN clauses
sub staggered_series {
  my($i)=@_;
  defined $i?
    ((map {'a_'.sprintf('%03d',$_)} ($i..$last_maptable)),
     (map {'b_'.sprintf('%03d',$last_maptable-$_)} (0..$i))):
       (map {('a_'.sprintf('%03d',$_),'b_'.sprintf('%03d',$_))} (0..$last_maptable));
}

sub binary_series {
  my($i)=@_;
  my @series=_binary_series($num_maptables,$i);
  map {"c_$_"} @series;
}
sub _binary_series {
  my($bits,$my_bit)=@_;
  if (defined $my_bit) {	# return $bits-wide numbers with $my_bit set
    my $mask=1<<$my_bit;
    return map {sprintf '%0*b',$bits,$_} grep {$_&$mask} (0..2**$bits-1);
  } else {			# return all $bits-wide numbers
    return map {sprintf '%0*b',$bits,$_} (0..2**$bits-1);
  }
}

# for debugging. args are number of bits, and number to convert
sub as_binary_string {sprintf '%0*b',@_}