#!/usr/bin/env perl
## Copyright © 2009,2010 by Daniel Friesel <derf@derf.homelinux.org>
## License: WTFPL <http://sam.zoy.org/wtfpl>
##   0. You just DO WHAT THE FUCK YOU WANT TO.
use strict;
use warnings;
use 5.010;

use Travel::Routing::DE::VRR;
use Exception::Class;
use Getopt::Long qw/:config no_ignore_case/;

our $VERSION = '1.3';
my $ignore_info = 'Fahrradmitnahme';
my $efa;
my ( @from, @to, @via, $from_type, $to_type, $via_type );
my $opt = {
	'help'        => sub { exec( 'perldoc', '-F', $0 ) },
	'ignore-info' => \$ignore_info,
	'from'        => \@from,
	'to'          => \@to,
	'version' => sub { say "efa version $VERSION"; exit 0 },
	'via' => \@via,
};

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

sub handle_efa_exception {
	my ($e) = @_;

	if ( $e->isa('Travel::Routing::DE::VRR::Exception::Setup') ) {
		if ( $e->message ) {
			printf STDERR ( "Error: %s (option '%s'): %s\n", $e->description,
				$e->message );
		}
		else {
			printf STDERR (
				"Error: %s (option '%s', got '%s', want '%s')\n",
				$e->description, $e->option, $e->have, $e->want
			);
		}

		exit 1;
	}
	if ( $e->isa('Travel::Routing::DE::VRR::Exception::Net') ) {
		printf STDERR ( "Error: %s: %s\n", $e->description,
			$e->http_errstr->as_string );
		exit 2;
	}
	if ( $e->isa('Travel::Routing::DE::VRR::Exception::NoData') ) {
		printf STDERR ( 'Error: %s', $e->description );
		exit 3;
	}
	if ( $e->isa('Travel::Routing::DE::VRR::Exception::Ambiguous') ) {
		printf STDERR (
			"Error: %s for key %s. Specify one of %s\n",
			$e->description, $e->post_key, $e->possibilities
		);
		exit 4;
	}
	if ( $e->isa('Travel::Routing::DE::VRR::Exception::NoConnections') ) {
		printf STDERR ( "Error: %s: %s\n", $e->description, $e->error );
		exit 5;
	}

	printf STDERR ( "Uncatched exception: %s\n%s", ref($e), $e->trace );
	exit 10;
}

sub check_for_error {
	my ($eval_error) = @_;

	if ( not defined $efa ) {
		if (    $eval_error
			and ref($eval_error)
			and $eval_error->isa('Travel::Routing::DE::VRR::Exception') )
		{
			handle_efa_exception($eval_error);
		}
		elsif ($eval_error) {
			printf STDERR
			  "Unknown Travel::Routing::DE::VRR error:\n${eval_error}";
			exit 10;
		}
		else {
			say STDERR 'Travel::Routing::DE::VRR failed to return an object';
			exit 10;
		}
	}

	return;
}

#<<<
GetOptions(
	$opt,
	qw{
		arrive|a=s
		bike|b
		date|d=s
		depart=s
		exclude|e=s@
		from=s@{2}
		help|h
		ignore-info|I:s
		max-change|m=i
		prefer|P=s
		proximity|p
		include|i=s
		time|t=s
		timeout=i
		to=s@{2}
		version|v
		via=s@{2}
		walk-speed|w=s
	},
) or die("Please see perldoc -F $0\n");
#>>>
if ( not( @from and @to ) ) {
	if ( @ARGV == 4 ) {
		( @from[ 0, 1 ], @to[ 0, 1 ] ) = @ARGV;
	}
	elsif ( @ARGV == 6 ) {
		( @from[ 0, 1 ], @via[ 0, 1 ], @to[ 0, 1 ] ) = @ARGV;
	}
}

for my $pair ( [ \@from, \$from_type ], [ \@via, \$via_type ],
	[ \@to, \$to_type ], )
{
	next if ( not defined $pair->[0]->[1] );

	if (
		$pair->[0]->[1] =~ s{ ^ (?<type> [^:]+ ) : \s* (?<target> .+ ) $ }
		{$+{target}}x
	  )
	{
		given ( $+{type} ) {
			when ('addr') { ${ $pair->[1] } = 'address' }
			default       { ${ $pair->[1] } = $+{type} }
		}
	}
}

if ( defined $opt->{'ignore-info'} and length( $opt->{'ignore-info'} ) == 0 ) {
	$opt->{'ignore-info'} = undef;
}

$efa = eval {
	Travel::Routing::DE::VRR->new(
		origin      => [ @from, $from_type ],
		destination => [ @to,   $to_type ],
		via => ( @via ? [ @via, $via_type ] : undef ),

		arrival_time   => $opt->{arrive},
		departure_time => $opt->{depart} // $opt->{time},
		date           => $opt->{date},
		exclude        => $opt->{exclude},
		train_type     => $opt->{include},
		with_bike      => $opt->{bike},

		select_interchange_by => $opt->{prefer},
		use_near_stops        => $opt->{proximity},
		walk_speed            => $opt->{'walk-speed'},
		max_interchanges      => $opt->{'max-change'},

		lwp_options => { timeout => $opt->{timeout} },
	);
};

check_for_error($@);

my @routes = $efa->routes();

for my $i ( 0 .. $#routes ) {
	for my $c ( $routes[$i]->parts() ) {

		for my $extra ( $c->extra() ) {

			if ( not( length $ignore_info and $extra =~ /$ignore_info/i ) ) {
				say "# $extra";
			}
		}

		printf(
			"%-5s ab  %-30s %-20s %s\n%-5s an  %-30s\n\n",
			$c->get(
				qw(departure_time departure_stop train_line
				  train_destination arrival_time arrival_stop)
			),
		);
	}
	if ( $i != $#routes ) {
		print "------\n\n";
	}
}

__END__

=head1 NAME

efa - unofficial efa.vrr.de command line client

=head1 SYNOPSIS

=over

=item B<efa> B<--from> I<city> I<stop> B<--to> I<city> I<stop> [ I<additional options> ]

=item B<efa> [ I<options> ] I<from-city> I<from-stop> [ I<via-city> I<via-stop> ] I<to-city> I<to-stop>

=back

=head1 VERSION

version 1.3

=head1 DESCRIPTION

B<efa> is a command line client for the L<http://efa.vrr.de> web interface.
It sends the specified information to the online form and displays the results.

It should be noted that B<efa>, although using the web interface of a local
transport association, is able to look up connections all over Germany.

=head1 OPTIONS

=over

=item B<--from> I<city> I<stop>

Departure place

=item B<--to> I<city> I<stop>

Arrival place

=item B<--via> I<city> I<stop>

Travel via this place

In case you want I<stop> to be an address or "point of interest", you can set
it to 'addr:something' or 'poi:something'.

=item B<-t>|B<--time>|B<--depart> I<hh>:I<mm>

Journey start time

=item B<-a>|B<--arrive> I<hh>:I<mm>

Journey end time (overrides --time/--depart)

=item B<-d>|B<--date> I<dd>.I<mm>.[I<yyyy>]

Journey date

=item B<-b>|B<--bike>

Choose connections allowing to carry a bike

=item B<-e>|B<--exclude> I<transports>

Exclude I<transports> (comma separated list).

Possible transports: zug, s-bahn, u-bahn, stadtbahn, tram, stadtbus, regionalbus,
schnellbus, seilbahn, schiff, ast, sonstige

=item B<-m>|B<--max-change> I<number>

Print connections with at most I<number> interchanges

=item B<-P>|B<--prefer> I<type>

Prefer connections of I<type>:

=over

=item * speed (default)

The faster, the better

=item * nowait

Prefer connections with less interchanges

=item * nowalk

Prefer connections with less walking (at interchanges)

=back

=item B<-p>|B<--proximity>

Take stops close to the stop/start into account and possibly use them instead

=item B<-i>|B<--include> I<type>

Include connections using trains of type I<type>, where I<type> may be:

=over

=item * local (default)

only take local trains ("Verbund-/Nahverkehrslinien"). Slow, but the cheapest
method if you're not travelling long distance

=item * ic

Local trains + IC

=item * ice

All trains (local + IC + ICE)

=back

=item B<-w>|B<--walk-speed> I<speed>

Set your walking speed to I<speed>.
Accepted values: normal (default), fast, slow

=item B<-I>|B<--ignore-info> [ I<regex> ]

Ignore additional information matching I<regex> (default: /Fahrradmitnahme/)

If I<regex> is not supplied, removes the default regex (-E<gt> nothing will be ignored)

=item B<--timeout> I<seconds>

Set timeout for HTTP requests. Default: 60 seconds.

=item B<--post> I<key>=I<value>

Add I<key> with I<value> to the HTTP POST request sent to the EFA server.
This can be used to use setting B<efa> does not yet cover, like
C<--post lineRestriction=400> to also show IC and ICE trains.
Note that B<--post> will be overridden by the standard efa options, such as
B<--time>

=item B<-v>|B<--version>

Print version information

=back

=head1 EXIT STATUS

    0    Everything went well
    1    Ambiguous input, re-run efa with different arguments
    2    efa.vrr.de error (i.e. unable to find matching connections)
    3    efa.vrr.de error (usually invalid input data)
    255  Any other kind of error

=head1 CONFIGURATION

None.

=head1 DEPENDENCIES

This script requires perl 5.10 (or higher) with the modules
WWW::Mechanize and XML::LibXML.

=head1 BUGS AND LIMITATIONS

B<efa> cannot handle Unicode in its arguments, so use plain ASCII.

The Parser is quite new and may not yet cover all corner cases, use with
caution.

=head1 AUTHOR

Copyright (C) 2009,2010 by Daniel Friesel E<lt>derf@derf.homelinux.orgE<gt>

=head1 LICENSE

  0. You just DO WHAT THE FUCK YOU WANT TO.