/**
* Global Sensor Networks (GSN) Source Code
* Copyright (c) 2006-2014, Ecole Polytechnique Federale de Lausanne (EPFL)
* 
* This file is part of GSN.
* 
* GSN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* 
* GSN 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 GSN.  If not, see <http://www.gnu.org/licenses/>.
* 
* File: src/gsn/http/DynamicGeoDataServlet.java
*
* @author Sofiane Sarni
*
*/

package gsn.http;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.index.strtree.STRtree;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import gsn.Main;
import gsn.Mappings;
import gsn.beans.VSensorConfig;
import gsn.http.ac.UserUtils;
import org.apache.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.sql.*;
import java.util.*;


public class DynamicGeoDataServlet extends HttpServlet {

    private static GeometryFactory geometryFactory;
    private static STRtree geoIndex;

    private static transient Logger logger = Logger.getLogger(DynamicGeoDataServlet.class);
    private static final String SEPARATOR = ",";
    private static final String NEWLINE = "\n";

    private List<SensorGeoReading> sensorReadingsList = new Vector<SensorGeoReading>();
    private HashMap<String, SensorGeoReading> sensorReadingsHash = new HashMap<String, SensorGeoReading>();
    private List<String> sensorsWithinEnvelope = new ArrayList<String>();

    private boolean debugMode = false;

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String env = HttpRequestUtils.getStringParameter("env", null, request); // e.g. "POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))";
        String field = HttpRequestUtils.getStringParameter("field", null, request);
        String timed = HttpRequestUtils.getStringParameter("timed", null, request);
        String query = HttpRequestUtils.getStringParameter("query", "value", request);
        String username = HttpRequestUtils.getStringParameter("username", null, request);
        String password = HttpRequestUtils.getStringParameter("password", null, request);
        String debugModeStr = HttpRequestUtils.getStringParameter("debug", "false", request);
        String format = HttpRequestUtils.getStringParameter("format", "csv", request);

        if (debugModeStr.equalsIgnoreCase("true"))
            debugMode = true;
        else
            debugMode = false;

        List<String> allowedSensors = new Vector<String>();

        if (Main.getContainerConfig().isAcEnabled()) {
            if (username != null && password != null) {
                if (UserUtils.allowUserToLogin(username, password) == null) {
                    response.getWriter().write("ERROR: incorrect login for user '" + username + "'. Check your credentials.");
                    return;
                } else { // user authenticated correctly
                    allowedSensors = UserUtils.getAllowedVirtualSensorsForUser(username, password, getAllSensors());
                }

            } else { // username or password is null
                response.getWriter().write("ERROR: username and password required.");
                return;
            }
        } else { // No access control
            allowedSensors = getAllSensors();
        }

        StringBuilder sb = new StringBuilder();

        StringBuilder sqlQueryStr = new StringBuilder();

        if (timed.equalsIgnoreCase("latest")) {

            for (int i = 0; i < allowedSensors.size(); i++) {
                sqlQueryStr.append("select '" + allowedSensors.get(i) + "'")
                        .append(" as name, timed, ")
                        .append(field)
                        .append(", latitude, longitude, altitude ")
                        .append(" from ")
                        .append(allowedSensors.get(i))
                        .append(" where timed = ( select max(timed) from ")
                        .append(allowedSensors.get(i))
                        .append(" )");
                if (i < allowedSensors.size() - 1)
                    sqlQueryStr.append("\n union \n");
            }

        } else { // timed explicitly specified
            for (int i = 0; i < allowedSensors.size(); i++) {
                sqlQueryStr.append("select '" + allowedSensors.get(i) + "'")
                        .append(" as name, timed, ")
                        .append(field)
                        .append(", latitude, longitude, altitude ")
                        .append(" from ")
                        .append(allowedSensors.get(i))
                        .append(" where timed = ")
                        .append(timed);
                if (i < allowedSensors.size() - 1)
                    sqlQueryStr.append("\n union \n");
            }
        }


        sb.append("env = " + env)
                .append("\ndebug = " + debugMode)
                .append("\n")
                .append("field = " + field)
                .append("\n")
                .append("timed = " + timed)
                .append("\n")
                .append("query = " + query)
                .append("\n")
                .append("all_sensors = " + sensorsToString(allowedSensors))
                .append("\n")
                .append(sqlQueryStr)
                .append("\n# -------\n")
                .append(executeQuery(sqlQueryStr.toString(), field));


        if (debugMode)
            response.getWriter().write(sb.toString());

        logger.warn(sb.toString());

        buildGeoIndex();


        try {
            sensorsWithinEnvelope = getListOfSensorsWithinEnvelope(env);
        } catch (ParseException e) {
            logger.warn(e.getMessage(), e);
            response.getWriter().write("ERROR: cannot create geographic index");
            return;
        }


        if (debugMode) {
            response.getWriter().write("\nSensors within envelope: ");
            response.getWriter().write(sensorsToString(sensorsWithinEnvelope));
            response.getWriter().write("\n");
        }

        //response.getWriter().write(formatter("default", field));
        response.getWriter().write(formatter(format, field));
    }

    public String formatter(String format, String field) {
        StringBuilder sb = new StringBuilder();
        if (format.equalsIgnoreCase("json")) {
            JSONArray sensorsReadings = new JSONArray();
            for (String aSensor : sensorsWithinEnvelope) {
                JSONObject aSensorReading = new JSONObject();
                aSensorReading.put("name", sensorReadingsHash.get(aSensor).sensorName);
                aSensorReading.put("latitude", sensorReadingsHash.get(aSensor).coordinates.getY());
                aSensorReading.put("longitude", sensorReadingsHash.get(aSensor).coordinates.getX());
                aSensorReading.put("timed", sensorReadingsHash.get(aSensor).timestamp);
                aSensorReading.put(field, sensorReadingsHash.get(aSensor).value);
                sensorsReadings.add(aSensorReading);
            }
            return sensorsReadings.toJSONString();
        } else if (format.equalsIgnoreCase("xml")) {
            sb.append("<geodata>\n");
            for (String aSensor : sensorsWithinEnvelope) {
                sb.append("\t<sensor name=\"")
                        .append(sensorReadingsHash.get(aSensor).sensorName)
                        .append("\" latitude=\"")
                        .append(sensorReadingsHash.get(aSensor).coordinates.getY())
                        .append("\" longitude=\"")
                        .append(sensorReadingsHash.get(aSensor).coordinates.getX())
                        .append("\" timed=\"")
                        .append(sensorReadingsHash.get(aSensor).timestamp)
                        .append("\" field=\"")
                        .append(field)
                        .append("\">")
                        .append(sensorReadingsHash.get(aSensor).value)
                        .append("</sensor>\n");
            }
            sb.append("</geodata>");
            return sb.toString();
        } else if (format.equalsIgnoreCase("native")) {
            for (String aSensor : sensorsWithinEnvelope) {
                sb.append(sensorReadingsHash.get(aSensor).toString() + "\n");
            }
            return sb.toString();
        } else if (format.equalsIgnoreCase("csv")) {
            sb.append("# name, latitude, longitude, timed, " + field + "\n");
            for (String aSensor : sensorsWithinEnvelope) {
                sb.append(sensorReadingsHash.get(aSensor).sensorName + ", " + sensorReadingsHash.get(aSensor).coordinates.getY() + ", " + sensorReadingsHash.get(aSensor).coordinates.getX() + ", " + sensorReadingsHash.get(aSensor).timestamp + ", " + +sensorReadingsHash.get(aSensor).value + "\n");
            }
            return sb.toString();
        } else { //default is CSV
            sb.append("# name, latitude, longitude, timed, " + field + "\n");
            for (String aSensor : sensorsWithinEnvelope) {
                sb.append(sensorReadingsHash.get(aSensor).sensorName + ", " + sensorReadingsHash.get(aSensor).coordinates.getY() + ", " + sensorReadingsHash.get(aSensor).coordinates.getX() + ", " + sensorReadingsHash.get(aSensor).timestamp + ", " + +sensorReadingsHash.get(aSensor).value + "\n");
            }
            return sb.toString();
        }
    }

    public List<String> getAllSensors() {

        Iterator iter = Mappings.getAllVSensorConfigs();
        List<String> sensors = new Vector<String>();

        while (iter.hasNext()) {
            VSensorConfig sensorConfig = (VSensorConfig) iter.next();
            //Double longitude = sensorConfig.getLongitude();
            //Double latitude = sensorConfig.getLatitude();
            //Double altitude = sensorConfig.getAltitude();
            String sensor = sensorConfig.getName();
            sensors.add(sensor);
        }

        return sensors;
    }

    public String sensorsToString(List<String> sensors) {
        StringBuilder sensorsAsString = new StringBuilder();
        for (String sensor : sensors) {
            sensorsAsString.append(sensor);
            sensorsAsString.append(SEPARATOR);
        }
        if (sensorsAsString.length() > 0)
            sensorsAsString.setLength(sensorsAsString.length() - 1);  // remove the last SEPARATOR

        return sensorsAsString.toString();
    }


    public String executeQuery(String query, String fieldName) {

        sensorReadingsList.clear(); // reset global sensor readings
        geometryFactory = new GeometryFactory();

        StringBuilder sb = new StringBuilder();
        Connection connection = null;

        try {
            connection = Main.getDefaultStorage().getConnection();
            Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
            ResultSet results = statement.executeQuery(query);
            ResultSetMetaData metaData;    // Additional information about the results
            int numCols, numRows;          // How many rows and columns in the table
            metaData = results.getMetaData();       // Get metadata on them
            numCols = metaData.getColumnCount();    // How many columns?
            results.last();                         // Move to last row
            numRows = results.getRow();             // How many rows?

            String s;

            sb.append("# ");

            for (int col = 0; col < numCols; col++) {
                sb.append(metaData.getColumnLabel(col + 1));
                if (col < numCols - 1)
                    sb.append(SEPARATOR);
            }
            sb.append(NEWLINE);

            for (int row = 0; row < numRows; row++) {
                results.absolute(row + 1);                // Go to the specified row

                Double latitude = results.getDouble("latitude");
                Double longitude = results.getDouble("longitude");
                Double altitude = results.getDouble("altitude");

                Double value = results.getDouble(fieldName);
                String sensorName = results.getString("name");
                Long timeStamp = results.getLong("timed");

                logger.warn("longitude = " + longitude + " , latitude = " + latitude);

                Point coordinates = geometryFactory.createPoint(new Coordinate(longitude, latitude));

                SensorGeoReading sensorReadings = new SensorGeoReading(sensorName, coordinates, timeStamp, value, fieldName);
                sensorReadingsList.add(sensorReadings);
                sensorReadingsHash.put(sensorName, sensorReadings);

                //String
                logger.warn(sensorReadings);

                for (int col = 0; col < numCols; col++) {
                    Object o = results.getObject(col + 1); // Get value of the column
                    //logger.warn(row + " , "+col+" : "+ o.toString());
                    if (o == null)
                        s = "null";
                    else
                        s = o.toString();
                    if (col < numCols - 1)
                        sb.append(s).append(SEPARATOR);
                    else
                        sb.append(s);
                }
                sb.append(NEWLINE);
            }
        } catch (SQLException e) {
            sb.append("ERROR in execution of query: " + e.getMessage());
        } finally {
            Main.getDefaultStorage().close(connection);
        }

        return sb.toString();
    }

    /*
   * Builds geographic geoIndex from list of sensors currently loaded in the system
   * */

    public void buildGeoIndex() {

        geoIndex = new STRtree();
        //geometryFactory = new GeometryFactory();

        for (int i = 0; i < sensorReadingsList.size(); i++) {
            geoIndex.insert(sensorReadingsList.get(i).coordinates.getEnvelopeInternal(), sensorReadingsList.get(i).coordinates);
            //logger.warn(sensors.get(i) + " : " + coordinates.get(i) + " : " + searchForSensors_String(coordinates.get(i)));
        }
        geoIndex.build();
    }

    public List<String> getListOfSensorsWithinEnvelope(String envelope) throws ParseException {
        Geometry geom = new WKTReader().read(envelope);
        List listEnvelope = geoIndex.query(geom.getEnvelopeInternal());
        List<String> sensors = new ArrayList<String>();
        for (int i = 0; i < listEnvelope.size(); i++) {
            sensors.add(searchForSensors_String((Point) listEnvelope.get(i)));
        }
        return sensors;
    }

    /*
    * Searches for the list of sensors which are located at the given point (comma separated)
    * */
    public String searchForSensors_String(Point p) {
        StringBuilder s = new StringBuilder("");
        for (int i = 0; i < sensorReadingsList.size(); i++) {
            if (sensorReadingsList.get(i).coordinates == p) {
                return sensorReadingsList.get(i).sensorName;
            }
        }
        return "";
    }


    public class SensorGeoReading {
        public SensorGeoReading(String name, Point coords, Long ts, double v, String field) {
            sensorName = name;
            coordinates = coords;
            timestamp = ts;
            value = v;
            fieldName = field;
        }

        String sensorName;
        Point coordinates;
        Long timestamp;
        Double value;
        String fieldName;

        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("[")
                    .append(sensorName)
                    .append("] (lat:")
                    .append(coordinates.getY())
                    .append(",lon:")
                    .append(coordinates.getX())
                    .append(") @ ")
                    .append(timestamp)
                    .append(" => ")
                    .append(fieldName)
                    .append(" = ")
                    .append(value);
            return sb.toString();
        }
    }
}
