#!/usr/bin/perl -w

#
# NatRail
#
# Copyright (c) 2004 by Andy Balaam
#
# natrail.sourceforge.net
#
# Released under the GNU General Public License
# with ABSOLUTELY NO WARRANTY.
#
# See the file COPYING for more information.
#

use strict;

use Date::Manip;
use Getopt::Long;
use HTTP::Request::Common;
use HTTP::Cookies;
use LWP;
use LWP::UserAgent;
use URI::Escape;
use CGI::Util;

# Two lines so MakeMaker can grok it.
my $VERSION;
$VERSION = '0.7';

use constant MAX_ERRORS => 10;

&Date_Init("DateFormat=UK");

sub get_page( $$$$$$ );
sub print_error( $$ );
sub debug_print( $ );
sub debug_print_page( $ );
sub print_info( $ );
sub fixMissingColonInTime( $ );
sub title_case( $ );
sub suggested_stations( $$$$$$$@ );
sub concat_urls( $$ );

debug_print( __LINE__." Entering debug mode" );

# User options
# ------------

# Defaults
my $opt_from = 'Durham';
my $opt_to   = 'Brighton Station';
my $opt_via  = '';
my $opt_depart_time = '';
my $opt_arrive_time = '';
my $opt_changes = 9;
my $opt_show_companies;
my $opt_output = 'text';
my $opt_debug;
my $opt_version;

# Get the options
if( !GetOptions(
       'from=s'         => \$opt_from,
	   'to=s'           => \$opt_to,
	   'via=s'          => \$opt_via,
	   'depart=s'       => \$opt_depart_time,
	   'arrive=s'       => \$opt_arrive_time,
       'changes=i'      => \$opt_changes,
       'show-companies' => \$opt_show_companies,
       'output=s'       => \$opt_output,
       'debug!'         => \$opt_debug,
       'version'        => \$opt_version,
	  ) ) {
    print_error( <<END
Usage:
$0 --from start --to end
    [--via waypoint]
    [--depart departure_time | --arrive arrival_time]
    [--changes how_many_changes] [--show-companies]
    [--output text|html] [--debug]

Example:
$0 --from Reading --to Oxford
(Returns trains that depart in the next few minutes)

$0 --from 'Durham' --to 'Brighton Station' --via 'Cheltenham' --arrive '23/04/05 12:40' --changes 5 --output text
(Returns the specific journey details requested)

$0 --version        prints version information and exits
END
  , "");

    exit( 1 );
}

if ( $opt_version ) {
    print <<END
natrail.pl $VERSION

Copyright 2004 Andy Balaam and others.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

END
  ;
    exit;
}


# HTTP stuff
# ----------

my $cookie_jar = HTTP::Cookies->new;

my $ua = LWP::UserAgent->new;
$ua->agent("NatRail/$VERSION");
$ua->cookie_jar($cookie_jar);
$ua->env_proxy;


my @base_urls;
$base_urls[0] = 'http://ojp.nationalrail.co.uk/planmyjourney/time_table/';
$base_urls[1] = 'http://www.nationalrail.co.uk/planmyjourney/time_table/';
my $base_url_i = 0;

$opt_depart_time = fixMissingColonInTime( $opt_depart_time );
$opt_arrive_time = fixMissingColonInTime( $opt_arrive_time );

my ($AD, $time);
$time=0;
if( not $opt_arrive_time and not $opt_depart_time ) {
    
    $time = 'now';
    
} elsif( not $opt_arrive_time and $opt_depart_time ) {
    
    $AD = 'D';
    $time = $opt_depart_time;
    
} elsif( $opt_arrive_time and not $opt_depart_time ) {
    
    $AD = 'A';
    $time = $opt_arrive_time;
    
} else {
    
    print_error( "Please specify either the arrival time or the departure "
        ."time, not both.", "" );
    exit( 12 );
    
}

my $parsed = ParseDate($time);
if( not $parsed ) {
    print_error( "I'm sorry, I don't understand that time: '$time'", "" );
    exit( 13 );
}

my $error_count = 0;

RESTART_AFTER_ERROR:

my $page = get_page( $opt_from, $opt_to, $opt_via,
		     $parsed, $AD, $opt_changes );

my $count = 0;
while( $page =~ /Outward travel must not be in the past/ ) {
    
    if( $count >= 10 ) {
        
        print_error( "I'm sorry, I can't get that information because the "
            ."departure time was too early.", "" );
        exit( 9 );
        
    }
    $count++;
    
    debug_print( __LINE__." Too early: trying 5 mins later..." );
  
    # Increase by 5 mins
    my $date_and_time = DateCalc( $parsed, "5 minutes" );
  
    # Re-get the results page (forcing depart time instead of arrive)
    $page = get_page( $opt_from, $opt_to, $opt_via, $date_and_time, 'D',
        $opt_changes );

}

if( $page =~ /<FONT class="error">(.*?)<\/FONT>/ ) {
    
    $error_count++;
    if( $error_count > MAX_ERRORS ) {
    
        print_error( "This query produced the following error: $1", $page );
        exit( 3 );
        
    } else {
        
        debug_print( __LINE__.
            " This query produced the following error: $1 - retrying." );
        goto RESTART_AFTER_ERROR;
        
    }
    
}

if( $page =~ /National Rail Enquiries Online: Find A Station/ ) {
    
    # Station name not recognized.
    if( $page =~ /You searched for ([^<]+)/ ) {
        my $unknown_station = $1;

        # (Normalize these just to compare them with site's message.)
        foreach( $opt_from, $opt_to ) {
            s/^\s+//; s/\s+$//
        }

        if( $unknown_station ne $opt_from and $unknown_station ne $opt_to ) {
            print_error( "Strange, site says station \"$unknown_station\" not "
                ."found, but it's not one you specified.", $page );
        } else {
            # Print the site's suggested alternatives.
            my @poss;
            while( $page =~ m!<a href=.*? class="link1">([^<]+)</a>!g ) {
                my $text = $1;
                title_case($text);
                push @poss, $text;
            }
            if( not @poss ) {
                print_error( "The station name \"$unknown_station\" was not "
                    ."found, and no alternatives were suggested.\n"
                    ."Please experiment on the National Rail web site to find "
                    ."the right name.", "$page" );
            } else {
                suggested_stations( $unknown_station, $opt_from, $opt_to,
                    $opt_via, $opt_depart_time, $opt_arrive_time, $opt_changes,
                    @poss );
            }
        }
    }
    else {
        
        $error_count++;
        if( $error_count > MAX_ERRORS ) {
        
            print_error( "Your station name wasn't recognised.  Try, e.g. \"London Victoria\" instead of \"Victoria\".", "$page" );
            
        } else {
            
            debug_print( __LINE__."Didn't see 'You searched for' in 'Find A Station' page - retrying." );
            goto RESTART_AFTER_ERROR;
            
        }
    }
    exit( 7 );
    
}

#debug_print_page( $page );

while( $page =~ /<A TITLE="Check if information is ready" HREF="(.*?)">Please follow this link to check if your information is ready.<\/A>/ ) {
    
    sleep( 1 );
    
    debug_print( __LINE__." Following link to check if ready: ".$base_urls[$base_url_i].$1 );
    my $res = $ua->request( GET $base_urls[$base_url_i].$1 );
    #debug_print( "cookie_jar=".$cookie_jar->as_string() );
    
    $page = $res->content;
    
}

debug_print_page( $page );

if( $page !~ /National Rail Enquiries Online: Journey options/ ) {
    
    $error_count++;
    if( $error_count > MAX_ERRORS ) {
    
        print_error( "Didn't get the journey options page.", $page );
        exit( 2 );
        
    } else {
        
        debug_print( __LINE__."Didn't get the journey options page - retrying." );
        goto RESTART_AFTER_ERROR;
        
    }
}

debug_print( __LINE__." Going to details matrix: ".$base_urls[$base_url_i]
    ." Matrix_Journey_Details.asp" );
my $res = $ua->request( GET $base_urls[$base_url_i]
    ."Matrix_Journey_Details.asp",
    Referer => $base_urls[$base_url_i]."Display_Timetable.asp" );

$page = $res->content;

my @from;
my @to;
my @time;
my @arrival_time;
my @total_time;
my @transport_method;
my @company;

my $cell = 0;
my $leg = 0;
my $journey = 0;

my $station;
my $arrive_time;
my $leave_time;
my $method;
my $company;

$count = 0;

while( $page =~ />&nbsp;(.+)&nbsp;<\/td>/g ) {

    $count++;
    
    if( $cell == 0 ) {
        
        $method = 0;
        $company = "";
        $station = $1;
        
	title_case($station);
        
    } elsif( $cell == 1 ) {
        
        $arrive_time = $1;
        
    } elsif( $cell == 2 ) {
        
        $leave_time = $1;
    } elsif( $cell == 3 ) {
        
        if( $1 !~ /Train|nbsp/ ) {
            $method = $1;
        }
               
    } elsif( $cell == 4 ) {
        # Finished this line
        
        if( $1 !~ /nbsp/ ) {
            $1 =~ /(.*)\./;
            $company = $1;
        }
        
        if( $arrive_time =~ /nbsp/ ) {
            # First on journey
            
            $from[$journey][$leg] = $station;
            $time[$journey][$leg] = $leave_time;
            if( $method ) {
                $transport_method[$journey][$leg] = $method;
            }
            $company[$journey][$leg] = $company;
            
        } elsif( $leave_time=~ /nbsp/ ) {
            # Last in journey
            
            $to[$journey][$leg] = $station;
            if( $method ) {
                $transport_method[$journey][$leg] = $method;
            }
            #$company[$journey][$leg] = $company;
            
            $arrival_time[$journey][$leg] = $arrive_time;
            
            $page =~ />&nbsp;DURATION:\ (.+)&nbsp;<\/TD>/g;
            $total_time[$journey] = $1;
            
            $leg = 0;
            
            $journey++;
            
            
        } else {
            # Middle of journey
            
            $to[$journey][$leg] = $station;
            
            $leg++;
            
            $from[$journey][$leg] = $station;
            $time[$journey][$leg] = $leave_time;
            $arrival_time[$journey][$leg-1] = $arrive_time;
            if( $method ) {
                $transport_method[$journey][$leg] = $method;
            }
            $company[$journey][$leg] = $company;
            
        }
        
    }
    
    $cell++;
    if( $cell == 5 ) {
        $cell = 0;
    }
}

if( $count == 0 ) {
    
    $error_count++;
    
    if( $error_count > MAX_ERRORS ) {
    
        print_error( "No journey details found.  Please try again.  If this problem persists, click \"More Info\" and send a bug report to the author.", $page );
        exit( 4 );
        
    } else {
        
        debug_print( __LINE__."No journey details found - retrying." );
        goto RESTART_AFTER_ERROR;
        
    }
    
}

print_info( UnixDate( $time, "%A %E %B %Y" ) );

if( $opt_output eq "html" ) {
    
    my $any_special_methods;
    for( my $i = 0; $i < @from; $i++ ) {
        for( my $j = 0; $j < @{$from[$i]}; $j++ ) {
            if( $transport_method[$i][$j] ) {
                $any_special_methods = 1;
                # FIXME: how to break out?
            }
        }
    }
    
    print "<table class='natrailtable'>\n";
    
    my $tmp;
    my $cols;
    
    if( $any_special_methods ) {
       $cols = 5;     
    } else {
       $cols = 6;     
    }
    
    if( $opt_show_companies ) {
        $cols++;
    }
    
    for( my $i = 0; $i < @from; $i++ ) {
        
        print "  <tr class='natrailtr'>\n";
        print "    <th class='natrailth'>Time</th>"
            ."<th class='natrailth'>From</th>"
            ."<th class='natrailth'>To</th>"
            ."<th class='natrailth'>Arrive</th>";
        
        if( $any_special_methods ) {
            print "<th class='natrailth'>By</th>";
        }
        if( $opt_show_companies ) {
            print "<th class='natrailth'>Company</th>";
        }
        
        print "\n";
        print "  </tr>\n";
        
        for( my $j = 0; $j < @{$from[$i]}; $j++ ) {
            
            print "  <tr class='natrailtr'>\n";
            print "    <td class='natrailtd'>$time[$i][$j]</td>"
                ."<td class='natrailtd'>$from[$i][$j]</td>"
                ."<td class='natrailtd'>$to[$i][$j]</td>"
                ."<td class='natrailtd'>$arrival_time[$i][$j]</td>";
            
            if( $any_special_methods ) {
            
                $tmp = $transport_method[$i][$j];
                if( $tmp ) {
                    print "    <td class='natrailtd'>$tmp</td>";
                } else {
                    print "    <td class='natrailtd'>&nbsp;</td>"
                }
                
            }
            
            if( $opt_show_companies ) {
                $tmp = $company[$i][$j];
                if( $tmp ) {
                    print "    <td class='natrailtd'>$tmp</td>";
                } else {
                    print "    <td class='natrailtd'>&nbsp;</td>";
                }
            }
            
            print "\n  </tr>\n";
        
        }
        print "  <tr class='natrailtr'>\n";
        print "<td class='natrailtd' colspan="
            .($cols)
            .">Total time: $total_time[$i]</td>";
        
        print "\n  </tr>\n";
        
        print "  <tr class='natrailtrspacer'>\n";
        print "    <td class='natrailtdspacer' colspan='$cols'><br /></td>\n";
        print "  </tr>\n";
        
    }
    
    print "</table>\n";
    
    # Times 1hr different from this query
    my $arrive_earlier_datetime;
    my $depart_earlier_datetime;
    my $arrive_later_datetime;
    my $depart_later_datetime;
    
    my $arrive_earlier_timestr = "";
    my $depart_earlier_timestr = "";
    my $arrive_later_timestr = "";
    my $depart_later_timestr = "";
    my $earlier_datestr = "";
    my $later_datestr = "";
    
    if( $opt_arrive_time ) {
        
        $arrive_earlier_datetime = DateCalc( $opt_arrive_time, "- 1 hour" );
        $arrive_later_datetime   = DateCalc( $opt_arrive_time, "+ 1 hour" );
        
        $arrive_earlier_timestr = &UnixDate( $arrive_earlier_datetime,
            "%H:%M" );
        $arrive_later_timestr = &UnixDate( $arrive_later_datetime,
            "%H:%M" );
        
        $earlier_datestr = UnixDate( $arrive_earlier_datetime,
            "%d/%m/%y" );
        $later_datestr = UnixDate( $arrive_later_datetime,
            "%d/%m/%y" );
        
        
    } elsif( $opt_depart_time ) {
        
        $depart_earlier_datetime = DateCalc( $opt_depart_time, "- 1 hour" );
        $depart_later_datetime   = DateCalc( $opt_depart_time, "+ 1 hour" );
        
        $depart_earlier_timestr = &UnixDate( $depart_earlier_datetime,
            "%H:%M" );
        $depart_later_timestr = &UnixDate( $depart_later_datetime,
            "%H:%M" );
        
        $earlier_datestr = UnixDate( $depart_earlier_datetime,
            "%d/%m/%y" );
        $later_datestr = UnixDate( $depart_later_datetime,
            "%d/%m/%y" );
        
    } else {
        
        my $tmp_now_time = ParseDate( 'now' );
        
        $depart_earlier_datetime = DateCalc( $tmp_now_time, "- 1 hour" );
        $depart_later_datetime   = DateCalc( $tmp_now_time, "+ 1 hour" );
        
        $depart_earlier_timestr = &UnixDate( $depart_earlier_datetime,
            "%H:%M" );
        $depart_later_timestr = &UnixDate( $depart_later_datetime,
            "%H:%M" );
            
        $earlier_datestr = UnixDate( $depart_earlier_datetime,
            "%d/%m/%y" );
        $later_datestr = UnixDate( $depart_later_datetime,
            "%d/%m/%y" );
        
    }
    
    print "<p><a href=\"natrail.php?from=$opt_from&to=$opt_to&via=$opt_via"
        ."&date=$earlier_datestr&depart=$depart_earlier_timestr"
        ."&arrive=$arrive_earlier_timestr"
        ."&changes=$opt_changes"
        ."&show-companies=$opt_show_companies\">Earlier trains</a> | <a "
        ."href=\"natrail.php?from=$opt_from&to=$opt_to&via=$opt_via"
        ."&date=$later_datestr&depart=$depart_later_timestr"
        ."&arrive=$arrive_later_timestr"
        ."&changes=$opt_changes"
        ."&show-companies=$opt_show_companies\">Later trains</a></p>\n";
    
} else {

    my $max_length1 = 0;
    my $max_length2 = 0;
    my $tmp;
    for( my $i = 0; $i < @from; $i++ ) {
        for( my $j = 0; $j < @{$from[$i]}; $j++ ) {
            $tmp = length( $from[$i][$j] );
            if( $tmp > $max_length1 ) {
                $max_length1 = $tmp;
            }
            $tmp = length( $to[$i][$j] );
            if( $tmp > $max_length2 ) {
                $max_length2 = $tmp;
            }
        }
    }
    
    print "\n";
    
    for( my $i = 0; $i < @from; $i++ ) {
        for( my $j = 0; $j < @{$from[$i]}; $j++ ) {
    
            print "$time[$i][$j] $from[$i][$j] "
                ." " x ( $max_length1 - length( $from[$i][$j] ) )              
                ."-> $to[$i][$j]"
                ." " x ( $max_length2 - length( $to[$i][$j] ) )
                ." $arrival_time[$i][$j]";
                
            $tmp = $transport_method[$i][$j];
            if( $tmp ) {
                print " ($tmp)";
            }
            if( $opt_show_companies ) {
                print " ".$company[$i][$j];
            }
            print "\n";
        
        }
        print "Total time: $total_time[$i]\n";
        print "\n";
    }
    
}


# --------------------- functions -----------------------------

sub get_page( $$$$$$ ) {
    
    $opt_from = shift;
    $opt_to = shift;
    $opt_via = shift;
    $time = shift;
    $AD = shift;
    $opt_changes = shift;
    
    
    # Sort out arguments
    # ---------------------------

    my $parsed_time = ParseDate( $time );

    if( not $parsed_time ) {
        print_error( "I'm sorry, I didn't understand that time: $time", "" );
        exit( 10 );
    }

    my $minute = &UnixDate( $parsed_time, "%M" );
    my $hour   = &UnixDate( $parsed_time, "%H" );

    my $date = UnixDate( $parsed_time, "%d/%m/%y" );
    
    my $page = "500";
    
    while( $page =~ /^500/ ) {
        
        debug_print( __LINE__." ".$base_urls[$base_url_i] );
        my $res = $ua->request( POST $base_urls[$base_url_i]
            .'Home_Validate_Timetable.asp',
            [0 => $opt_from,            # From
            1 => $opt_to,              # To
            3 => $opt_via,             # Via
            11 => $date,           # Date
            9 => $AD,                  # D or A for Depart or Arrive
            outHourField => $hour,     # Hour
            outMinuteField => $minute, # Minute
            27 => $opt_changes,        # Num. changes
            14 => "",                  # Return: date
            12 => "",                  #     D or A
            retHourField => "",        #     Hour
            retMinuteField => "",       #     Minute
            Referer => $base_urls[$base_url_i]
            ] );

        #debug_print( "cookie_jar=".$cookie_jar->as_string() );
    
        $page = $res->content;

        if( !$page ) {
            
            print_error( "No page returned from National Rail site.", "" );
            exit( 8 );
            
        }
        if( $page =~ /^500/ ) {
            $base_url_i++;
        }
    }
    
    debug_print_page( $page );
    
    my $count = 0;

    my $rel_url;
    my $found = $page =~ /The document has moved <A HREF="(.*?)"/;
    $rel_url = $1;
    if( !$found ) {
        $found = $page =~ /This object may be found <a HREF="(.*?)"/;
        $rel_url = $1;
    }
    
    while( $found ) {

        $count++;
    
        debug_print( __LINE__." ".concat_urls( $base_urls[$base_url_i],
            $rel_url ) );
        $res = $ua->request( GET concat_urls( $base_urls[$base_url_i],
            $rel_url ) );
    
        $page = $res->content;
        
        $found = $page =~ /The document has moved <A HREF="(.*?)"/;
        $rel_url = $1;
        if( !$found ) {
            $found = $page =~ /This object may be found <a HREF="(.*?)"/;
            $rel_url = $1;
        }
        
    }
    
    debug_print( __LINE__." $count" );
    
    if( $count == 0 ) {
    
        print_error( "No 'page moved' redirect found.", $page );
        exit( 5 );
    
    }
    
    return $page;
    
}

sub print_error( $$ ) {
    
    my $message = shift;
    my $offending_page = shift;
    
    $message = "Error: ".$message;
    
    # Write this message either with HTML or text
    if( $opt_output eq "html" ) {
        print "<p class=\"error\">$message</p>\n";
    } else {
        print "$message\n";
    }
    
    # Dump the page if needed
    if( $offending_page ) {    
        debug_print_page( $offending_page );
    }
    
}

sub debug_print( $ ) {
    
    if( $opt_debug ) {
    
        my $message = shift;
        $message = "$message";
    
        # Write this message either with HTML or text
        if( $opt_output eq "html" ) {
            print "<p class=\"info\">$message</p>\n";
        } else {
            print STDERR "$message\n";
        }
        
    }
    
}

sub debug_print_page( $ ) {
    
    my $page = shift;
    
    if( $opt_debug ) {

        if( $opt_output eq "html" ) {
        
            #$page = simple_escape( $page );
            print "<textarea rows='20' cols='80' readonly>$page</textarea>\n";
        
        } else {
        
            print STDERR "\n$page\n\n\n";
        
        }
        
    }
    
}

sub print_info( $ ) {
    
    my $message = shift;
    
    # Write this message either with HTML or text
    if( $opt_output eq "html" ) {
        
        print "<p class=\"info\">$message</p>\n";
        
    } else {
        
        print STDERR "$message\n";
        
    }
    
}

sub fixMissingColonInTime( $ ) {
    
    my $time = shift;
    
    if( !$time ) {
        return $time;
    }
    
    if( $time =~ /(.*?)(\d?\d)(\d\d)$/ ) {
        
        return "$1$2:$3"
        
    } else {
        
        return $time;
        
    }
    
}

# Modifies string passed in.
sub title_case( $ ) {
    for (shift) {
        s/([\w']+)/\u\L$1/g;
    }
}

sub suggested_stations( $$$$$$$@ ) {

    my $unknown_station = shift;
    my $opt_from = shift;
    my $opt_to = shift;
    my $opt_via = shift;
    my $opt_depart_time = shift;
    my $opt_arrive_time = shift;
    my $opt_changes = shift;
    my @poss = @_;
    
    print_error( "The station name \"$unknown_station\" was not found, "
        ."suggested instead:", "" );
    
    if( $opt_output eq "html" ) {
        
        my $depart_time = "";
        my $arrive_time = "";
        my $date = "";
        
        if( $opt_arrive_time ) {
        
            $arrive_time = &UnixDate( $opt_arrive_time, "%H:%M" );
            $date = UnixDate( $opt_arrive_time, "%d/%m/%y" );
            
        } elsif( $opt_depart_time ) {
            
            $depart_time = &UnixDate( $opt_depart_time, "%H:%M" );
            $date = UnixDate( $opt_depart_time, "%d/%m/%y" );
            
        }
        
        my $link;
        
        if( $unknown_station eq $opt_from ) {
            
            $link = "<a href='natrail.php?via=$opt_via&"
                ."changes=$opt_changes&"
                ."show-companies=$opt_show_companies&"
                ."arrive=$arrive_time&"
                ."depart=$depart_time&"
                ."date=$date&"
                ."to=$opt_to&"
                ."from=INSERT_SUGGESTION_HERE"
                ."'>INSERT_SUGGESTION_HERE</a>"; 
            
        } else {
            
            $link = "<a href='natrail.php?via=$opt_via&"
                ."changes=$opt_changes&"
                ."show-companies=$opt_show_companies&"
                ."arrive=$arrive_time&"
                ."depart=$depart_time&"
                ."date=$date&"
                ."to=INSERT_SUGGESTION_HERE&"
                ."from=$opt_from"
                ."'>INSERT_SUGGESTION_HERE</a>";
            
        }
        
        foreach( @poss ) {
            
            my $tmp_link = $link;
            
            $tmp_link =~ s/INSERT_SUGGESTION_HERE/$_/g;
            
            print_info( $tmp_link );
            
        }
        
    } else {
        
        print_error( join("\n", @poss), "" );
        
    }    
                
}

sub concat_urls( $$ ) {
    
    my $basis_url = shift;
    my $rel_url = shift;
    
    my $ans;
    
    if( $rel_url =~ /^http:/ ) {
        $ans = $rel_url;
    } else {
        $ans = $basis_url.$rel_url;
    }
    
    return $ans;
}

