#!/usr/bin/perl -w

our $version = "1.1";

########################################################################
#
# portspage
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# This is a script for generating CRUX port listings.
# Distributed under the terms of the GPL license.
#
# Changelog:
# 1.1
#   - Limit recursion to depth 1 when searching for Pkgfiles
#   - Treat additional args as ports to be inserted into an existing index
#   - Read the stylesheet from an external file
# 1.0.5
#   - Added a table row containing the signify public key
# 1.0.4
#   - Added --date-from-pkgfile (patch from Mikhail Kolesnik)
# 1.0.3
#   - Fixed a problem with tabs in Pkgfile
# 1.0.2
#   - Might as well make it XHTML 1.1
# 1.0.1
#   - Output is now valid XHTML 1.0 Strict
#
########################################################################

use strict;
use Cwd qw(cwd getcwd);

our %options =
(
  title => "CRUX ports",
  timestamp_accuracy => 1,
  date_from_file => 0,
);
our $stylepage = "/usr/share/portspage/style.html";
our @updates; our @ports;
our %parity = ( 0 => "even", 1 => "odd" );

sub print_usage {
  my $ok = shift;
  print STDERR <<EOT;
Usage: portspage [OPTION]... [DIRECTORY [port1...portN]]

  --title=TITLE               set the page title
  --header=FILE               name of file to insert before port listing
  --footer=FILE               name of file to insert after port listing
  --timestamp-accuracy=LEVEL  0 = no timestamp, 1 = date only, 2 = date and time
                              default is 1
  --date-from-file            take date from newest file instead of directory
  --date-from-pkgfile         take date from Pkgfile instead of directory
  --version                   output version information and exit
  [DIRECTORY]                 specify a collection other than \$PWD
  [port1...portN]             individual ports to overwrite (or insert)
                              in an existing index.html

Report bugs to <jmcquah\@disroot dot org>.
EOT
  exit $ok;
}

sub parse_args {
  while (my $arg=shift @ARGV) {
    if ($arg =~ /^--header=(.*)$/) {
      $options{header} = $1;
    }
    elsif ($arg =~ /^--footer=(.*)$/) {
      $options{footer} = $1;
    }
    elsif ($arg =~ /^--title=(.*)$/) {
      $options{title} = $1;
    }
    elsif ($arg =~ /^--timestamp-accuracy=(0|1|2)$/) {
      $options{timestamp_accuracy} = $1;
    }
    elsif ($arg eq "--date-from-file") {
      $options{date_from_file} = 1;
    }
    elsif ($arg eq "--date-from-pkgfile") {
      $options{date_from_pkgfile} = 1;
    }
    elsif ($arg eq "--version") {
      print "$version\n";
      exit 0;
    }
    elsif ($arg eq "--help") {
      print_usage(0);
    }
    elsif (! $options{directory}) {
      (-d $arg) or print_usage(1);
      $options{directory} = $arg;
    }
    else {
      push @updates, $arg;
    }
  }
  $options{directory} = "." if (! $options{directory});
}

sub main {
  parse_args();

  if (@updates) { # individual ports passed as args.
                  # Discard any that are invalid.
      foreach my $port (@updates) {
        if (-f "$options{directory}/$port/Pkgfile") {
          push @ports, $port;
        } else {
          print STDERR "$port not found in $options{directory}, ignoring.\n";
        }
      }
  } else {
      foreach my $file (glob($options{directory} . "/*/Pkgfile")) {
        my $port = (split /\//, $file)[-2];
        push @ports, $port;
      }
  }

  open(my $fS, $stylepage) or die "style page missing! please reinstall $ARGV[0]";
  while (<$fS>) {
     if (m/<(title|h[1-3])>/) {
         s/(title|h[1-3])>[^<]*</$1>$options{title}</;
     }
     print;
  }
  close($fS);

  if ($options{header}) {
    open(my $hH, $options{header}) or die "Couldn't open header file";
    while (<$hH>) {
      print "  " . $_;
    }
    close($hH);
  }

  my $count = 0;
  my $firstrun = 0;
  if (@updates) { # when an existing index.html only needs a quick update
    my @queue = sort @ports;
    my %followH; my $oH; my $col_checked=0; my $oline; my $oname; my $fname;
    my @oldIdx = glob($options{directory} . "/index.htm*");
    if ($#oldIdx >= 0) {
        # check how many columns the existing index has, and modify our options accordingly
        open ($oH, $oldIdx[0]);
        while (($options{timestamp_accuracy}>0) and ($col_checked==0) and ($oline = <$oH>)) {
            if ($oline =~ m/class="header".*Port/) {
                $options{timestamp_accuracy} -= ($oline =~ m/Last modified/) ? 0 : $options{timestamp_accuracy};
                $col_checked = 1;
            }
        }
    } else {
        $firstrun = 1;
    }
    tablehead();

    HROW: while (my $p = shift @queue) {
        if ($firstrun == 1) {
            $count++;
            htmlrow($count,$p);
            next HROW;
        }
        # Shift entries from the old html index until we find a successor to the current arg
        while ( (! $followH{$p}) and ($oline=<$oH>) ) {
            chomp($oline);
            next if ($oline !~ m/^[[:space:]]*<tr class="(odd|even)"/);
            $oname = $oline;
            $oname =~ s/.*a href="(http|https|ftp):[^>]*">([^<]*)<.*/$2/;
            if ($oname lt $p) { 
                $count++;
                $oline =~ s/class="(even|odd)"/class="$parity{($count % 2)}"/;
                if ($options{timestamp_accuracy}==0) {
                    $oline =~ s/<td>[0-9]{4}-[0-1][0-9]-[0-3][0-9].*<\/td>//;
                }
                print "$oline\n";
            } elsif ($oname eq $p) {
                $count++;
                htmlrow($count, $p);
            } else {
                $count++;
                htmlrow($count, $p);
                $followH{$p} = "$oline\n";
            }
            # Before breaking out of the loop, append all the packages from the queue that 
            # are lexographically earlier than the current entry in the old html index.
	    # In the event of equality, the command-line arg takes precedence.
            while (($queue[0]) and ($queue[0] le $oname)) {
                $p = shift @queue;
                $count++;
                htmlrow($count, $p);
                $followH{$p} = "$oline\n" if ($p lt $oname);
            }
        }
        # Either the old index has a successor to the current arg, or all remaining args
        # should be appended at the end of the html table.
        if (! $followH{$p}) {
            $count++;
            htmlrow($count, $p);
            while ($p = shift @queue) {
                $count++; htmlrow($count, $p);
            }
        }
        # Args still remaining in the queue means that the old index hasn't been exhausted.
        # Decide whether to:
        # - print the next row of the old index.
        # - save it in the followH array where it will be printed later.
        if (@queue) {
            $fname = $followH{$p};
            $fname =~ s/.*a href="(http|https|ftp):[^>]*">([^<]*)<.*/$2/; 
            if ($queue[0] gt $fname) {
                $count++;
                $followH{$p} =~ s/class="(even|odd)"/class="$parity{($count % 2)}"/;
                if ($options{timestamp_accuracy}==0) {
                    $followH{$p} =~ s/<td>[0-9]{4}-[0-1][0-9]-[0-3][0-9].*<\/td>//;
                }
                print $followH{$p};
            } else {
                $followH{$queue[0]} = $followH{$p};
            }
        }
        # Shift another port from the queue
    }
    # Now append the tail of the old html index.
    while (($firstrun == 0) and ($oline = <$oH>)) {
        if ($oline =~ m/class="(even|odd)"/) {
            $count++;
            $oline =~ s/class="(even|odd)"/class="$parity{($count % 2)}"/;
            if ($options{timestamp_accuracy}==0) {
                $oline =~ s/<td>[0-9]{4}-[0-1][0-9]-[0-3][0-9].*<\/td>//;
            }
            print $oline;
        }
    }
    ($firstrun == 1) or close($oH);
  }
  else { # No individual ports specified, just process the entire collection
    tablehead();
    foreach my $port (@ports) {
        $count++;
        htmlrow($count, $port);
    }
  }

  # Close all the html tags and append the footer
  print "  </table>\n";
  print "  <p><strong>$count ports</strong></p>\n";

  if ($options{footer}) {
      open(my $fH, $options{footer}) or die "Couldn't open footer file";
      while (<$fH>) {
          print "  " . $_;
      }
      close($fH);
  }

  print "  <p><em>Generated by portspage $version on " . isotime() . ".</em></p>\n  </body>\n</html>";

  return 0;
}

sub tablehead {
    print "  <table width=\"100%\" cellspacing=\"0\">\n";
    my $CWD = getcwd;
    my $repo = (split /\//, $CWD)[-1];
    my $pubkey = "/etc/ports/".$repo.".pub";
    if ( (-e $pubkey) and open(my $kH, $pubkey) ) {
      while (my $line = <$kH>) {
        chomp $line;
        if ($line !~ "untrusted comment") {
          print "  <tr class=\"header\"><td colspan=\"4\">\n";
          print "  <strong>Signify public key:</strong> $line\n";
          print "  </td></tr>\n";
        }
      }
      close($kH);
    }
    print "   <tr class=\"header\"><td><strong>Port</strong></td>";
    print "<td><strong>Version</strong></td><td><strong>Description</strong></td>";
    if ($options{timestamp_accuracy} > 0) {
      print "<td><strong>Last modified</strong></td>";
    }
    print "</tr>\n";
}

sub htmlrow {
    my ($count, $p) = @_;
    my ($url, $version, $release, $pver, $desc, $date);

    open (my $pF, "$options{directory}/$p/Pkgfile") or die "$p/Pkgfile unreadable!";
    while (<$pF>) {
      if ($_ =~ /^#\s*URL:\s*(.*)$/) {
        $url = $1;
        $url =~ s/</&lt;/g;
        $url =~ s/>/&gt;/g;
        $url =~ s/&/&amp;/g;
      } elsif ($_ =~ /^#\s*Description:\s*(.*)$/) {
        $desc = $1;
      } elsif ($_ =~ /^version=(.*)$/) {
        $version = $1;
      } elsif ($_ =~ /^release=(.*)$/) {
        $release = $1;
      }
    }
    close ($pF);
    $pver = $version ."-". $release;
    if ($options{timestamp_accuracy} > 0) {
      if ($options{date_from_file}) {
        my @dates;
        foreach my $file (glob($options{directory}."/".$p."/*")) {
          push (@dates, (stat($file))[9]);
        }
        $date = (sort @dates)[-1];
      }
      elsif ($options{date_from_pkgfile}) {
        $date = (stat("$options{directory}/$p/Pkgfile"))[9];
      }
      else {
        $date = (stat("$options{directory}/$p"))[9];
      }
    }
   
  print "   <tr class=\"$parity{($count % 2)}\"><td>";
  ($url) ? print "<a href=\"$url\">$p</a></td>" : print "$p</td>";
  print "<td><a href=\"$options{directory}/$p/\">$pver</a></td>";
  ($desc) ? print "<td>$desc</td>" : print "<td></td>";
  print "<td>" . isotime($date, $options{timestamp_accuracy}) . "</td>" if ($date);
  print "</tr>\n";
}

sub isotime {
  my $time = (shift or time);
  my $accuracy = (shift or 2);
  my @t = gmtime ($time);
  my $year = $t[5] + 1900;
  my $month = sprintf("%02d", $t[4] + 1);
  my $day = sprintf("%02d", $t[3]);

  if ($accuracy == 1) {
    return "$year-$month-$day";
  }

  return "$year-$month-$day " . sprintf("%02d:%02d:%02d UTC", $t[2], $t[1], $t[0]);
}

exit(main());

# End of file
