#!/usr/bin/perl

use v5.14;
use autodie;
use strict;
use warnings;
use Getopt::Long;
use Scalar::Util qw(blessed);
use Time::HiRes qw(gettimeofday tv_interval);
use Attean;
use Try::Tiny;

unless (@ARGV) {
	print <<"END";
Usage: $0 -list
       $0 -i IN_FORMAT [-o OUT_FORMAT] FILENAME

END
	exit;
}

my $verbose		= 0;
my $pull		= 0;
my $push		= 0;
my $list		= 0;
my $block_size	= 25;
my %namespaces;
my $in	= 'GUESS';
my $out	= 'STRING';
my $result	= GetOptions ("list" => \$list, "verbose" => \$verbose, "block=i" => \$block_size, "pull" => \$pull, "push" => \$push, "in=s" => \$in, "out=s" => \$out, "define=s" => \%namespaces, "D=s" => \%namespaces);

if ($list) {
	say "Parsers:";
	say sprintf("- %s", s/^.*://r) for (sort Attean->list_parsers);
	say "\nSerializers:";
	say sprintf("- %s", s/^.*://r) for (sort Attean->list_serializers);
	say '';
	exit;
}

unless (@_) { push(@ARGV, '-') }
my $file	= shift or die "An input filename must be given";
my $fh;
if ($file eq '-') {
	$fh	= \*STDIN;
} else {
	open( $fh, '<:encoding(UTF-8)', $file ) or die $!;
}

my $out_io		= \*STDOUT;
$|				= 1;
my $parser;
if ($in eq 'GUESS') {
	my $class	= Attean->get_parser( filename => $file ) // 'ntriples';
	$parser		= $class->new();
} else {
	$parser		= Attean->get_parser($in)->new();
}
if ($out eq 'STRING') {
	parse_to_string($parser, $fh);
} else {
	my $sclass		= Attean->get_serializer($out) || die "*** No serializer found for format $out\n";
	my $serializer	= $sclass->new(namespaces => \%namespaces);

	try {
		if ($pull) {
			print "# Pull parsing\n" if ($verbose);
			pull_transcode($parser, $serializer, $fh, $out_io);
		} elsif ($push) {
			print "# Push parsing\n" if ($verbose);
			push_transcode($parser, $serializer, $fh, $out_io);
		} elsif ($parser->does('Attean::API::PullParser')) {
			print "# Pull parsing\n" if ($verbose);
			pull_transcode($parser, $serializer, $fh, $out_io);
		} elsif ($parser->does('Attean::API::PushParser')) {
			print "# Push parsing\n" if ($verbose);
			push_transcode($parser, $serializer, $fh, $out_io);
		}
	} catch {
		my $e	= $_;
		if (blessed($e) and $e->isa('Error::TypeTiny::Assertion')) {
			my $type	= $e->type;
			my $value	= $e->value;
			my $class	= ref($value);
			$class		=~ s/^.*:://;
			if ($type->isa('Type::Tiny::Role')) {
				my $role	= ($type->role =~ s/^.*:://r);
				die "*** Cannot serialize a $class as a $role\n";
			}
			die "*** Failed to serialize a $class with parser $sclass\n";
		}
		die "$e\n";
	};
}

sub parse_to_string {
	my $parser		= shift;
	my $fh			= shift;
	$parser->handler(sub {
		my $item	= shift;
		say $item->as_string;
	});
	$parser->parse_cb_from_io($fh);
}

sub pull_transcode {
	my $parser		= shift;
	my $serializer	= shift;
	my $fh			= shift;
	my $out_io		= shift;
	warn "Pull parser" if ($verbose);
	my $iter		= $parser->parse_iter_from_io($fh);
	$serializer->serialize_iter_to_io($out_io, $iter);
}

sub push_transcode {
	my $parser		= shift;
	my $serializer	= shift;
	my $fh			= shift;
	my $out_io		= shift;
	warn "Push parser" if ($verbose);
	if ($serializer->does('Attean::API::AppendableSerializer')) {
		my $count	= 0;
		my $start	= [gettimeofday];
		my @queue;
		my $handler	= sub {
			my $triple	= shift;
			$count++;
			print STDERR "\r" if ($verbose);
			
			push(@queue, $triple);
			if (scalar(@queue) > 100) {
				$serializer->serialize_list_to_io($out_io, @queue);
				@queue	= ();
			}
			
			if ($verbose and $count % $block_size == 0) {
				my $elapsed	= tv_interval($start);
				my $tps		= $count / $elapsed;
				print STDERR sprintf("%6d (%9.1f T/s)", $count, $tps);
			}
		};
		$parser->handler($handler);
		$parser->parse_cb_from_io($fh);

		# finish
		$serializer->serialize_list_to_io($out_io, @queue);
		my $elapsed	= tv_interval($start);
		my $tps		= $count / $elapsed;
		if ($verbose) {
			print STDERR sprintf("\r%6d (%9.1f T/s)\n", $count, $tps);
		}
	} else {
		pull_transcode($parser, $serializer, $out_io);
	}
}
