package atmsdriver.model;

import atmsdriver.trafficeventseditor.TrafficLaneEvent;
import atmsdriver.model.LoopDetector.DOTCOLOR;
import atmsdriver.model.Station.DIRECTION;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import tmcsim.common.SimulationException;

/**
 * The Highways class aggregates all Highway instances within a geographic
 * region, and all of the FEPLines within an electronic detector network, in the
 * same geographic region. An instance of Highways.java comprises the underlying
 * model for the ATMSDriver application.
 *
 * Highways uses method writeToFEP() to communicate with the FEP Simulator. It
 * creates a socket client which sends the FEP Simulator a highways status
 * message over the socket. This message is sent in the format required by the
 * FEP Simulator.
 *
 *
 * @author John A. Torres
 */
final public class Highways
{

    final private String FEPHostName;
    final private int FEPPortNum;
    
    final private List<FEPLine> lines;
    final public List<Highway> highways;

    public Highways(String highwaysMapFileName, String FEPHostName, int FEPPortNum)
    {
        // load FEP Lines
        lines = loadLines(highwaysMapFileName);
        // build highways data structure
        this.highways = buildHighways();

        // write to FEP host and port number
        this.FEPHostName = FEPHostName;
        this.FEPPortNum = FEPPortNum;
    }

    private ArrayList<Highway> buildHighways()
    {
        System.out.println("Building highways...");
        // The list of highways to return
        ArrayList<Highway> highways = new ArrayList<Highway>();
        
        // map of hwy number to its list of stations
        Map<Integer, ArrayList<Station>> highwayMap = new HashMap<>();
        
        // iterate through FEPLines and get data to add to the above map
        for (FEPLine line : lines)
        {
            // grab all stations from the current FEPLine
            ArrayList<Station> lineStations = (ArrayList<Station>) line.stations;
            // iterate through each station in the list of stations
            for (Station station : lineStations)
            {
                Integer hwyNum = station.routeNumber;
                // if the map does not contain an entry for the highway, create
                // a new entry (key/value pair) for the highway and instantiate
                // the empty list of stations
                if (!highwayMap.containsKey(hwyNum))
                {
                    ArrayList<Station> stnList = new ArrayList<>();
                    stnList.add(station);
                    highwayMap.put(hwyNum, stnList);
                } 
                // if the map does have an entry for the highway, add the current
                // station to its list of stations
                else
                {
                    highwayMap.get(hwyNum).add(station);
                }
            }
        }
        
        // get the set of highway numbers
        Set<Integer> hwyKeys = highwayMap.keySet();
        // get the highway number and associated stations and create a new hwy
        // and add the hwy to this.highways
        for (Integer hwyKey : hwyKeys)
        {
            ArrayList<Station> hwyStations = highwayMap.get(hwyKey);
            Collections.sort(hwyStations);
            System.out.println("Loaded highway " + hwyKey + " with " +
                    hwyStations.size() + " stations.");
            highways.add(new Highway(hwyKey,
                    hwyStations));
        }
        System.out.println("");
        return highways;
    }

    /** Search for a station with the given attributes 
     * 
     * @param routeNumber
     * @param direction
     * @param postmile
     * @return the desired station, or null if not found.
     */
    public Station findStation(Integer routeNumber, Station.DIRECTION direction,
            Double postmile)
    {
        // Get the highway by route number
        Highway highway = getHighwayByRouteNumber(routeNumber);
        if (highway == null)
        {
            Logger.getLogger(Highways.class.getName()).log(Level.SEVERE,  
                    "Highway "+routeNumber+" not found in findStation()", "");
            return null;
        }
        //Search the stations on this highway for a match
        for (Station station : highway.stations)
        {
            if (station.matches(direction, postmile))
            {
                return station;
            }
        }
        return null;
    }
    /**
     * 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, Station.DIRECTION direction,
            Double postmile, Double range, LoopDetector.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 = getHighwayByRouteNumber(routeNumber);
        if (highway == null)
        {
            Logger.getLogger(Highways.class.getName()).log(Level.SEVERE,  
                    "Highway "+routeNumber+" not found trying to applyColor", "");
            return;
        }
        // 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(Station.DIRECTION.NORTH) || direction.equals(Station.DIRECTION.EAST))
        {
            // add range value to startPost to get
            // the end postmile value of the highway section
            startPost = postmile;
            endPost = postmile + range;
            //TODO: Catch NPE exception for situation when the events file 
            //   specifies a highway that doesn't exist in the network.
            //   Also the case where a desired postmile to color isn't in
            //   the network.
            // 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("");
    }
    
    /**
     * Loads all FEPLines from the specified highways map file.
     * 
     * @param highwaysMapFileName
     * @return List of FEPLines
     */
    private ArrayList<FEPLine> loadLines(String highwaysMapFileName)
    {
        ArrayList<FEPLine> lines = new ArrayList<>();
        try
        {
            Scanner sc = new Scanner(new File(highwaysMapFileName));
            // first line of file contains number of FEP Lines
            String firstLine = sc.nextLine();
            Scanner linesc = new Scanner(firstLine);
            int numLines = linesc.nextInt();
            linesc.close();
            // FOR each FEP Line
            for (int i = 0; i < numLines; i++)
            {
                lines.add(loadLine(sc));
            }
            sc.close();

        } catch (FileNotFoundException ex)
        {
            Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
        }
        return lines;
    }
    
    /**
     * Load all the stations for a single FEP Line from the highways map file.
     * 
     * @param sc scanner at the current FEPLine line
     * @return FEPLine
     */
    private FEPLine loadLine(Scanner sc)
    {
        String line = sc.nextLine();
        Scanner scline = new Scanner(line);
        // Get the attributes of this FEP Line
        int lineNum = scline.nextInt();
        int count = scline.nextInt();
        int numStations = scline.nextInt();
        
        // initialze stations array
        ArrayList<Station> stations = new ArrayList<>();
        // Read all the stations for thie FEP Line
        for (int i = 0; i < numStations; i++)
        {
            stations.add(loadStation(sc, lineNum));
        }

        return new FEPLine(lineNum, stations, count);
    }
    
    /**
     * Loads a single Station from the highways map file
     * @param sc scanner at the current station line
     * @param lineNum the FEPLine number for the station
     * @return Station
     */
    private Station loadStation(Scanner sc, int lineNum)
    {
        String line = sc.nextLine();
        Scanner scline = new Scanner(line);
        
        int ldsID = scline.nextInt();
        int drop = scline.nextInt();
        int fwy = scline.nextInt();
        DIRECTION dir = DIRECTION.toDirection(scline.next());
        double postmile = scline.nextDouble();
        int numLoops = scline.nextInt();
        String location = getStationLoc(line);
        ArrayList<LoopDetector> loops = new ArrayList<>();
        for (int i = 0; i < numLoops; i++)
        {
            loops.add(loadLoop(sc));
        }

        return new Station(lineNum, ldsID, drop, location, loops, fwy, dir, postmile);
    }
    
    /**
     * Loads a single loop from the highways map file
     *
     * @param sc scanner at the current loop line
     * @return LoopDetector
     */
    private LoopDetector loadLoop(Scanner sc)
    {
        String line = sc.nextLine();
        Scanner scline = new Scanner(line);

        int loopID = scline.nextInt();
        String loopLocID = scline.next();
        String loopLoc = scline.next();
        scline.close();
        return new LoopDetector(loopID, loopLocID, loopLoc);
    }

    /**
     * Scans the LoopDetector line and grabs the String location from the line.
     * 
     * @param line the line containing the location
     * @return A String loop location.
     */
    private String getLoopLoc(String line)
    {
        Scanner sc = new Scanner(line);
        sc.nextInt();

     // GRABS FROM CURRENT TO END OF LINE
        sc.useDelimiter("\\z");
        String loc = sc.next().trim();
        sc.close();
        return loc;
    }

    /**
     * Scans the Station line and grabs the String location from the line.
     * 
     * @param line the line containing the location
     * @return A String station location.
     */
    private String getStationLoc(String line)
    {
        Scanner scline = new Scanner(line);
        scline.nextInt();
        scline.nextInt();
        scline.nextInt();
        scline.next();
        scline.nextDouble();
        scline.nextInt();

        // GRABS FROM CURRENT TO END OF LINE
        scline.useDelimiter("\\z");
        String loc = scline.next().trim();
        scline.close();
        return loc;
    }
    
    /** 
     * Creates a socket client that writes the Highways data to the FEP Simulator.
     * 
     * @throws SimulationException 
     */
    public void writeToFEP() throws SimulationException
    {
        try
        {
            // Create the socket to the FEP Simulator
            Socket sock = new Socket(FEPHostName, FEPPortNum);
            PrintWriter out = new PrintWriter(sock.getOutputStream(), true);
            
            // Print the number of bytes the highways data message contains
            System.out.println("Highways sending " + this.toCondensedFormat(false).toCharArray().length + 1 + "bytes to FEPSIM.");
            String outMsg = this.toCondensedFormat(false);
            // Write the highways data over the socket
            out.println(outMsg);
            
            // close the socket
            sock.close();
        } catch (java.net.ConnectException ex)
        {
            //Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
            System.out.println("writeToFEP() can't connect, no data sent to FEP.");
            throw new SimulationException(SimulationException.BINDING);
        } catch (IOException ex)
        {
            //Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
            System.out.println("Highway Model failed writing to FEPSim.");
            throw new SimulationException(SimulationException.BINDING);
        }
    }
    
    /** Returns a string of highways data. If MetaDataOnly is true, you get a full
     *  dump of the highways meta data, which does not include dynamic loop values,
     *  and does include the string location names. If MetaDataOnly is false,
     *  dynamic loop values are included, and unnecessary information like string
     *  location values are not included.
     * 
     *  The FEPSimulator takes in the toCondensedFormat() output, with a MetaDataOnly
     *  value of false, over the socket.
     * 
     *  The MetaDataOnly flag should be used to get a full dump of the highways
     *  information. This was used to get the highways_fullmap.txt output.
     * 
     * @param MetaDataOnly Whether you want meta data, or a full dump for FEPSim
     * @return String, highways data in condensed format
     * 
     * Example toCondensedFormat(MetaDataOnly = false) output:
     * 
     * 43                       // "number of lines"
     * 32 0 13                  // "line id" "count num" "number of stations"
     * 1210831 1 5 S 0.9 8      // "station id" "drop num" "route num"...
     *                          //      ..."direction" "postmile" "number of loops"
     * 1210832  0.0 0  ML_1     // "loop id" "occ" "vol"
     * 1210833  0.0 0  ML_2     // ..
     * 1210834  0.0 0  ML_3     // ..
     * 1210835  0.0 0  ML_4     // ..
     * 1210836  0.0 0  PASSAGE  // ..
     * 1210837  0.0 0  DEMAND   // ..
     * 1210838  0.0 0  QUEUE    // ..
     * 1210839  0.0 0  RAMP_OFF // ..
     * ...
     * 
     * Example toCondensedFormat(MetaDataOnly = true) output:
     * 
     * 43                           // "number of lines"
     * 32 0 13                      // "line id" "count num" "number of stations"
     * 1210831 1 5 S 0.9 8 CALAFIA  // "station id" "drop num" "route num"...
     *                              //      ..."direction" "postmile"...
     *                              //      ..."number of loops" "string location"
     * 1210832 ML_1                 // "loop id" "loop location"
     * 1210833 ML_2                 // "            "
     * 1210834 ML_3                 // "            "
     * 1210835 ML_4                 // "            "
     * 1210836 PASSAGE              // "            "
     * 1210837 DEMAND               // "            "
     * 1210838 QUEUE                // "            "
     * 1210839 RAMP_OFF             // "            "
     * ...
     */
    public String toCondensedFormat(boolean MetaDataOnly)
    {
        // first line: number of FEPLines
        StringBuilder build = new StringBuilder();
        build.append(lines.size());
        build.append("\n");
        // append each fep line to the string
        for(FEPLine line : lines)
        {
            build.append(line.toCondensedFormat(MetaDataOnly));
        }
        // return the full condensed format string
        return build.toString();
    }
    
    /**
     * Returns the Highways model data in XML format.
     * Probably obsolete, since we aren't using exchange.xml any longer.
     * @return highways data in XML format
     */
    public String toXML()
    {
        String xml = null;
        try
        {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document theDoc = builder.newDocument();

            Element networkElement = theDoc.createElement(XML_TAGS.NETWORK.tag);
            theDoc.appendChild(networkElement);

            for (FEPLine line : lines)
            {
                line.toXML(networkElement);
            }

            Transformer tf = TransformerFactory.newInstance().newTransformer();

            tf.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            tf.setOutputProperty(OutputKeys.INDENT, "yes");
            tf.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");

            Writer out = new StringWriter();
            tf.transform(new DOMSource(theDoc), new StreamResult(out));
            xml = out.toString();
            out.close();
        } catch (Exception ex)
        {
            Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
        }
        return xml;

    }

    /**
     * Returns a highway by given highway number.
     * 
     * @param routeNum
     * @return Highway with specified route number, or null if no highway with
     *          the specified route num
     */
    public Highway getHighwayByRouteNumber(Integer routeNum)
    {
        Highway returnHwy = null;
        // search through highways and check routeNums
        for (Highway hwy : highways)
        {
            if (hwy.routeNumber.equals(routeNum))
            {
                returnHwy = hwy;
                break;
            }
        }
        return returnHwy;
    }

    /** Return a string representation of the Highways */
    public String toString()
    {
        StringBuilder result = new StringBuilder();
        for (Highway hwy: highways)
        {
            // Consider each route direction
            for (DIRECTION dir: hwy.availDirs)
            {
                String rowLabel = ""+String.format("%3s ",hwy.routeNumber)+dir.getLetter()+' ';
                StringBuilder lineout = new StringBuilder();
                // Examine every station on this highway and direction
                for (Station stat: hwy.stations)
                {
                    if (stat.direction.equals(dir))
                    {
                    //lineout.append("" + dir.getLetter() + stat.postmile);
                    lineout.append(stat.getColor());
                    //lineout.append("  ");
                    }
                    else 
                    {
                        lineout.append(".");
                    }
                }
                // See if there were stations for this direction
                String checkMe = lineout.toString().trim();
                // if any stations were colored, output the line
                if (checkMe.length() > 1)
                {
                    result.append(rowLabel);
                    result.append(lineout + "\n");
                }
            }
        }
        result.append("\n");
        return result.toString();
    }
    /** Return a json representation of the Highways, readable by Google Maps */
    public String toJson()
    {
        // TODO: move loading this file to init method so it doesn't get 
        // called every time.
        PostmileCoords pmList = new PostmileCoords();
        FileInputStream fis = null;
        try
        {
            fis = new FileInputStream("config/vds_data/postmile_coordinates.txt");
        }
        catch (FileNotFoundException ex)
        {
            Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
        }
        Scanner s = new Scanner(fis).useDelimiter("\\A");
        pmList.load(s);
        
        String header = "{\n" +
        "  \"type\": \"FeatureCollection\",\n" +
        "  \"features\": [";
        StringBuilder result = new StringBuilder();
        result.append(header);
        for (Highway hwy: highways)
        {
            // Examine every station on this highway
            StringBuilder lineout = new StringBuilder();
            for (Station stat: hwy.stations)
            {
                String pmID = "" + hwy.routeNumber + " " 
                        + stat.direction.getLetter() + " " 
                        + stat.postmile;
                PostmileCoords.Postmile currentPM = pmList.find(pmID);
                if (currentPM == null)
                { 
                Logger.getLogger(Highways.class.getName()).log(Level.INFO, 
                        "Postmile Coords lookup couldn't find Station: "+pmID,
                        " ");
                }
                if (currentPM != null)
                {    
                    //lineout.append("" + dir.getLetter() + stat.postmile);
                    //lineout.append(stat.getColorByDirection(dir));
                    String outString = currentPM.toJson();
                    // replace the color code with the color name
                    String colorName=stat.getColorName();
                    outString = outString.replace("desiredcolor",colorName);
                    lineout.append(outString);
                    lineout.append("  ");
                }
            }
            //result.append(rowLabel);
            result.append(lineout + "\n");

        }
        // remove last trailing comma
        result.replace(result.lastIndexOf(","), result.lastIndexOf(",") + 1, " "  );

        result.append("  ]\n" +  "}");
        return result.toString();
    }
    
    /**
     * Generates the route number list, used for user input validation.
     * @return list of route numbers.
     */
    public List<Integer> getAllRouteNums()
    {
        ArrayList<Integer> routeNums = new ArrayList<>();
        // add the route number for each highway to the list
        for(Highway hwy : highways)
        {
            routeNums.add(hwy.routeNumber);
        }
        return routeNums;
    }
    
    /**
     * XML tags used in writeToXML()
     */
    private static enum XML_TAGS
    {

        NETWORK("Network");

        String tag;

        private XML_TAGS(String n)
        {
            tag = n;
        }
    }
    
    public void reset()
    {
        for(FEPLine line : lines)
        {
            for(Station stn : line.stations)
            {
                for(LoopDetector ld : stn.loops)
                {
                    ld.occ = 0;
                    ld.vol = 0;
                }
            }
        }
    }
    
    public void applyTrafficLaneEvent(TrafficLaneEvent event)
    {
        Integer routeNum = event.routeNum;
        Highway hwy = getHighwayByRouteNumber(routeNum);
        for(Station stn: hwy.stations)
        {
            if(stn.equals(event.station))
            {
                for(LoopDetector ld : stn.loops)
                {
                    if(ld.equals(event.loopDetector))
                    {
                        ld.occ = event.color.occupancy();
                        ld.vol = event.color.volume();
                        break;
                    }
                }
                break;
            }
        }
    }
}