#!/usr/bin/perl

#
# Author: Fernando H Giorgetti < fgiorgetti@gmail.com>
# Date  : 08/27/2010
#
# Description:  Provides a friendly interface for tracking packages
#               by keeping a list of all requested tracking numbers
#              till they have been delivered
#
#               It only works with brazilian tracking numbers (thru the Correios website)
#

# Libraries
use POSIX qw(strftime);
use Env qw(HOME);

#
# General configuration
#

# Tracking numbers datafile
$DATAFILE="$HOME/tracking_numbers.dat";

# Flag that indicates whether to remove tracking numbers once delivered
$REMOVE_COMPLETED=1;

# Remove Tracking numbers that have no results (invalid not yet available)
$REMOVE_NORESULTS=0;

# State that indicates whether a package has been delivered
$COMPLETED_STATE="Entregue";

# URL for the Correios website
$URL="http://websro.correios.com.br/sro_bin/txect01\$.QueryList?P_LINGUA=001&P_TIPO=001&P_COD_UNI=

";

# Website output header (used to determine where relevant content starts)
$HEADER="Data .*Local .*Situa";

# Results header (Used when showing overall results)
$RESULTSHEADER="TRACKING NUMBER    | DATE             | STATUS\n";

# End of statuses string
$ENDOFSTATUSES="____________________________________";

#
# Validate whether return code matches the expected code
# and in case it does not match, show the error message and terminate
#
sub validateReturnCode {

      $code = $_;
      $expected = $_;
      $msg = $_;

      if ( $code != $expected ) {
              printf "%s\n", $msg;
              exit $code;
      }

}

#
# Tries to create the DAT file
# In case an error occurs, show an error message then exit
#
sub createDataFile {

      if ( ! -f $DATAFILE ) {
              system(touch, "$DATAFILE");
              validateReturnCode("$?", "0", "Error creating $DATAFILE");
      }

}

sub printTrackingNumberInfo {

      $trackingNumber = shift(@_);
      $date = shift(@_);
      $status = shift(@_);
      $status =~ s/< BR>/\n                                        /g;

      printf "%-18s | %s | %s\n", $trackingNumber, $date , $status;

}

#
# Dumps the website and searches for the given tracking number's last status
#
sub dumpWebsite {

      # Reading given tracking number
      $TRACKINGNUMBER = shift(@_);

      # Dumping the website
      open(IN, "lynx -dump \"$URL$TRACKINGNUMBER\" |");

      # Reading content
      @data = < IN>;

      # Closing input
      close(IN);

      # Cleaning the status
      $date = strftime("%m/%d/%Y %H:%M", localtime);
      $STATUS = "";

      # Parsing response content
      $found = 0;
      if ( ! (grep {/$HEADER/} @data ) ) {
              $STATUS = "No data has been found";
      } else {

              # Found the header, proceeding
              foreach $line (@data) {
                      # Found the right line
                      if ($found == 1) {
                              chomp($line);
                              if ( $line =~ /([0-9]{2}\/[0-9]{2}\/[0-9]{4} [0-9]{2}:[0-9]{2})[ ]+(.*$)/ ) {
                                      # Found the 2nd latest status entry, time to skip
                                      if ($firstFound == 1) {
                                              last;
                                      }
                                      $date = $1;
                                      $STATUS = $2;
                                      $firstFound = 1;
                              } else {
                                      # Found the deadline
                                      if ( $line =~ /$ENDOFSTATUSES/ ) {
                                              last;
                                      }

                                      # Appending further lines to the status message
                                      $line =~ s/^\s+//;
                                      $STATUS .= sprintf("< BR>%s", $line);
                              }
                      }

                      # Found the header, mark found = 1 to read next line and skip
                      if ( $line =~ /$HEADER/ ) {
                              $found = 1;
                              $firstFound = 0;
                              next;
                      }
              }

      }

      # Displaying current status for given Tracking number
      printTrackingNumberInfo($TRACKINGNUMBER, $date , $STATUS);
      $LIST{$TRACKINGNUMBER}{status}="$STATUS";
      $LIST{$TRACKINGNUMBER}{date}="$date";

      # If STATUS means delivered and REMOVE_COMPLETED = 1, remove the tracking number from the list
      if (    ( $REMOVE_COMPLETED == 1 && $STATUS =~ /$COMPLETED_STATE$/ ) ||
              ( $REMOVE_NORESULTS && $found == 0) ) {
              delete $LIST{$TRACKINGNUMBER}{status};
              delete $LIST{$TRACKINGNUMBER}{date};
              delete $LIST{$TRACKINGNUMBER};
      }

}

#
# Add all valid tracking numbers to the hash named LIST
#
sub pushTrackingNumbers {

      # Loop through all given tracking numbers
      foreach $trackingNumber (@_) {

              # Sanity check
              if ( $trackingNumber !~ /[A-Z0-9]+/ ) {
                      printf "Invalid tracking number: %s\n", $trackingNumber;
                      next;
              }

              # Add to the hash
              $LIST{$trackingNumber}{status} = "";
              $LIST{$trackingNumber}{date} = "";

      }

}

#
# Serializes the active list of tracking numbers
#
sub serializeActiveList {

      # Opening output file
      open(OUT, ">$DATAFILE") or die "Could not open $DATAFILE";

      # Saving all tracking numbers and current status
      foreach $key (sort keys %LIST) {
              printf OUT "%s;%s;%s\n", $key, $LIST{$key}{date}, $LIST{$key}{status};
      }

      # Closing output file
      close(OUT);
}

#
# Main Code
#

# Validating if lynx is installed
open(LYNX, "lynx -version |") or die "Lynx must be installed! Exiting.";
close(LYNX);

# Hash of active tracking numbers
%LIST = ();

# Reading input tracking numbers
pushTrackingNumbers(@ARGV);

# Reading DAT file list and pushing to the LIST
open(DATA, "<$DATAFILE");
@LINES=< DATA>;
close(DATA);

# If there are stored tracking numbers, show a header
if (scalar(@LINES) > 0) {
      printf "Previous state for stored tracking numbers\n";
      printf "%s", $RESULTSHEADER;
}

# Pushing stored tracking numbers to STORED array
@STORED;
foreach $line (@LINES) {
      if ( $line =~ /([A-Z0-9]+);(.*);(.*)/ ) {
              printTrackingNumberInfo($1, $2, $3);
              push(@STORED, "$1");
      }
}

pushTrackingNumbers(@STORED);
printf "\n" if (scalar(@STORED) > 0);

# Dumping the website for all valid tracking numbers
if (scalar(keys %LIST) > 0) {
      printf "State for all tracking numbers\n";
      printf "%s", $RESULTSHEADER;

      foreach $trackingNumber (sort keys %LIST) {
              dumpWebsite($trackingNumber);
      }
}

# Saving the datafile
serializeActiveList;
