/* 
 * This file is part of Transitime.org
 * 
 * Transitime.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPL) as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * Transitime.org is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Transitime.org .  If not, see <http://www.gnu.org/licenses/>.
 */
package org.transitime.applications;

import java.io.PrintWriter;
import java.util.Date;
import java.util.TimeZone;

import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.transitime.config.ConfigFileReader;
import org.transitime.configData.AgencyConfig;
import org.transitime.gtfs.readers.GtfsAgencyReader;
import org.transitime.statistics.ScheduleDataProcessor;
import org.transitime.utils.Time;

/**
 * For generating more accurate schedule times for GTFS trips.txt file by
 * using departure data obtained via GPS.
 *  
 * @author SkiBu Smith
 *
 */
public class ScheduleGenerator {

	// Command line args
	private static String agencyId;
	private static String gtfsDirectoryName;
	private static Date beginTime;
	private static Date endTime;
	private static Time timeForUsingCalendar;
	private static double desiredFractionEarly;
	private static int allowableDifferenceFromMeanSecs;
	private static int allowableDifferenceFromOriginalTimeSecs;
	private static boolean doNotUpdateFirstStopOfTrip;
	private static int allowableEarlySecs;
	private static int allowableLateSecs;
	
	// Read in configuration files. This should be done statically before
	// the logback LoggerFactory.getLogger() is called so that logback can
	// also be configured using a transitime config file. The files are
	// specified using the java system property -Dtransitime.configFiles .
	static {
		ConfigFileReader.processConfig();
	}
	
	private static final Logger logger = 
			LoggerFactory.getLogger(ScheduleGenerator.class);


	/********************** Member Functions **************************/

	/**
	 * Displays the command line options on stdout
	 * 
	 * @param options
	 */
	private static void displayCommandLineOptions(Options options) {
		// Display help
		final String commandLineSyntax = "java transitime.jar";
		final PrintWriter writer = new PrintWriter(System.out);
		final HelpFormatter helpFormatter = new HelpFormatter();
		helpFormatter.printHelp(writer,
								80, // printedRowWidth
								commandLineSyntax,
								"args:", // header
								options,
								2,             // spacesBeforeOption
								2,             // spacesBeforeOptionDescription
								null,          // footer
								true);         // displayUsage
		writer.write("Also need to set VM parameters: \n" + 
							" -Dtransitime.core.agencyId=<agencyId>\n");
		writer.close();
	}

	/**
	 * Processes all command line options using Apache CLI.
	 * Further info at http://commons.apache.org/proper/commons-cli/usage.html .
	 * Returns the CommandLine object that provides access to each arg.
	 * Exits if there is a parsing problem.
	 * 
	 * @param args The arguments from the command line
	 * @return CommandLine object that provides access to all the args
	 * @throws ParseException
	 */
	@SuppressWarnings("static-access")  // Needed for using OptionBuilder
	private static CommandLine processCommandLineOptions(String[] args) {
		// Specify the options
		Options options = new Options();
		
		options.addOption("h", false, "Display usage and help info."); 
		
		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("dirName")
                .isRequired()
                .withDescription("Directory where unzipped GTFS file are. Can " + 
                		"be used if already have current version of GTFS data " + 
                		"and it is already unzipped.")
                .create("gtfsDirectoryName")
                );
		
		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("MM-dd-yyyy")                
                .isRequired()
                .withDescription("Begin date for reading arrival/departure " +
                		"times from database. Format is MM-dd-yyyy, " +
                		"such as 9-20-2014.")
                .create("b")
                );

		options.addOption(
				OptionBuilder.hasArg()                
				.withArgName("MM-dd-yyyy")
                .withDescription("Optional end date for reading arrival/departure " +
                		"times from database. Format is MM-dd-yyyy, " +
                		"such as 9-20-2014. Will read up to current " +
                		"time if this option is not set.")
                .create("e")
                );

		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("fraction")
                .isRequired()
                .withDescription("Specifies fraction of times that should. " +
                		"Good value is probably 0.2")
                .create("f")
                );
		
		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("secs")
				.withDescription("How many seconds arrival/departure must be " +
						"within mean for the trip/stop to not be filtered out.")
				.create("allowableFromMean")
				);
		
		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("secs")
				.withDescription("How many seconds arrival/departure must be " +
						"within original schedule time for the trip/stop to " +
						"not be filtered out.")
				.create("allowableFromOriginal")
				);
		
		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("minutes")
				.withDescription("For providing schedule adherence information. " +
						"Specifies how many minutes a vehicle can be ahead of " +
						"the schedule and still be considered on time. Default " +
						"is 1 minute.")
				.create("allowableEarly")
				);
		
		options.addOption(
				OptionBuilder.hasArg()
				.withArgName("minutes")
				.withDescription("For providing schedule adherence information. " +
						"Specifies how many minutes a vehicle can be behind " +
						"schedule and still be considered on time. Default is " +
						"5 minutes.")
				.create("allowableLate")
				);
		
		options.addOption("updateFirstStopOfTrip", false, 
				"Set if should modify time even for first stops of trips."); 

		// Parse the options
		CommandLineParser parser = new BasicParser();
		CommandLine cmd = null;
		try {
			cmd = parser.parse( options, args);
		} catch (Exception e) {
			// There was a parse problem so log the problem,
			// display the command line options so user knows
			// what is needed, and exit since can't continue.
			logger.error(e.getMessage());
			displayCommandLineOptions(options);
			System.exit(0);
		}
		
		// Handle help option
		if (cmd.hasOption("h")) {
			displayCommandLineOptions(options);
			System.exit(0);
		}
		
		// Get project ID from VM param transitime.core.agencyId
		agencyId = AgencyConfig.getAgencyId();
		if (agencyId == null) {
			displayCommandLineOptions(options);
			System.exit(0);
		}
		
		// Determine if should update even the first stops of trips.
		// Default is to no update the times for those stops.
		doNotUpdateFirstStopOfTrip = !cmd.hasOption("updateFirstStopOfTrip");
		
		// Set variables based on the command line args
		gtfsDirectoryName = cmd.getOptionValue("gtfsDirectoryName");
				
		// Time zones are complicated. Need to create both timeForUsingCalendar
		// and also set the system timezone so that times are processed 
		// correctly when read from the database. NOTE: must set default
		// timezone before calling anything from Time.java so that when the
		// SimpleDateFormat objects are created when the Time class is
		// initialized they will get the correct timezone.
		String timeZoneStr = 
				GtfsAgencyReader.readTimezoneString(gtfsDirectoryName);
		TimeZone.setDefault(TimeZone.getTimeZone(timeZoneStr)); 
		timeForUsingCalendar = new Time(timeZoneStr);
		
		// Get the fraction early "e" command line option
		String fractionEarlyStr = cmd.getOptionValue("f");
		try {
			desiredFractionEarly = Double.parseDouble(fractionEarlyStr);
		} catch (NumberFormatException e1) {
			System.err.println("Paramater -f \"" + desiredFractionEarly + 
					"\" could not be parsed.");
			System.exit(-1);
		}
		if (desiredFractionEarly < 0.0 || desiredFractionEarly > 0.5) {
			System.err.println("Paramater -f \"" + desiredFractionEarly + 
					"\" must be between 0.0 and 0.5");
			System.exit(-1);

		}
		
		// Get the beginTime "b" command line option
		String beginDateStr = cmd.getOptionValue("b");
		try {
			beginTime = Time.parseDate(beginDateStr);
			
			// If specified time is in the future then reject
			if (beginTime.getTime() > System.currentTimeMillis()) {
				System.err.println("Paramater -b \"" + beginDateStr + 
						"\" is in the future and therefore invalid!");
				System.exit(-1);					
			}				
		} catch (java.text.ParseException e) {
			System.err.println("Paramater -b \"" + beginDateStr + 
					"\" could not be parsed. Format must be \"MM-dd-yyyy\"");
			System.exit(-1);
		}

		// Get the optional endTime "e" command line option
		if (cmd.hasOption("e")) {
			String endDateStr = cmd.getOptionValue("e");
			try {
				// Get the end date specified and add 24 hours since want to 
				// load data up to the end of the date
				endTime = new Date(Time.parseDate(endDateStr).getTime() + 
						24*Time.MS_PER_HOUR);
				
			} catch (java.text.ParseException e) {
				System.err.println("Paramater -e \"" + endDateStr + 
						"\" could not be parsed. Format must be \"MM-dd-yyyy\"");
				System.exit(-1);
			}
		} else {
			// End time not specified so simply uses current time
			endTime = new Date();
		}
		
		// Get the optional "allowableFromMean" command line option.
		// Default is 15 minutes.
		allowableDifferenceFromMeanSecs = 15 * Time.SEC_PER_MIN;  
		if (cmd.hasOption("allowableFromMean")) {
			String param = cmd.getOptionValue("allowableFromMean");
			try {
				allowableDifferenceFromMeanSecs = 
						Integer.parseInt(param);
			} catch (NumberFormatException e) {
				System.err.println("Option -allowableFromMean value \"" + 
						param +	"\" could not be parsed into an integer.");
				System.exit(-1);
			}
		}
		
		// Get the optional "allowableFromOriginal" command line option
		// Default is 30 minutes.
		allowableDifferenceFromOriginalTimeSecs = 30 * Time.SEC_PER_MIN;
		if (cmd.hasOption("allowableFromOriginal")) {
			String param = cmd.getOptionValue("allowableFromOriginal");
			try {
				allowableDifferenceFromOriginalTimeSecs = 
						Integer.parseInt(param);
			} catch (NumberFormatException e) {
				System.err.println("Option -allowableFromOriginal value \"" + 
						param + "\" could not be parsed into an integer.");
				System.exit(-1);
			}
		}
		
		// Get the optional "allowableEarly" and "allowableLate" command
		// line options. Default is 1 minute early and 5 minutes late.
		allowableEarlySecs = 1 * Time.SEC_PER_MIN;
		if (cmd.hasOption("allowableEarly")) {
			String param = cmd.getOptionValue("allowableEarly");
			try {
				allowableEarlySecs = (int) (Double.parseDouble(param) * Time.SEC_PER_MIN);				
			} catch (NumberFormatException e) {
				System.err.println("Option -allowableEarly value \"" + 
						param + "\" could not be parsed.");
				System.exit(-1);
			}
		}
		allowableLateSecs = 5 * Time.SEC_PER_MIN;
		if (cmd.hasOption("allowableLate")) {
			String param = cmd.getOptionValue("allowableLate");
			try {
				allowableLateSecs = (int) (Double.parseDouble(param) * Time.SEC_PER_MIN);				
			} catch (NumberFormatException e) {
				System.err.println("Option -allowableLate value \"" + 
						param + "\" could not be parsed.");
				System.exit(-1);
			}
		}
		
		// Return the CommandLine so that arguments can be further accessed
		return cmd;
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		processCommandLineOptions(args);

		// Use ScheduleDataProcessor class to actually process all the data
		ScheduleDataProcessor stats = 
				new ScheduleDataProcessor(gtfsDirectoryName, 
						beginTime, endTime, timeForUsingCalendar,
						desiredFractionEarly,
						allowableDifferenceFromMeanSecs,
						allowableDifferenceFromOriginalTimeSecs,
						doNotUpdateFirstStopOfTrip,
						allowableEarlySecs,
						allowableLateSecs);
		stats.process();	
	}
	
}