#!/usr/bin/perl -w

# yukicoder tester ver.5.8 (2015/02/21)
# Copyright (C) 2014 naoki_kp
my $VERSION = "5.8";

# コマンドオプションに問題番号を指定すると
# サンプルケースをダウンロードするスクリプト。
# $PNAME への標準入力に各サンプルを突っ込んで一致判定します。
# 自分用なのでカスタマイズ性とか知らない。

# [オプション]
# -s : 改行込みで一致判定します。
# -f : サンプルケースを再ダウンロードします。
# -q : テストケースNG時に追加情報を表示しません。
# -v : 各動作メッセージを表示します。また、テストケースOK時でも追加情報を表示します。

# 問題番号の後に実行コマンドを指定できます。

use strict;
use HTML::TreeBuilder;
use LWP::Simple;
use Getopt::Std;
use File::Path qw/make_path/;
use Time::HiRes qw/gettimeofday/;
use utf8;
$Getopt::Std::STANDARD_HELP_VERSION = 1;

binmode STDERR, ':utf8';

$| = 1;
my $ESCCR = "\x1b[1;31m";
my $ESCCG = "\x1b[32m";
my $ESCCN = "\x1b[m";

# システム定数
my $URLBASE         = 'http://y...content-available-to-author-only...r.me/problems/no/';
my $PROBLEMDIR_FMT  = 'y%04u';      # $PROBLEM_NO
my $SAMPLEDIR_IN    = 'test_in';
my $SAMPLEDIR_OUT   = 'test_out';
my $SAMPLETXT_FMT   = '#sample%02u.txt';  # $CASE_NO
my $STDOUT          = 'stdout.txt';
my $STDERR          = 'stderr.txt';
my $PNAME           = './yukicoder.exe';

sub VERSION_MESSAGE{
    print <<"EOD";
## yukicoder tester ver.$VERSION ##
EOD
}

sub HELP_MESSAGE{
    print <<EOD;
[usage]
./tester.pl [-h | --help | --version]
./tester.pl [-efstqv] <problem_no> [execution command]
 -e : eps
 -f : force download
 -s : strict mode
 -t : all testcase download(zip)
 -q : quiet
 -v : verbose
EOD
    exit;
}

sub gettick {
    my ($sec, $usec) = gettimeofday();
    return $sec * 1000 + $usec / 1000;
}

# オプション解析
my %opt;
getopts('e:fhstqv', \%opt);
if($opt{h} || !@ARGV){
    HELP_MESSAGE();
}

my $PROBLEM_NO = 0 + shift @ARGV;
my $URL = $URLBASE . $PROBLEM_NO;

# 実行パス指定
$PNAME = join " ", @ARGV if(@ARGV);

my $dirp = sprintf $PROBLEMDIR_FMT, $PROBLEM_NO;
my $diri = $dirp . "/" . $SAMPLEDIR_IN;
my $diro = $dirp . "/" . $SAMPLEDIR_OUT;
if($opt{f} || ! -d $diri || ! -d $diro){
    print "downloading samples from $URL\n" if($opt{v});
    my $html = get($URL);
    my $tree = HTML::TreeBuilder->new;
    $tree->parse($html);

    my @case;
    foreach ($tree->look_down('class','sample')){
        my @sample;
        foreach ($_->look_down('_tag','pre')){
            if(ref $_->content eq 'ARRAY'){
                push @sample, $_->content->[0];
            } else {
                push @sample, "";
            }
        }
        while(@sample >= 2){
            my $in = shift @sample;
            my $out = shift @sample;
            push @case, [$in, $out];
        }
        if(@sample){
            print STDERR "warning: フォーマット異常\n";
        }
    }
    my $num = @case;
    if($num == 0){
        print STDERR "サンプルケースがありません。(非公開の問題かもしれません)\n";
        exit(1);
    }

    make_path($diri, $diro);
    print " directory: $diri\n" if($opt{v});
    print " directory: $diro\n" if($opt{v});

    for(my $i = 0; $i < $num; $i++){
        my $fn = sprintf $SAMPLETXT_FMT, $i+1;
        my $fni = $diri . '/' . $fn;
        my $fno = $diro . '/' . $fn;
        my ($in,$out) = @{$case[$i]};
        print " file: $fn\n" if($opt{v});
        fileout($fni, $in);
        fileout($fno, $out);
    }
    print "download OK.\n" if($opt{v});
}

if($opt{t}){
    my $zipname = 'testcase.zip';
    my $URL2 = $URL . "/$zipname";
    print "downloading testcases from $URL2\n" if($opt{v});
    my $data = get($URL2);
    if(substr($data,0,2) ne 'PK'){
        print STDERR "テストケースをダウンロードできませんでした。\n";
        exit(1);
    }
    fileout_raw("$dirp/$zipname", $data);
    if(system("unzip -oq -d $dirp $dirp/$zipname")){
        print STDERR "テストケースの展開に失敗しました。\n";
        exit(1);
    } else {
        print "download OK.\n" if($opt{v});
        unlink "$dirp/$zipname";
    }
}

my @caselist;
if(opendir my $dh, $diri){
    while(my $n = readdir $dh){
        next if($n =~ /^\.\.?$/);
        next if(! -f "$diri/$n");
        push @caselist, $n;
    }
    closedir $dh;
}
@caselist = sort {
    my($t1,$n1) = ($a =~ /^(\D*)(\d*)/);
    my($t2,$n2) = ($b =~ /^(\D*)(\d*)/);
    return $t1 ne $t2 ? $t1 cmp $t2 : $n1 <=> $n2;
} @caselist;
my $num = @caselist;
my $maxlen = 0;
foreach (@caselist){
    if($maxlen < length($_)){ $maxlen = length($_); }
}

if($PNAME =~ /\.exe$/){
    system("make $PNAME") and die;
}

for(my $i = 0; $i < $num; $i++){
    my $fn = $caselist[$i];
    my $fni = $diri . '/' . $fn;
    my $fno = $diro . '/' . $fn;

    my $cmd = "$PNAME < $fni > $STDOUT 2> $STDERR";
    my $st = gettick();
    my $ret = system($cmd);
    my $et = gettick();
    my $err = fileread2($STDERR);
    my $result = 1;
    if($ret == 0){
        if(! -f $fno){
            fileout_raw($fno, fileread($STDOUT));
            $result = 3;
        } elsif($opt{e}){
            my $r1 = fileread($fno);    normalize(\$r1);
            my $r2 = fileread($STDOUT); normalize(\$r2);
            my @r1 = split /\n/, $r1;
            my @r2 = split /\n/, $r2;
            if(@r1 == @r2){
                my $eps = $opt{e};
                my $n = @r1;
                my $f = 1;
                for(my $i = 0; $i < $n; $i++){
                    if(!epschk($r1[$i], $r2[$i], $eps)){ $f = 0; last; }
                }
                $result = 0 if $f;
            }
        } elsif($opt{s}){
            my $cmpcmd = "cmp -s $fno $STDOUT";
            my $cmpret = system($cmpcmd);
            $result = 0 if(!$cmpret);
        } else {
            my $r1 = fileread($fno);    normalize(\$r1);
            my $r2 = fileread($STDOUT); normalize(\$r2);
            $result = 0 if($r1 eq $r2);
        }
    } else {
        $result = 2;
    }
    $result = ('OK', 'NG', 'Runtime Error', 'New')[$result];
    if($result ne 'OK'){ $result = "$ESCCR$result$ESCCN"; }

    printf "%-*s %s %s\n", $maxlen, $fn, $result, $result eq 'OK' ? int($et-$st)."ms" : "";
    if($opt{v} || $result ne 'OK' && $result ne 'New' && !$opt{q}){
        my $idat = fileread2($fni);
        print "[Input]\n";
        print "$ESCCG$idat$ESCCN";
        my $edat = fileread2($fno);
        print "[Expected Output]\n";
        print "$ESCCG$edat$ESCCN";
        my $odat = fileread2($STDOUT);
        print "[Your Answer]\n";
        print "$ESCCG$odat$ESCCN";
        if($err){
            print "[STDERR]\n";
            print "$ESCCG$err$ESCCN";
        }
        print "\n";
    }
}
unlink $STDOUT, $STDERR;
exit;

sub fileread {
    my $fn = shift;
    open my $fh, '<', $fn or die;
    my $tmp = $/; $/ = ''; my $data = <$fh>; $/ = $tmp;
    close $fh;
    return defined $data ? $data : "";
}
sub fileread2 {
    my $fn = shift;
    open my $fh, '<', $fn or die;
    my $data;
    for(1..10){ if(my $l = <$fh>){chomp($l);if(length($l)>100){$l=substr($l,0,100)." ...";};$data.=$l."\n";}else{last;}}
    close $fh;
    return defined $data ? $data : "";
}

sub fileout_raw{
    my($fn, $dat) = @_;
    open my $fh, '>', $fn or die;
    binmode $fh;
    print $fh $dat;
    close $fh;
}
sub fileout{
    my($fn, $dat) = @_;
    normalize(\$dat);
    fileout_raw($fn, $dat);
}

sub normalize{
    my $ref = shift;
    my $data = "";
    $$ref =~ s/[\r\n]/\n/g;
    $$ref =~ s/^\n//g;
    foreach (split /\n/, $$ref){
        s/^\s+|\s+$//;
        $data .= "$_\n";
    }
    $$ref = $data;
}

sub epschk{
    my($p1,$p2,$eps) = @_;
    if($p1 =~ /[^\d\-\.]/){
        return ($p1 eq $p2)
    }
    my $aerr = abs($p1-$p2);
    my $rerr = $p1 != 0 ? abs(($p1-$p2)/$p1) : 0;
    return ($aerr < $eps || ($p1 && $rerr < $eps));
}
