package atmsdriver;

import atmsdriver.model.Highways;
import atmsdriver.model.Station.DIRECTION;
import atmsdriver.model.Highway;
import atmsdriver.model.Station;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import tmcsim.common.SimulationException;

/**
 * A console application to drive the ATMS Server.
 *
 * @author jdalbey, John A. Torres
 * @version 10/11/2017
 */
public final class ConsoleDriver {
    // highways model
    private final Highways highways;
    
    // lists used for user input validation
    private final List<Integer> routeNumInputList;
    private final List<String> dotColorInputList;
    /**
     * Properties for the ConsoleDriver
     */
    private static Properties ConsoleDriverProperties;

    /** Entry point for the application.
     * 
     * @param args unused
     */
    public static void main(String[] args) {
        try {
            if (System.getProperty("ATMSDRIVER_PROPERTIES") != null) 
            {
                // Load properties of runtime parameters
                if (!loadProperties()) 
                {
                    System.exit(0);
                }        
                // Create the Highway Model
                Highways highways = new Highways(
                    "config/vds_data/lds.txt",
                    "config/vds_data/loop.txt",
                    "config/vds_data/highwaysMeta.txt",
                    ConsoleDriverProperties.getProperty(
                        "FEPWriterHost"),
                    Integer.parseInt(ConsoleDriverProperties.getProperty(
                        "FEPWriterPort")));

                // Construct the console driver using the highways model
                ConsoleDriver driver = new ConsoleDriver(highways);
                driver.runConsole();    
            } else {
                throw new Exception("ATMSDRIVER_PROPERTIES system property not defined.");
            }
        } catch (Exception e) {
            Logger.getLogger("ConsoleDriver").logp(Level.SEVERE, "ConsoleDriver", "Main",
                    "Error occured initializing application", e);
            System.exit(-1);
        }
    }    
    /**
     * Load the properties file containing values for runtime parameters.
     * 
     * @param propertiesFile
     * @return 
     */
    private static boolean loadProperties() 
    {
        // Load the properties file.
        try {
            ConsoleDriverProperties = new Properties();
            ConsoleDriverProperties.load(new FileInputStream(System.getProperty("ATMSDRIVER_PROPERTIES")));
        } catch (Exception e) {
            Logger.getLogger("CosoleDriver").logp(Level.SEVERE, "ConsoleDriver",
                    "Constructor", "Exception in reading properties file.", e);
        }

        return true;
    }
    /**
     * Constructor. Sets the highways model and generates the input validation
     * lists, and then runs the console driver application.
     * @param highways 
     */
    public ConsoleDriver(Highways highways) {
        // set highways model
        this.highways = highways;
        
        // set input validation lists
        routeNumInputList = generateRouteNumInputList();
        dotColorInputList = new ArrayList<>(Arrays.asList("R", "Y", "G"));        
    }
    
    /**
     * Generates the route number list, used for user input validation.
     * @return list of route numbers.
     */
    private ArrayList<Integer> generateRouteNumInputList()
    {
        ArrayList<Integer> routeNums = new ArrayList<>();
        // add the route number for each highway to the list
        for(Highway hwy : highways.highways)
        {
            routeNums.add(hwy.routeNumber);
        }
        return routeNums;
    }

    /**
     * Runs the console driver application.
     */
    public void runConsole() {
        Scanner sc = new Scanner(System.in);
        // Run continuously
        while (true) {
            // Get necessary values for colorization of highways
            Integer routeNumber = getRouteNumber(sc);
            DIRECTION direction = getDirection(sc, routeNumber);
            Double postmile = getPostmile(sc, routeNumber, direction);
            Double range = getRange(sc, postmile);
            DOTCOLOR dotcolor = getDotColor(sc);
            
            // apply colorization to highways
            applyColorToHighwayStretch(routeNumber, direction, postmile, range, dotcolor);
        }
    }
    
    /**
     * Applies specified color to the specified highway stretch. Route number and
     * direction specify the highway. Postmile and range specify the stretch of
     * specified highway. Dot color is the color to be applied to the stretch.
     * 
     * @param routeNumber highway route number
     * @param direction highway direction
     * @param postmile origin postmile value
     * @param range range from origin postmile
     * @param dotColor the color to be applied to specified highway stretch
     */
    public void applyColorToHighwayStretch(Integer routeNumber, DIRECTION direction, 
            Double postmile, Double range, DOTCOLOR dotColor) {
        System.out.println("Applying " + dotColor.name() + " dots to highway " 
                + routeNumber + " " + direction.name() + " at postmile " 
                + postmile + " with a range of " + range + " miles...");
        
        // Get the highway by route number
        Highway highway = highways.getHighwayByRouteNumber(routeNumber);
        
        // start value for highway section, and end value for highway section
        // by postmile
        Double startPost;
        Double endPost;
        
        // postmiles increase from s to n and w to e
        
        // if the direction is south or west
        if(direction.equals(DIRECTION.SOUTH) || direction.equals(DIRECTION.WEST))
        {
            // add range value to startPost to get
            // the end postmile value of the highway section
            startPost = postmile;
            endPost = postmile + range;
            
            // iterate through the stations, if within the specified highway
            // stretch, update the station by direction and apply dot color
            for(Station station : highway.stations)
            {
                if(station.postmile > startPost && station.postmile < endPost)
                {
                    station.updateByDirection(direction, dotColor);
                }
            }
        }
        // if the direction is north or east 
        else
        {
            //subtract range value from startPost
            // to get the end postmile value of the highway section
            startPost = postmile;
            endPost = postmile - range;
            
            // iterate through the stations, if within the specified highway
            // section, update the station by direction and apply dot color
            for(Station station : highway.stations)
            {
                if(station.postmile < startPost && station.postmile > endPost)
                {
                    station.updateByDirection(direction, dotColor);
                }
            }
        }
        System.out.println("");
        try {
            highways.writeToFEP();
        } catch (SimulationException ex) {
            System.out.println("Skipping writeToFEP...");
        }
    }
    
    /**
     * Gets the highway route number from user and validates the input.
     * 
     * @param sc stdIn scanner
     * @return highway route number
     */
    private Integer getRouteNumber(Scanner sc) {
        Integer routeNum = null;
        Boolean verified = false;
        
        // validation loop
        while(!verified)
        {
            // Prints out available route numbers to user to select from
            System.out.print("Available route numbers: [");
            for(Integer rtNum : routeNumInputList)
            {
                System.out.print(rtNum.toString() + ", ");
            }
            System.out.print("]");
            System.out.println("");
            
            // Prompt user to input a route number
            System.out.println("Enter a route number: ");
            routeNum = sc.nextInt();
            System.out.println("");
            
            // validate the user's input
            if(routeNumInputList.contains(routeNum))
            {
                verified = true;
            }
            else
            {
                System.out.println("Invalid route number, please re-enter: ");
            }
        }
        
        return routeNum;
    }
    
    /**
     * Gets the highway direction from the user and validates the input.
     * 
     * @param sc stdIn scanner
     * @return highway direction
     */
    private DIRECTION getDirection(Scanner sc, Integer routeNum) {
        DIRECTION direction;
        String directionInput = null;
        Boolean verified = false;
        
        // validation loop
        while(!verified)
        {
            // Get available directions for route
            ArrayList<DIRECTION> availDirs = new ArrayList<>();
            for(Station stn : highways.getHighwayByRouteNumber(routeNum).stations)
            {
                if(!availDirs.contains(stn.direction))
                {
                    availDirs.add(stn.direction);
                }
            }
            
            // prompt user for input
            System.out.print("Available directions for highway " + routeNum + ": [");
            for(DIRECTION dir : availDirs)
            {
                System.out.print(dir.getLetter() + ", ");
            }
            System.out.print("]");
            System.out.println("");
            System.out.println("Enter a direction:");
            directionInput = sc.next().toUpperCase();
            System.out.println("");
            
            // validate the user's input
            if(availDirs.contains(DIRECTION.toDirection(directionInput)))
            {
                verified = true;
            }
            else
            {
                System.out.println("Invalid direction, please re-enter: ");
            }
        }
        
        return DIRECTION.toDirection(directionInput);
    }
    
    /**
     * Gets the starting/origin postmile value for the highway section from the 
     * user and validates the input.
     * 
     * @param sc stdIn scanner
     * @param routeNumber highway route number
     * @param dir highway direction
     * @return highway section start/origin postmile value
     */
    private Double getPostmile(Scanner sc, Integer routeNumber, DIRECTION dir) {
        Double postmile = null;
        Boolean verified = false;
        
        // validation loop
        while(!verified)
        {
            // Get highway, and grab the floor and ceiling for postmile values
            // from the highway stations to present to the user
            Highway hwy = highways.getHighwayByRouteNumber(routeNumber);
            Double floorPostmile = hwy.stations.get(0).postmile;
            Double ceilPostmile = hwy.stations
                    .get(hwy.stations.size() - 1).postmile;
            
            // present user with range of postmiles for given highway
            System.out.println("Route " + hwy.routeNumber + " " + dir 
                    + " postmile range: [" + floorPostmile + ", " 
                    + ceilPostmile + "]");
            
            // prompt user for postmile value
            System.out.println("Enter a postmile value (Integer/Double): ");
            postmile = sc.nextDouble();
            System.out.println("");
            
            // validate user's input, ensures that the postmile is within given
            // postmile range (floorPostmile, ceilPostmile)
            if(postmile >= floorPostmile && postmile <= ceilPostmile)
            {
                verified = true;
            }
            else
            {
                System.out.println("Postmile must be within postmile range: [" + floorPostmile + ", " 
                    + ceilPostmile + "] please re-enter: ");
            }
        }
        
        return postmile;
    }
    
    /**
     * Gets the range to extend the highway stretch from the start/origin postmile
     * value from the user and validates the input.
     * 
     * @param sc stdIn scanner
     * @param postmile origin/start postmile value for highway stretch
     * @return range value
     */
    private Double getRange(Scanner sc, Double postmile) {
        Double range = null;
        Boolean verified = false;
        
        // validation loop
        while(!verified)
        {
            // prompt user for range value
            System.out.println("Enter a range value (decimal):");
            range = sc.nextDouble();
            System.out.println("");
            
            // range must be greater than or equal to 0
            if(range >= 0)
            {
                verified = true;
            }
            else
            {
                System.out.println("Range must be >= 0");
            }
        }
        
        return range;
    }

    /**
     * Gets the dot color from the user, to be applied to specified highway
     * stretch and validates the user's input.
     * 
     * @param sc stdIn scanner
     * @return dot color to be applied to highway stretch
     */
    private DOTCOLOR getDotColor(Scanner sc) {
        DOTCOLOR dotColor;
        String dotColorInput = null;
        Boolean verified = false;
        
        // validationloop
        while(!verified)
        {
            // prompt user for color
            System.out.println("Enter a dot color (G/Y/R):");
            dotColorInput = sc.next();
            System.out.println("");
            // validate user's input
            if(dotColorInputList.contains(dotColorInput))
            {
                verified = true;
            }
            else
            {
                System.out.println("Invalid dot color, please re-enter: ");
            }
        }
        
        return DOTCOLOR.toDotColor(dotColorInput);
    }
    
    /**
     * Enum for highway status dot colors. Each color has associated volume
     * and occupancy constants.
     *
     * @author John A. Torres, jdalbey
     * @version 10/11/2017
     */
    public static enum DOTCOLOR {

        RED(10,10),
        YELLOW(5,5),
        GREEN(0,0);
        
        // All the first letters of the values, in order.
        private static String allLetters = "RYG";
        
        private int vol;  /* volume */
        private int occ;  /* occupancy */      
        
        private DOTCOLOR(int v, int o)
        {
            vol = v;
            occ = o;
        }
        /**
         * Return the first letter of this enum.
         *
         * @return String first letter of this enum.
         */
        public String getLetter() {
            return this.toString().substring(0, 1);
        }

        public int volume()
        {
            return vol;
        }
        public int occupancy()
        {
            return occ;
        }
        /**
         * Returns a dot color given its first character.
         *
         * @param letter the first character of a dot color
         * @return dot color corresponding to letter
         * @pre letter must be one of allLetters
         */
        public static DOTCOLOR toDotColor(String letter) {
            return values()[allLetters.indexOf(letter.charAt(0))];
        }
    }  
}