#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;

no if $] >= 5.018, warnings => 'experimental::smartmatch';

our $VERSION = '2.01';

binmode( STDOUT, ':encoding(utf-8)' );

use DateTime;
use DateTime::Format::Duration;
use Getopt::Long qw(:config no_ignore_case bundling);
use List::Util qw(first max);
use Travel::Status::DE::URA;

my (@grep_lines);
my $hide_past       = 1;
my $strftime_format = '%H:%M:%S';
my $strfrel_format  = '%M min';
my ( %edata, @edata_pre );
my $calculate_routes = 0;
my $developer_mode;
my $via;
my ( $list_services, $service );
my $ura_base    = 'http://ivu.aseag.de/interfaces/ura';
my $ura_version = 1;
my $script_name = ( split( qr{/}, $0 ) )[-1];

GetOptions(
	'f|strftime=s'  => \$strftime_format,
	'F|strfrel=s'   => \$strfrel_format,
	'h|help'        => sub { show_help(0) },
	'l|line=s@'     => \@grep_lines,
	'o|output=s@'   => \@edata_pre,
	'p|with-past'   => sub { $hide_past = 0 },
	's|service=s'   => \$service,
	'v|via=s'       => \$via,
	'V|version'     => \&show_version,
	'devmode'       => \$developer_mode,
	'list'          => \$list_services,
	'ura-base=s'    => \$ura_base,
	'ura-version=s' => \$ura_version,

) or show_help(1);

if ($list_services) {
	show_services(0);
}

if ( @ARGV != 1 ) {
	show_help(1);
}

if ( not $service and $script_name ne 'ura-m' ) {
	($service) = ( $script_name =~ m{ ^ ( [^-]+ ) -m $ }x );
}

# --line=foo,bar support
@edata_pre  = split( qr{,}, join( q{,}, @edata_pre ) );
@grep_lines = split( qr{,}, join( q{,}, @grep_lines ) );

for my $efield (@edata_pre) {
	given ($efield) {
		when ('a') { $edata{route_after}       = 1; $calculate_routes = 1 }
		when ('b') { $edata{route_before}      = 1; $calculate_routes = 1 }
		when ('f') { $edata{route_full}        = 1; $calculate_routes = 1 }
		when ('i') { $edata{indicator}         = 1 }
		when ('r') { $edata{route_interesting} = 1; $calculate_routes = 1 }
		when ('T') { $edata{relative_times}    = 1 }
		default    { $edata{$efield}           = 1 }
	}
}

if ($service) {
	my $service_ref = first { lc( $_->{shortname} ) eq lc($service) }
	Travel::Status::DE::URA::get_services();
	if ( not $service_ref ) {
		printf STDERR (
"Error: Unknown service '%s'. The following services are supported:\n\n",
			$service
		);
		show_services(1);
	}
	$ura_base    = $service_ref->{ura_base};
	$ura_version = $service_ref->{ura_version};
}

my ($stop_name) = @ARGV;

my $status = Travel::Status::DE::URA->new(
	developer_mode => $developer_mode,
	ura_base       => $ura_base,
	ura_version    => $ura_version,
	with_messages  => 1,
);

sub show_help {
	my ($code) = @_;

	print
	  "Usage: $script_name [-pV] [-o <output>] [-l <lines>] [-v <stopname>] "
	  . "<stopname>\n"
	  . "See also: man ura-m\n";

	exit $code;
}

sub show_services {
	my ($code) = @_;

	printf( "%-60s %-14s %s\n\n", 'service', 'abbr. (-s)', 'url (--ura-base)' );
	for my $service ( Travel::Status::DE::URA::get_services() ) {
		printf( "%-60s %-14s %s\n", @{$service}{qw(name shortname ura_base)} );
	}

	exit $code;
}

sub show_version {
	say "$script_name version ${VERSION}";

	exit 0;
}

sub display_result {
	my (@lines) = @_;

	if ( not @lines ) {
		die("Nothing to show\n");
	}

	my $max_col_idx = $#{ $lines[0] } - 1;

	my @format = (q{%-}) x ( $max_col_idx + 1 );

	if ( $edata{relative_times} ) {
		$format[0] = q{%};
	}

	for my $i ( 0 .. $max_col_idx ) {
		$format[$i] .= max map { length( $_->[$i] ) } @lines;
		$format[$i] .= 's';
	}

	for my $line (@lines) {

		printf( join( q{  }, @format ) . "\n", @{$line}[ 0 .. $max_col_idx ] );

		if ( @{ $line->[ $max_col_idx + 1 ] } ) {
			for my $route ( @{ $line->[ $max_col_idx + 1 ] } ) {
				printf( join( q{  }, @format ) . "\n", @{$route} );
			}
			print "\n";
		}
	}

	return;
}

sub get_exact_stop_name {
	my ($fuzzy_name) = @_;

	my @stops = $status->get_stop_by_name($fuzzy_name);

	if ( @stops == 0 ) {
		say STDERR "Got no departures for '$fuzzy_name'";
		say STDERR 'The stop may not exist or not be in service right now';
		exit(3);
	}
	elsif ( @stops == 1 ) {
		return $stops[0];
	}
	else {
		say STDERR "The stop name '$fuzzy_name' is ambiguous. Please choose "
		  . 'one of the following:';
		say STDERR join( "\n", @stops );
		exit(3);
	}
}

sub show_route {
	my ( $dt_now, $dt_format, @routes ) = @_;
	my @res;

	if ( $edata{relative_times} ) {
		@res = map {
			[
				$dt_format->format_duration(
					$_->datetime->subtract_datetime($dt_now)
				),
				q{},
				$_->name,
				q{},
			]
		} @routes;
	}
	else {
		@res = map {
			[ $_->datetime->strftime($strftime_format), q{}, $_->name, q{}, ]
		} @routes;
	}

	return @res;
}

sub show_results {
	my @output;

	my $dt_now = DateTime->now( time_zone => 'Europe/Berlin' );
	my $dt_format
	  = DateTime::Format::Duration->new( pattern => $strfrel_format );

	for my $m ( $status->messages_by_stop_name($stop_name) ) {
		printf( "# %s\n", $m );
	}

	for my $d (
		$status->results(
			calculate_routes => $calculate_routes,
			hide_past        => $hide_past,
			stop             => $stop_name,
			via              => $via,
		)
	  )
	{

		if ( ( @grep_lines and not( $d->line ~~ \@grep_lines ) ) ) {
			next;
		}
		my ( @line, @route );

		if ( $edata{route_full} ) {
			@route = (
				show_route( $dt_now, $dt_format, $d->route_pre ),
				[ ' - - - -', q{}, q{}, q{} ],
				show_route( $dt_now, $dt_format, $d->route_post ),
			);
		}
		elsif ( $edata{route_after} ) {
			@route = show_route( $dt_now, $dt_format, $d->route_post );
		}
		elsif ( $edata{route_before} ) {
			@route = reverse show_route( $dt_now, $dt_format, $d->route_pre );
		}

		if ( $edata{relative_times} ) {
			@line = (
				$dt_format->format_duration(
					$d->datetime->subtract_datetime($dt_now)
				),
				$d->line,
				q{},
				$d->destination,
				\@route,
			);
		}
		else {
			@line = (
				$d->datetime->strftime($strftime_format),
				$d->line, q{}, $d->destination, \@route,
			);
		}

		if ( $edata{indicator} ) {
			splice( @line, 1, 0, $d->stop_indicator );
		}

		if ( $edata{route_interesting} ) {
			$line[2] = join( q{  }, map { $_->name } $d->route_interesting );
		}

		push( @output, \@line );
	}

	display_result(@output);

	return;
}

if ( my $err = $status->errstr ) {
	say STDERR "Request error: ${err}";
	exit 2;
}

$stop_name = get_exact_stop_name($stop_name);
if ($via) {
	$via = get_exact_stop_name($via);
}
show_results();

__END__

=head1 NAME

ura-m - Unofficial interface to URA-based departure monitors

=head1 SYNOPSIS

B<ura-m> [B<-s> I<service>] [B<-pV>] [B<-l> I<lines>] [B<-o> I<outputtypes>]
[B<-f> I<timefmt> | B<-F> I<timefmt>]
[B<-v> I<stopname>] I<stopname>

=head1 VERSION

version 2.01

=head1 DESCRIPTION

B<ura-m> lists upcoming bus departures and bus service messages at the stop
I<name>.  It only shows realtime data and has no knowledge of schedules or
delays.  Departures without such data may not appear at all.

=head1 OPERATOR SELECTION

By default, B<ura-m> looks up departures for stops operated by ASEAG (Aachener
StraE<szlig>enbahn und Energieversorgungs AG), so it only works for Aachen and
its vicinity. Other operators (and, thus, other areas) can be selected using
either the B<-s>/B<--service> option, the B<--ura-base> option, or the program
name.

By creating a I<service>-m symlink to B<ura-m>, it will default to the URA
interface operated by I<service>, as if B<-s> I<service> was specified. So,
for example, linking tfl-m to ura-m will request departures for TfL-operated
stops, and linking aseag-m to ura-m will request departures for ASEAG-operated
stops.

Use the B<--list> option to get a list of supported backend services.

=head1 OPTIONS

=over

=item B<-f>, B<--strftime> I<format>

Format absolute times in I<format>, applies both to departure and route
output.  See DateTime(3pm) for allowed patterns.

=item B<-F>, B<--strfrel> I<format>

Format relative times in I<format>, only applies when used with B<-oT>.
See DateTime::Format::Duration(3pm) for allowed patterns.

=item B<-l>, B<--line> I<lines>

Limit output to departures of I<lines> (comma-separated list of line
names, may be used multiple times).

=item B<--list>

List supported URA services with their URLs (see B<--ura-base>) and
abbreviations (see B<-s>).

=item B<-o>, B<--output> I<outputtypes>

Format output according to I<outputtypes>. I<outputtypes> is a
comma-separated list and the B<--output> option may be repeated. Each
output type has both a short and a long form, so for instance both
C<< -or,T >> and C<< --output=route_interesting,relative_times >> are valid.

Valid output types are:

=over

=item a / route_after

For each departure, include the route after I<name>. Both stop names and
departure times are shown.

=item b / route_before

For each departure, include the route leading to I<name>. Both stop names and
departure times are shown.

=item f / route_full

For each departure, include the entire route (stop names and departure times).

=item i / indicator

Show stop point indicator, if available. This is usually a sub-stop or
platform number, such as "H3".

=item r / route_interesting

For each departure, show up to three "interesting" stops between I<name> and
its destination. The importance of a stop is determined heuristically based on
its name, so it is not always accurate.

=item T / relative_times

Show relative times. Applies to departure and route output.

=back

Note that the routes may be incomplete, since the backend only provides a
limited amount of departures and the routes are calculated from this set.
intermediate stops are always included, but both route_after and route_before
may be cut off after / before any stop. The same applies to route_full.

=item B<-p>, B<--with-past>

Include past departures. Applies both to the departure output and to the
route output of B<-oa>, B<-ob>, B<-of>.

=item B<-s>, B<--service> I<service>

Request departures for URA instance I<service>, e.g. ASEAG (Aachen, Germany)
or TfL (London, UK). Use B<--list> to get a list of supported URA instances.
Note that I<service> is not case sensitive.

=item B<-v>, B<--via> I<stop>

Only show lines which also serve I<stop> after I<name>.

=item B<-V>, B<--version>

Show version information.

=item B<--ura-base> I<url>

Set URA base to I<url>, defaults to C<< http://ivu.aseag.de/interfaces/ura >>.
See also B<--list> and B<-s>.

=item B<--ura-version> I<version>

Set URA API version to I<version>, defaults to C<< 1 >>.

=back

=head1 EXIT STATUS

Normally zero. B<1> means B<ura-m> was called with invalid options,
B<2> indicates a request error from Travel::Status::DE::URA(3pm),
B<3> a bad (unknown or ambiguous) I<stop> name.

=head1 CONFIGURATION

None.

=head1 DEPENDENCIES

=over

=item * Class::Accessor(3pm)

=item * DateTime(3pm)

=item * DateTime::Format::Duration(3pm)

=item * LWP::UserAgent(3pm)

=item * Text::CSV(3pm)

=back

=head1 BUGS AND LIMITATIONS

Unknown.

=head1 AUTHOR

Copyright (C) 2013-2016 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>

=head1 LICENSE

This program is licensed under the same terms as Perl itself.