package atmsdriver.model;

import atmsdriver.FEPLineLoader;
import java.io.File;
import java.io.FileWriter;
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.Map;
import java.util.Observable;
import java.util.Observer;
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 in the XML format required by the 
 *  FEP Simulator, which is provided by the resident toXML() method.
 * 
 *  Currently, there is no driving logic within the highways class. It is only
 *  done through an instance of the ConsoleDriver.java class. Eventually, it
 *  will receive incident data (from exchange.xml or better yet, directly from 
 *  the TMCSimulator itself) and drive the highways using resident logic.
 *
 * @author John A. Torres
 */
public class Highways {

    final private ArrayList<FEPLine> lines;
    final private String FEPHostName;
    final private int FEPPortNum;
    
    final public ArrayList<Highway> highways;
    
    public Highways(String ldsFileName, String loopsFileName,
            String highwayMetaFileName, String FEPHostName, int FEPPortNum) {
        /*
         lines = loadLines(highwayMetaFileName);
         System.out.println("SIZE: " + toXML().toCharArray().length);
         */

        FEPLineLoader ldr = new FEPLineLoader(new File(ldsFileName), new File(loopsFileName));
        this.lines = (ArrayList<FEPLine>) ldr.getFEPLines();
        this.FEPHostName = FEPHostName;
        this.FEPPortNum = FEPPortNum;
        this.highways = loadHighways();
        //writeHighwaysMeta("hard.txt");
    }
    
    private ArrayList<Highway> loadHighways() {
        System.out.println("Loading highways...");
        // The list of highways to return
        ArrayList<Highway> highways = new ArrayList<Highway>();
        
        Map<Integer, ArrayList<Station>>
                highwayMap = new HashMap<>();
        
        for (FEPLine line : lines)
        {
            ArrayList<Station> lineStations = (ArrayList<Station>) line.stations;
            for(Station station : lineStations)
            {
                Integer hwyNum = station.routeNumber;

                if(!highwayMap.containsKey(hwyNum))
                {
                    ArrayList<Station> stnList = new ArrayList<>();
                    stnList.add(station);
                    highwayMap.put(hwyNum, stnList);
                }
                else
                {
                    highwayMap.get(hwyNum).add(station);
                }
            }
        }
        
        Set<Integer> hwyKeys = highwayMap.keySet();
        for(Integer hwyKey : hwyKeys)
        {
            ArrayList<Station> hwyStations = highwayMap.get(hwyKey);
            Collections.sort(hwyStations);
            System.out.println("Loaded highway " + hwyKey + "...");
            highways.add(new Highway(hwyKey, 
                    hwyStations));
        }
        System.out.println("");
        return highways;
    }
        /**
     * 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);
        
        // 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.SOUTH) || direction.equals(Station.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("");
    }

    /*
     private ArrayList<FEPLine> loadLines(String highwayMetaFileName) {
     ArrayList<FEPLine> lines = new ArrayList<>();
     try {
     Scanner sc = new Scanner(new File(highwayMetaFileName));
     String firstLine = sc.nextLine();

     Scanner linesc = new Scanner(firstLine);
     int numLines = linesc.nextInt();
     linesc.close();

     for (int i = 0; i < numLines; i++) {
     System.out.println("CURR: " + i);
     lines.add(loadLine(sc));
     }
     sc.close();

     } catch (FileNotFoundException ex) {
     Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
     }
     return lines;
     }

     private FEPLine loadLine(Scanner sc) {
     String line = sc.nextLine();
     System.out.println(line);
     Scanner scline = new Scanner(line);

     int lineNum = scline.nextInt();
     int count = scline.nextInt();
     int numStations = scline.nextInt();
     ArrayList<Station> stations = new ArrayList<>();
     for (int i = 0; i < numStations; i++) {
     stations.add(loadStation(sc, lineNum));
     }

     return new FEPLine(lineNum, stations, count);
     }

     private Station loadStation(Scanner sc, int lineNum) {
     String line = sc.nextLine();
     System.out.println(line);
     Scanner scline = new Scanner(line);
     int ldsID = scline.nextInt();
     int drop = scline.nextInt();
     int fwy = scline.nextInt();
     DIRECTION dir = DIRECTION.getEnum(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);
     }

     private LoopDetector loadLoop(Scanner sc) {
     String line = sc.nextLine();
     Scanner scline = new Scanner(line);

     int loopID = scline.nextInt();
     int laneNum = scline.nextInt();
     String loopLoc = getLoopLoc(line); // NEED GET LOOPLOC
     scline.close();
     return new LoopDetector(loopID, loopLoc, laneNum);
     }

     private String getLoopLoc(String line) {
     Scanner sc = new Scanner(line);
     sc.nextInt();
     sc.nextInt();

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

     // Returns the loction given the whole line from the lookup file
     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;
     }
     */

    public void writeToFEP() throws SimulationException {
        try {
            Socket sock = new Socket(FEPHostName /*"192.168.251.130"*/, 8080);
            PrintWriter out = new PrintWriter(sock.getOutputStream(), true);
            System.out.println("BYTES: " + this.toXML().toCharArray().length + 1);
            out.println(this.toXML());
            sock.close();
        } 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);
        }
        updateSequences();
    }

    // CHECK: DO WE EVEN NEED TO DO THIS?
    private void updateSequences() {
        for (FEPLine line : lines) {
            line.updateSequences();
        }
    }

    /**
     * Returns the network metadata in condensed form. This is just a quick
     * script function to make a proper highway metadata configuration file, so
     * that we can read the network faster.
     *
     * @return Network metadata
     */
    public void writeHighwaysMeta(String fileName) {

        try {
            FileWriter fw = new FileWriter(new File(fileName));
            StringBuilder build = new StringBuilder();
            build.append(lines.size());
            build.append("\n");
            System.out.println(lines.size());
            fw.write(build.toString());
            int count = 1;
            for (FEPLine line : lines) {
                System.out.println("Writing num: " + count);
                count++;
                fw.write(line.getLineMeta());

            }
        } catch (IOException ex) {
            Logger.getLogger(Highways.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    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;

    }

    public Highway getHighwayByRouteNumber(Integer routeNum) {
        Highway returnHwy = null;
        for(Highway hwy : highways)
        {
            if(hwy.routeNumber.equals(routeNum))
            {
                returnHwy = hwy;
                break;
            }
        }
        return returnHwy;
    }

    private static enum XML_TAGS {

        NETWORK("Network");

        String tag;

        private XML_TAGS(String n) {
            tag = n;
        }
    }
}
