
use strict;
use warnings;
use Lingua::EN::Numbers qw(num2en num2en_ordinal);
use File::Slurp;
use File::Copy;
use File::Temp qw/ tempfile tempdir /;
use Data::Dumper;
use Getopt::Long;
use Cwd;

# structuredTable is a general Perl interface to creating tables in several columns in LaTeX document. 
# The basic idea is that we are passed an array of data and want to split it into columns so that we 
# can construct tables consisting of several columns, each continuing the same array of data. The data
# is split into groups by various separators. We assume that the separators contain \addlinespace tags.

# We assume that we are passed a hash containing:

# 'number of columns'
# 'columns per entry' (the number of sub-columns in each table);
# 'column heads' (an array of heads for the columns in the input data)
# 'target height' in points
# 'data' a reference to an array of hashes containing entries 'data' (in TeX form) and 'separator' (if 
#      this is a boundary between groups of data)
# 'topmatter' topmatter from the final tex document in which we're going to insert the table (to set fonts, etc)
# 'caption' caption for the table (in TeX, of course)
# 'label' the labels for the various pages of the table (if it's split across more than one will be '$label 0', '$label 1', etc..

# We then adjust the number of data items in each column, and tweak the height of the separators to get the 
# total column heights to work out as best we can.

# The first thing we use a general mechanism for figuring out the height of a piece of TeX

sub tex_height {

  my $topmatter = shift;  # anything needed to get the block to typeset correctly. Should match topmatter from final doc.
  my $texblock = shift;  # actual block of TeX
  
  my $fh;
  my $filename;
  my $texheight;
  my $dir;

  $dir = tempdir( CLEANUP => 1 );
  ($fh, $filename) = tempfile( "tempXXXXX", SUFFIX => '.tex', DIR => $dir);

  # print "filename was $filename\n";
  
  print $fh $topmatter;
  print $fh "\n\n\\begin{document}\n\n\\newsavebox{\\mybox}\n\\savebox{\\mybox}{\\parbox{4cm}{\n";
  print $fh $texblock;
  print $fh " }}\n\n\\showthe\\ht\\mybox\n\\end{document}\n\n";
  
  my @texoutput;

  open(PDFLATEX,"pdflatex --interaction=nonstopmode $filename |");
  while (<PDFLATEX>) {
    
    if ($_ =~ />(.+)pt\./) { $texheight = $1; }
    push @texoutput, $_;
    
  }
  
  unless (defined $texheight) { die("pdfLatex couldn't TeX this chunk. Probably a package or topmatter problem.\nTeX output was: @texoutput"); }

  # print "texheight was $texheight\n";

  return $texheight;
  
 };

sub tex_source {

  my $dataref = shift;
  my @data = @{$dataref};

  # print "tex_source called with dataref $dataref which points to an array containing ".scalar @data." elements.\n";
  # We print a column in the form $dataelt->{data} $dataelt->{separator} unless a nontrivial separator occurs last.

  my @output;
  my $dataelt;
  
  foreach $dataelt ( @data ) {

    push @output, $dataelt->{data}.$dataelt->{separator};

  }

  $dataelt = pop(@data);
  push @data, $dataelt;

  pop @output;
  push @output, $dataelt->{data}; # Fix the last element, so it doesn't appear with a separator

  #print "tex_source produced output array of ".scalar @output." elements: @output \n";

  my $output = join("\n",@output);
  
  #print "tex_source produced output $output \n";

  return $output;

}

sub tex_table {

  my $tdref = shift; 
  my %tabledata = %{$tdref};

  my $numcolumns = $tabledata{'number of columns'};
  my $colsperentry = $tabledata{'columns per entry'};
  my $colheads = $tabledata{'column heads'};
  my $targetheight = $tabledata{'target height'};
  my @data = @{$tabledata{'data'}};
  my $topmatter = $tabledata{'topmatter'};
  my $caption = $tabledata{'caption'};
  my $label = $tabledata{'label'};

  die("Need 'number of columns', 'columns per entry', 'column heads', 'target height' (in pts), \n'data' (an array ref), 'topmatter', and 'caption' to call tex_table.\n") 
    unless (defined $numcolumns && defined $colsperentry && defined $targetheight && defined $topmatter && defined $caption);

  print "tex_table called with $numcolumns columns of target height $targetheight pts and ".scalar @data." pieces of data.\n";

  # job 0 is to clean the input, making sure that separators are defined (even if empty) for all data elts.
  
  # print "Actual data is: Dumper(@data) \n";

  # The idea is that we're going to shuffle the data between the columns until the texheights are as close together as we can manage, 
  # then adjust the separators slightly until they match. We first get an estimate of the size of one line.

  my @linetest;
  $linetest[0] = $data[0];

 # print "\t linetest is ".Dumper(@linetest)." and data[0] is $data[0] \n";

  my $lineheight = tex_height($topmatter,tex_source(\@linetest));

  print "Lineheight is: $lineheight. \n";

  my @columns;
  my $page = -1;
  my $column = 0;
  my $entry = 0;

  # columns is a three dimensional array: pages, columns, data.
  
 COLUMN: while ( scalar @data > 0 ) {

    if ($column == 0) { $page++; }
    print "\nColumn $column of page $page.\n\n";

    # goal: fill a column as closely as possible. We start by guessing about the number of lines.
    
    my $initialguess = int($targetheight/$lineheight);
    INITIALPUSH: for (0..$initialguess-1) { 

      push @{$columns[$page][$column]}, shift @data;
      last INITIALPUSH if (scalar @data == 0);

    }

    #print "After initial push/shift we have order \n".tex_source(\@{$columns[$page][$column]});

    if (scalar @{$columns[$page][$column]} == $initialguess) {

      print "\tPushed initialguess of $initialguess data elements into column $column of page $page. Data contains ".scalar @data." items.\n";

    } else {

      print "\tRan out of data and only pushed ".scalar @{$columns[$page][$column]}." data elements into column $column of page $page. Data contains ".scalar @data." items\n";

    }
      
    # Now we are either too large or too small; we check both.

    TOOSMALLPUSH: while (tex_height($topmatter,tex_source(\@{$columns[$page][$column]})) < $targetheight) {

	last TOOSMALLPUSH if (scalar @data == 0); # We could start with nothing on the data stack.
	push @{$columns[$page][$column]}, shift @data;
      
    }

    #print "After toosmall push/shift we have order \n".tex_source(\@{$columns[$page][$column]});

    while (tex_height($topmatter,tex_source(\@{$columns[$page][$column]})) > $targetheight) {

      unshift @data, pop @{$columns[$page][$column]};

    }

    print "\tAfter pushing and popping, we have ".scalar @{$columns[$page][$column]}." data elements for tex height of ".(tex_height($topmatter,tex_source(\@{$columns[$page][$column]})))."\n";

    # We now tune in the separators to hit the target height.

    my $rawstring;
    $rawstring = tex_source(\@{$columns[$page][$column]});
    
    my $current_sepsize;
    if ($rawstring =~ /\\addlinespace\[(.+?)em\]/) {
      $current_sepsize = $1;
    } else {
      die("Couldn't match a separator in $rawstring.\n");
    }

    my $current_height = tex_height($topmatter,tex_source(\@{$columns[$page][$column]}));

    print "\tCalculated current height of $current_height pt and current separatorsize of $current_sepsize em.\n";

    if (scalar @data > 0 || ($targetheight - $current_height) < 2*$lineheight) {

      my $data;
      my $new_sepsize = $current_sepsize + 1.0;
      foreach $data (@{$columns[$page][$column]}) { 
	$data->{separator} =~ s/\\addlinespace\[.+?\]/\\addlinespace[${new_sepsize}em]/g;
      }
      my $new_height = tex_height($topmatter,tex_source(\@{$columns[$page][$column]}));
      
      print "\tChanging separator size to $new_sepsize em results in texheight of $new_height pt.\n";
      
      # We now know that we have ($current_sepsize,$current_height) and ($new_sepsize,$new_height).
      # Using these, we solve for the $final_sepsize which should reach $target_height.
      
      my $m = ($new_height - $current_height)/($new_sepsize - $current_sepsize);
      my $final_sepsize = ($targetheight - $current_height)/$m + $current_sepsize;
      foreach $data (@{$columns[$page][$column]}) { 
	$data->{separator} =~ s/\\addlinespace\[.+?\]/\\addlinespace[${final_sepsize}em]/g;
      }
      my $final_height = tex_height($topmatter,tex_source(\@{$columns[$page][$column]}));
      
      print "\tAfter calculating final separator size of $final_sepsize em, we have a final texheight of $final_height pt (vs target of $targetheight pt) for this column.\n";
      
    } else {

      print "\tLast column is too short to adjust to final height. We do not adjust separators for final height of $current_height pt.\n";
      
    }
    
    $column++;
    if ($column == $numcolumns) { $column = 0; }
    
  }

  # We have now separated the data into columns and pages and tried to make everything match heights.
  # Our goal now is actually to produce the table as a string.

  my $numpages = $page + 1;

  print "\nThe final table has $numpages pages. Top dimension of columns looks like ".(scalar @columns - 1)."\n";

  my @finalpages;
  foreach $page (0..scalar @columns-1) { # the top dimension is pages

    # Wrap each collection of columns in a table environment.

     $finalpages[$page] .=  "\\begin{table}[h]\n \\caption{\\label{$label $page} $caption ";
     if ($numpages > 1) {
       $finalpages[$page] .= "Part ".($page+1)." of $numpages";
     }
     $finalpages[$page] .= "}\n";

    # Each column is its own table, separated by \hspace{0.25}.
  
    foreach $column (0..scalar @{$columns[$page]}-1) {

      print "column $column of page $page. \n";

      $finalpages[$page] .= "\n".'\begin{tabular}[t]{'.("l" x ($colsperentry)).'} \toprule'."\n".$colheads."\\\\ \\midrule \n".tex_source(\@{$columns[$page][$column]}).'\bottomrule'."\n".'\end{tabular}'."\n".'\hspace{0.25in}'."\n";
    
    }

    # Now we wrap up the table.

    $finalpages[$page] .= "\\end{table}\n\n";

  }

  return join("\n\\newpage\n",@finalpages);
    
}

my @data;
my $i;

foreach $i (0..100) {
  
  $data[$i]{data} = "This is table line $i.";

  if ($i % 5 == 0) {

    $data[$i]{'separator'} = '\addlinespace[0.45em]\cmidrule(r{0.5em}l{0.5em}){1-3}\addlinespace[0.45em]';

  } else {

    $data[$i]{'separator'} = '\\\\';

  }

}

# print "@data \n";

my @colheads = qw( Data ); 

my $test;
my %tabledata;

$tabledata{'number of columns'} = 3;
$tabledata{'columns per entry'} = 1;
$tabledata{'target height'} = 320;
$tabledata{'column heads'} = qw( Data );
$tabledata{'data'} = \@data;
$tabledata{'topmatter'} = <<'EOHEADER'
% Get revtex4 from http://publish.aps.org/revtex4
%\documentclass[amsmath,secnumarabic,%
%    floatfix,amssymb,aps,letterpaper,nofootinbib,showkeys]{revtex4}
\documentclass[12pt,letterpaper]{amsart}
\usepackage{times,graphicx,amsthm}
\usepackage[percent]{overpic}
\usepackage{varioref}
\pdfoutput=1
\usepackage{placeins}
\usepackage[matrix,arrow]{xy}
\usepackage{array}
\usepackage{longtable}
\usepackage{dcolumn}
%\usepackage{multicol}
\usepackage{booktabs}
% \usepackage{colortbl} this appears to conflict with revtex4
\usepackage{fullpage}
\usepackage{epic,eepic}
\usepackage{fp}
\usepackage{units}
\usepackage{url}
% \usepackage{tikz}
\newcommand{\PRop}{Rop_p}
\newcommand{\Rop}{Rop}
EOHEADER
;
$tabledata{'caption'} = "Test Caption";
$tabledata{'label'} = "testlabel";

$test = tex_table(\%tabledata);

print $test;

