/*******************************************************************************
 * Copyright 2013-2016 alladin-IT GmbH
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package at.alladin.rmbt.controlServer;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.restlet.data.Status;
import org.restlet.resource.Get;

import at.alladin.rmbt.shared.Classification;
import at.alladin.rmbt.shared.model.SpeedItems;
import at.alladin.rmbt.shared.model.SpeedItems.SpeedItem;

import com.google.gson.Gson;


public class OpenTestResource extends ServerResource
{
    //maximum of rows sent in one single request
    public final int MAXROWS = 400;
    
    //all fields that should be displayed in a detailed request
    private final String[] openDataFieldsFull = {
    		"open_uuid", //csv 1
    		"open_test_uuid", //csv 2
    		"time", //csv 3
    		"cat_technology",  //csv 4
    		"network_type", //csv 5
    		"lat", //csv 6
    		"long", //csv 7
    		"loc_src", //csv 8
    		"loc_accuracy",
    		"public_ip_as_name",
            "zip_code", //csv 9
            "download_kbit", //csv 10
            "upload_kbit", //csv 11
            "wifi_link_speed", 
            "ping_ms", //csv 12
            "signal_strength", //csv 13
            "lte_rsrp", //csv 29
            "lte_rsrq",
            "server_name", //csv 14
            "implausible", //csv 28
            "test_duration", //csv 15
            "num_threads_requested",
            "num_threads", //csv 16
            "num_threads_ul",
            "platform", //csv 17
            "model", //csv 18
            "model_native",
            "product",     
            "client_version",  //csv 19
            "network_mcc_mnc", //csv 20
            "network_country",    
            "roaming_type",
            "network_name", //csv 21
            "sim_mcc_mnc", //csv 22           
            "sim_country",           
            "provider_name",
            "connection", //csv 23
            "asn", //csv 24
            "ip_anonym", //csv 25
            "ndt_download_kbit", //csv 26
            "ndt_upload_kbit", //csv 27
            "country_geoip",
            "country_location",
            "country_asn",
            "bytes_download",
            "bytes_upload",
            "test_if_bytes_download",
            "test_if_bytes_upload",
            "testdl_if_bytes_download",
            "testdl_if_bytes_upload",
            "testul_if_bytes_download",
            "testul_if_bytes_upload",
            "duration_download_ms",
            "duration_upload_ms",
            "time_dl_ms",
            "time_ul_ms"
    };
    
    
    //all fields that are numbers (and are formatted as numbers in json)
    private final HashSet<String> openDataNumberFields = new HashSet<>(Arrays.asList(new String[]{"time", "lat", "long", "loc_accuracy", "zip_code", "download_kbit",
        "upload_kbit","ping_ms","signal_strength","lte_rsrp","lte_rsrq","test_duration","num_threads","ndt_download_kbit","ndt_upload_kbit","asn",
        "bytes_download","bytes_upload","test_if_bytes_download","test_if_bytes_upload","testdl_if_bytes_download",
        "testdl_if_bytes_upload","testul_if_bytes_download","testul_if_bytes_upload","duration_download_ms","duration_upload_ms","time_dl_ms",
        "time_ul_ms", "roaming_type", "num_threads_ul", "num_threads_requested"
        }));
    
    //all fields that are boolean
    private final HashSet<String> openDataBooleanFields = new HashSet<>(Arrays.asList(new String[] {"implausible"}));
    
    @Get("json")
    public String request(final String entity)
    {
        addAllowOrigin();

        //routing should be in a way in which open_test_uuid is always set
        String openUUID = getRequest().getAttributes().get("open_test_uuid").toString();
        return getSingleOpenTest(openUUID);
        
    }
    
    /**
     * Gets the JSON-Representation of all open-data-fields for one specific
     * open-test-uuid
     * @param openTestUUID
     * @return the json-string
     * 
     * 
     * columns in csv ("open data")
     * 1:open_uuid,
     * 2:open_test_uuid,
     * 3:time,
     * 4:cat_technology,
     * 5:network_type,
     * 6:lat,
     * 7:long,
     * 8:loc_src,
     * 9:zip_code,
     * 10:download_kbit,
     * 11:upload_kbit,
     * 12:ping_ms,
     * 13:signal_strength,
     * 14:server_name,
     * 15:test_duration,
     * 16:num_threads,
     * 17:platform,
     * 18:model,
     * 19:client_version,
     * 20:network_mcc_mnc,
     * 21:network_name,
     * 22:sim_mcc_mnc,
     * 23:connection,
     * 24:asn,
     * 25:ip_anonym,
     * 26:ndt_download_kbit,
     * 27:ndt_upload_kbit,
     * 28:implausible,
     * 29:lte_rsrp
     * 
     * 
     * Columns in test table
     *   uid (internal)
     *   uuid (private)
     *   client_id
     *   client_version
     *   client_name
     *   client_language (private)
     *   token (private, obsolete)
     *   server_id
     *   port
     *   use_ssl *
     *   time
     *   speed_upload
     *   speed_download
     *   ping_shortest
     *   encryption *
     *   client_public_ip (private)
     *   plattform 
     *   os_version (internal)
     *   api_level (internal)
     *   device
     *   model
     *   product
     *   phone_type (internal)
     *   data_state (internal)
     *   network_country (internal)
     *   network_operator
     *   network_operator_name
     *   network_sim_country (internal)
     *   network_sim_operator
     *   network_sim_operator_name
     *   wifi_ssid (private)
     *   wifi_bssid (private)
     *   wifi_network_id (private)
     *   duration
     *   num_threads
     *   status
     *   timezone (private)
     *   bytes_download
     *   bytes_upload
     *   nsec_download
     *   nsec_upload
     *   server_ip
     *   client_software_version
     *   geo_lat
     *   geo_long
     *   network_type
     *   location
     *   signal_strength
     *   software_revision
     *   client_test_counter
     *   nat_type
     *   client_previous_test_status
     *   public_ip_asn
     *   speed_upload_log
     *   speed_download_log
     *   total_bytes_download
     *   total_bytes_upload
     *   wifi_link_speed
     *   public_ip_rdns
     *   public_ip_as_name
     *   test_slot
     *   provider_id
     *   network_is_roaming (internal)
     *   ping_shortest_log
     *   run_ndt (internal)
     *   num_threads_requested
     *   client_public_ip_anonymized
     *   zip_code
     *   geo_provider
     *   geo_accuracy
     *   deleted (internal)
     *   comment (internal)
     *   open_uuid
     *   client_time (internal)
     *   zip_code_geo (internal)
     *   mobile_provider_id
     *   roaming_type
     *   open_test_uuid
     *   country_asn
     *   country_location
     *   test_if_bytes_download
     *   test_if_bytes_upload
     *   implausible
     *   testdl_if_bytes_download
     *   testdl_if_bytes_upload
     *   testul_if_bytes_download
     *   testul_if_bytes_upload
     *   country_geoip
     *   location_max_distance
     *   location_max_distance_gps
     *   network_group_name
     *   network_group_type
     *   time_dl_ns
     *   time_ul_ns
     *   num_threads_ul 
     *   lte_rsrp
     *   lte_rsrq
     *   mobile_network_id
     *   mobile_sim_id
     *   dist_prev
     *   speed_prev
     *   tag
     *   ping_median
     *   ping_median_log
     *   client_ip_local_type (private)
     *
     *   private: visible to user only (not public)
     *   internal: not visible (neither user nor public)
     * 
     */
    
    private String getSingleOpenTest(String openTestUUID) {       
        final String sql = "SELECT t.uid as test_uid, " +
                " ('P' || t.open_uuid) open_uuid," +  //csv 1:open_uuid, UUID prefixed with 'P'
                " ('O' || t.open_test_uuid) open_test_uuid," + //csv  open_test_uuid, UUID prefixed with 'O'
                " to_char(t.time AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') \"time\"," +
                " t.time full_time," + //csv: 3:time server time-stamp of start of measurement
                " t.client_time client_time," +  //(internal) client time-stamp of start of measure
                " t.network_group_name cat_technology," + //csv 4:cat_technology
                " t.network_group_type network_type," + //csv 5:network_type
                /*
                " t.geo_lat lat," + //csv 6:lat
                " t.geo_long long," + // csv 7:long
                " t.geo_provider loc_src," + //csv 8:loc_src android: 'gps'/'network'; browser/iOS: '' (empty string)
                " t.geo_accuracy loc_accuracy, " + //accuracy of geo location in m
                */
                //csv 6:lat
                " (CASE WHEN (t.geo_accuracy < ?) AND (t.geo_provider != 'manual') AND (t.geo_provider != 'geocoder') THEN" +
                " t.geo_lat" +
                " WHEN (t.geo_accuracy < ?) THEN" +
                " ROUND(t.geo_lat*1111)/1111" + // approx 100m
                " ELSE null" +
                " END) lat," +
                // csv 7:long
                " (CASE WHEN (t.geo_accuracy < ?) AND (t.geo_provider != 'manual') AND (t.geo_provider != 'geocoder') THEN" +
                " t.geo_long" +
                " WHEN (t.geo_accuracy < ?) THEN" +
                " ROUND(t.geo_long*741)/741 " + //approx 100m
                " ELSE null" +
                " END) long," +
                // csv 8:loc_src android: 'gps'/'network'; browser/iOS: '' (empty string)
                " (CASE WHEN ((t.geo_provider = 'manual') OR (t.geo_provider = 'geocoder')) THEN" +
                " 'rastered'" + //make raster transparent
                " ELSE t.geo_provider" +
                " END) loc_src," +
                // accuracy of geo location in m
                " (CASE WHEN (t.geo_accuracy < ?) AND (t.geo_provider != 'manual') AND (t.geo_provider != 'geocoder') " +
                " THEN t.geo_accuracy " +
                " WHEN (t.geo_accuracy < 100) AND ((t.geo_provider = 'manual') OR (t.geo_provider = 'geocoder')) THEN 100" + // limit accuracy to 100m
                " WHEN (t.geo_accuracy < ?) THEN t.geo_accuracy" +
                " ELSE null END) loc_accuracy, " +
                //csv 9:zip-code - defined as integer in data base, only meaningful for measurements in Austria
                " (CASE WHEN (t.zip_code < 1000 OR t.zip_code > 9999) THEN null ELSE t.zip_code END) zip_code," +
                // " zip_code_geo," + //(internal) zip-code, integer derived from geo_location, Austria only
                " t.speed_download download_kbit," + //csv 10:download_kbit
                " t.speed_upload upload_kbit," + //csv 11: upload_kbit
                " t.wifi_link_speed," + // nominal speed of wifi-link in mbit/s , Android-only
                " (t.ping_median::float / 1000000) ping_ms," + //median ping-time in ms (stored in ns in data base)
                " signal_strength," + //csv 13:signal_strength RSSI, mainly GSM/UMTS and Wifi, Android only, in dBm
                " lte_rsrp," + // csv 29: signal_strength RSRP, Android only, in dBm
                " lte_rsrq," + // signal quality RSRQ, Android only, in dB
                " ts.name server_name," + //csv 14:server_name, name of the test server used for download/upload (not applicable for JStest)
                " implausible, " +  //csv 28:implausible, measurement not shown in map nor used in statistics, normally not visible
                " public_ip_as_name, " + //name of AS (not number)
                " duration test_duration," +  //csv 15:test_duration, nominal duration of downlink and uplink throughput tests in seconds
                " num_threads_requested," + // number of threads requested by control-server
                " num_threads," + //csv 16:num_threads, number of threads used in downlink throughput test (uplink may differ)
                " num_threads_ul," + // number of threads used in uplink test
                " COALESCE(t.plattform, t.client_name) as platform," + //csv 17:platform; currently used: 'CLI'/'Android'/Applet/iOS/[from client_name: RMBTws, RMBTjs](null); (null) is used for RMBTjs 
                " COALESCE(adm.fullname, t.model) model," + //csv 18:model, translated t.model (model_native) to human readable form
                " t.model model_native," + //device used for test; Android API 'model'; iOS:'product'; Browser: Browser-name (zB Firefox)
                " t.product product," +  // product used for test; Android APO 'product'; iOS: '' (not used); Browser: same as for model (browser name)
                " t.client_software_version client_version," + //csv 19:client_version, SW-version of client software (not RMBT-client-version), eg. '1.3'
                " t.network_operator network_mcc_mnc," + //csv 20:network_mcc_mnc, mobile country and network code of current network (Android only), string, eg "232-12'
                " network_country," + //(internal) Android-only, country code derived by client from mobile network code
                // " network_is_roaming," + //(internal) roaming-status of mobile network, boolean or (null); Android-only (obsolete)
                " roaming_type," + //roaming-status of mobile network, integer: 0:not roaming,1:national,2:international,(null):unknown (eg. iOS)
                " t.network_operator_name network_name," + //csv 21:network_name, name of current mobile network as displayed on device (eg: '3likeHome')
                " t.network_sim_operator sim_mcc_mnc," + //csv 22:sim_mcc_mnc, home network of SIM (initial 5 digits from IMSI), eg '232-01'
                " t.network_sim_country sim_country," + //(internal) Android-only, country derived by client from SIM (country of home network)
                " COALESCE(mprov.name,msim.shortname,msim.name,prov.name) provider_name," +  //pre-defined list of providers (based on provider_id) //TODO replace provider
                " t.nat_type \"connection\"," + //csv 23:connection, translation-mechanism in NAT, eg. nat_local_to_public_ipv4
                " t.public_ip_asn asn," + //csv 24:asn, AS (autonomous system) number, number of public IP network
                " t.client_public_ip_anonymized ip_anonym," +  //csv 25:ip_anonym, anonymized IP of client (IPv4: 8 bits removed, IPv6: 72 bits removed)
                " (ndt.s2cspd*1000)::int ndt_download_kbit," + //csv 26:ndt_download_kbit, result of NDT downlink throughput test kbit/s
                " (ndt.c2sspd*1000)::int ndt_upload_kbit," + //csv 27 ndt_uoload_kbit, result of NDT uplink throughput test in kbit/s
                " country_geoip," + // country-code derived from public IP-address, eg. 'AT'
                " country_location," + // country-code derived from geo_location, eg. 'DE'
                " country_asn," + // country_code derived from AS, eg. 'EU'
                " bytes_download," + // number of bytes downloaded during test (download and upload) (obsolete)
                " bytes_upload," + // number of bytes uploaded during test (download and upload) (obsolete)
                " test_if_bytes_download," + //downloaded bytes on interface during total test (inc. training, ping, without NDT) (obsolete)
                " test_if_bytes_upload," + //uploaded bytes on interface during total test (inc. training, ping, without NDT) (obsolete)
                " testdl_if_bytes_download," + //downloaded bytes on interface during download-test (without training-seq)
                " testdl_if_bytes_upload," + //uploaded bytes on interface during download-test (without training-seq)
                " testul_if_bytes_download," + //downloaded bytes on interface during upload-test (without training-seq)
                " testul_if_bytes_upload," + //downloaded bytes on interface during upload-test (without training-seq)
                " (t.nsec_download::float / 1000000) duration_download_ms," + //duration of download-test in ms
                " (t.nsec_upload::float / 1000000) duration_upload_ms," + //duration of upload-test in ms
                " (t.time_dl_ns::float / 1000000) time_dl_ms," + //relative start time of download-test in ms (ignoring training-phase)
                " (t.time_ul_ns::float / 1000000) time_ul_ms," + //relative start time of download-test in ms (ignoring training-phase)
                // " phone_type" + //(internal) radio type of phone: 0 no mobile radio, 1 GSM (incl. UMTS,LTE) 2 CDMA (obsolete)
                " speed.items speed_items" + // json representation of individual up+down speed items

                " FROM test t" +
                " LEFT JOIN device_map adm ON adm.codename=t.model" +
                " LEFT JOIN test_server ts ON ts.uid=t.server_id" +
                " LEFT JOIN test_ndt ndt ON t.uid=ndt.test_id" +
                " LEFT JOIN provider prov ON t.provider_id=prov.uid" +
                " LEFT JOIN provider mprov ON t.mobile_provider_id=mprov.uid" +
                " LEFT JOIN mccmnc2name msim ON t.mobile_sim_id=msim.uid" +
                " LEFT JOIN speed ON speed.open_test_uuid=t.open_test_uuid" +
                " WHERE " +
                " t.deleted = false " +
                " AND t.status = 'FINISHED' " +
                " AND t.open_test_uuid = ? ";
        

        //System.out.println(sql);
        
        
        PreparedStatement ps = null;
        ResultSet rs = null;
        final JSONObject response = new JSONObject();
        try
        {
            ps = conn.prepareStatement(sql);
            
            //insert filter for accuracy
            double accuracy = Double.parseDouble(settings.getString("RMBT_GEO_ACCURACY_DETAIL_LIMIT"));
            ps.setDouble(1, accuracy);
            ps.setDouble(2, accuracy);
            ps.setDouble(3, accuracy);
            ps.setDouble(4, accuracy);
            ps.setDouble(5, accuracy);
            ps.setDouble(6, accuracy);

            
            //openTestIDs are starting with "O"
            if (openTestUUID != null && openTestUUID.startsWith("O")) {
                openTestUUID = openTestUUID.substring(1);
            }
            ps.setObject(7, openTestUUID,Types.OTHER);
            
            if (!ps.execute())
                return null;
            rs = ps.getResultSet();
            
            
            if (rs.next())
            {
                //fetch data for every field
                for (int i=0;i<openDataFieldsFull.length;i++) {
                    
                    //convert data to correct json response
                    final Object obj = rs.getObject(openDataFieldsFull[i]);
                    if (openDataBooleanFields.contains(openDataFieldsFull[i])) {
                    	if (obj == null) {
                    		response.put(openDataFieldsFull[i], false);
                    	} 
                    	else {
                    		response.put(openDataFieldsFull[i], obj);
                    	}
                    }
                    else if (obj==null) {
                        response.put(openDataFieldsFull[i], JSONObject.NULL);
                    } 
                    else if (openDataNumberFields.contains(openDataFieldsFull[i])) {
                    	final String tmp = obj.toString().trim();
                    	if (tmp.isEmpty()) 
                    		response.put(openDataFieldsFull[i], JSONObject.NULL);
                    	else
                    		response.put(openDataFieldsFull[i], JSONObject.stringToValue(tmp));
                    }  
                    else {
                    	final String tmp = obj.toString().trim();
						if (tmp.isEmpty())
							response.put(openDataFieldsFull[i], JSONObject.NULL);
						else
							response.put(openDataFieldsFull[i], tmp);
                    }
                    
                }
                /* obsolete (now in database)
                //special threatment for lat/lng: if too low accuracy -> do not send back to client
                double accuracy = rs.getDouble("loc_accuracy");
                if (accuracy > Double.parseDouble(settings.getString("RMBT_GEO_ACCURACY_DETAIL_LIMIT"))) {
                	response.put("loc_accuracy", JSONObject.NULL);
                	response.put("long", JSONObject.NULL);
                	response.put("lat", JSONObject.NULL);
                }
                
                try {
                // do not output invalid zip-codes, must be 4 digits 
                int zip_code = rs.getInt("zip_code");
                if (zip_code <= 999 || zip_code > 9999)
                	response.put("zip_code", JSONObject.NULL);
                }
                catch (final SQLException e) {
                	System.out.println("Error on zip_code: " + e.toString());
                };
                */
                
                //classify download, upload, ping, signal
                response.put("download_classification", Classification.classify(Classification.THRESHOLD_DOWNLOAD, rs.getLong("download_kbit"), capabilities.getClassificationCapability().getCount()));
                response.put("upload_classification", Classification.classify(Classification.THRESHOLD_UPLOAD, rs.getLong("upload_kbit"), capabilities.getClassificationCapability().getCount()));
                response.put("ping_classification", Classification.classify(Classification.THRESHOLD_PING, rs.getLong("ping_ms")*1000000, capabilities.getClassificationCapability().getCount()));
                //classify signal accordingly
				if ((rs.getString("signal_strength") != null || rs
						.getString("lte_rsrp") != null)
						&& rs.getString("network_type") != null) { // signal available
					if (rs.getString("lte_rsrp") == null) { // use RSSI
						if (rs.getString("network_type").equals("WLAN")) { // RSSI for Wifi
							response.put(
									"signal_classification",
									Classification
											.classify(
													Classification.THRESHOLD_SIGNAL_WIFI,
													rs.getLong("signal_strength"),capabilities.getClassificationCapability().getCount()));
						} else { // RSSI for Mobile
							response.put(
									"signal_classification",
									Classification
											.classify(
													Classification.THRESHOLD_SIGNAL_MOBILE,
													rs.getLong("signal_strength"),capabilities.getClassificationCapability().getCount()));
						}
					} else // RSRP for LTE
						response.put("signal_classification", Classification
								.classify(Classification.THRESHOLD_SIGNAL_RSRP,
										rs.getLong("lte_rsrp"), capabilities.getClassificationCapability().getCount()));
				} else { // no signal available
					response.put("signal_classification", JSONObject.NULL);
				}
                
                
                //also load download/upload-speed-data, signal data and location data if possible
                JSONObject speedCurve = new JSONObject();
                JSONArray downloadSpeeds = new JSONArray();
                JSONArray uploadSpeeds = new JSONArray();
                JSONArray locArray = new JSONArray();
                JSONArray signalArray = new JSONArray();
                
                
                
                // speed data
                final Gson gson = getGson(false);
                final SpeedItems speedItems = gson.fromJson(rs.getString("speed_items"), SpeedItems.class);
                
                if (speedItems != null)
                {
                    long lastTime = -1;
                    for (SpeedItem item : speedItems.getAccumulatedSpeedItemsUpload()) {
                    	JSONObject obj = new JSONObject();
                    	final long time = Math.round((double)item.getTime() / 1000000);
                    	if (time == lastTime)
                    	    continue;
                        obj.put("time_elapsed", time);
                    	obj.put("bytes_total", item.getBytes());
                    	uploadSpeeds.put(obj);
                    	lastTime = time;
                    }
                    lastTime = -1;
                    for (SpeedItem item : speedItems.getAccumulatedSpeedItemsDownload()) {
                    	JSONObject obj = new JSONObject();
                    	final long time = Math.round((double)item.getTime() / 1000000);
                    	if (time == lastTime)
                            continue;
                    	obj.put("time_elapsed", time);
                    	obj.put("bytes_total", item.getBytes());
                    	downloadSpeeds.put(obj);
                    	lastTime = time;
                    }
                }
                
                
                //Load signal strength from database
                SignalGraph sigGraph = new SignalGraph(rs.getLong("test_uid"), rs.getTimestamp("client_time").getTime(), conn);
                for (SignalGraph.SignalGraphItem item : sigGraph.getSignalList()) {
                	JSONObject json = new JSONObject();
                	json.put("time_elapsed",item.getTimeElapsed());
                	json.put("network_type", item.getNetworkType());
                 	json.put("signal_strength", item.getSignalStrength()); 
                	json.put("lte_rsrp", item.getLteRsrp());
                 	json.put("lte_rsrq", item.getLteRsrq());     
                 	json.put("cat_technology", item.getCatTechnology());
                	signalArray.put(json);
                }
                          
                
                //Load gps coordinates from database
                LocationGraph locGraph = new LocationGraph(rs.getLong("test_uid"),  rs.getTimestamp("client_time").getTime(), conn);
                double totalDistance = locGraph.getTotalDistance();
                for (LocationGraph.LocationGraphItem item : locGraph.getLocations()) {
                	JSONObject json = new JSONObject();
                	json.put("time_elapsed",item.getTimeElapsed());
                	json.put("lat", item.getLatitude());
                	json.put("long", item.getLongitude());
                	json.put("loc_accuracy", (item.getAccuracy() > 0) ? item.getAccuracy() : JSONObject.NULL );
                	locArray.put(json);
                }
                

                
                speedCurve.put("upload", uploadSpeeds);
                speedCurve.put("download", downloadSpeeds);
                speedCurve.put("signal", signalArray);
                speedCurve.put("location", locArray);
                response.put("speed_curve", speedCurve);
                //add total distance during test - but only if within bounds
                if ((totalDistance > 0) &&
                        totalDistance <= Double.parseDouble(settings.getString("RMBT_GEO_DISTANCE_DETAIL_LIMIT")))
                    response.put("distance", totalDistance);
                else
                    response.put("distance", JSONObject.NULL);
                
            } else {
                //invalid open_uuid
                setStatus(Status.CLIENT_ERROR_NOT_FOUND);
                response.put("error","invalid open-uuid");
            }
        }
        catch (final JSONException e) {
            Logger.getLogger(OpenTestResource.class.getName()).log(Level.SEVERE, null, e);
        } catch (SQLException ex) {
            try {
                setStatus(Status.CLIENT_ERROR_NOT_FOUND);
                response.put("error","invalid open-uuid");
            } catch (JSONException ex1) {
                Logger.getLogger(OpenTestResource.class.getName()).log(Level.SEVERE, null, ex1);
            }
            Logger.getLogger(OpenTestResource.class.getName()).log(Level.SEVERE, null, ex);
        }
        finally
        {
            try
            {
                if (rs != null)
                    rs.close();
                if (ps != null)
                    ps.close();
            }
            catch (final SQLException e)
            {
                Logger.getLogger(OpenTestResource.class.getName()).log(Level.SEVERE, null, e);
            }
        }

        return response.toString();
    }
        
    /**
     * Calculate the rough distance in meters between two points
     * taken from http://stackoverflow.com/questions/120283/working-with-latitude-longitude-values-in-java
     * @param lat1 
     * @param lng1
     * @param lat2
     * @param lng2
     * @return
     */
    private static double distFrom(double lat1, double lng1, double lat2, double lng2) {
        double earthRadius =  6371000;
        double dLat = Math.toRadians(lat2-lat1);
        double dLng = Math.toRadians(lng2-lng1);
        double sindLat = Math.sin(dLat / 2);
        double sindLng = Math.sin(dLng / 2);
        double a = Math.pow(sindLat, 2) + Math.pow(sindLng, 2)
                * Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2));
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        double dist = earthRadius * c;

        return dist;
    }
    
    public static class LocationGraph {
    	private double totalDistance;
    	private ArrayList<LocationGraphItem> locations = new ArrayList<>();
    	
    	/**
    	 * Gets all distinctive locations of a client during a test
    	 * @param testUID the uid of the test
    	 * @param testTime the begin of the test
    	 * @throws SQLException
    	 */
    	public LocationGraph(long testUID, long testTime, java.sql.Connection conn) throws SQLException {
    		PreparedStatement psLocation = conn.prepareStatement("SELECT test_id, g.geo_lat lat, g.geo_long long, g.accuracy loc_accuracy, time "
            		+ "FROM geo_location g "
            		+ "WHERE g.test_id = ? and provider != 'network' " //do not mix with network-locations  (iOS: provider = '')
            		+ "ORDER BY time;");
            psLocation.setLong(1, testUID);
            ResultSet rsLocation = psLocation.executeQuery();
            
            boolean first = true;
            boolean usedCurrentItem = false;
            LocationGraphItem item = null;
            
            double lastLat=0;
            double lastLong=0;
            double lastAcc=0;
            this.totalDistance=0;
            while (rsLocation.next()) {
            	long timeElapsed = rsLocation.getTimestamp("time").getTime() - testTime;
            	//there could be measurements taken before a test started
            	//in this case, only return the last one
            	if (first && timeElapsed > 0 && item != null) {
            		this.locations.add(item);
            		lastLat = item.getLatitude();
    				lastLong = item.getLongitude();
    				lastAcc = item.getAccuracy();
            		first = false;
            	}
            	
            	
            	item = new LocationGraphItem(Math.max(timeElapsed,0), rsLocation.getDouble("long"), rsLocation.getDouble("lat"),  rsLocation.getDouble("loc_accuracy"));
            	usedCurrentItem = false;
            	
            	//put triplet in the array if it is not the first one
            	if (!first) {
            		//only put the point in the resulting array, if there is a significant
            		//distance from the last point
            		//therefore (difference in m) > (tolerance last point + tolerance new point)
            		double diff = OpenTestResource.distFrom(lastLat, lastLong, item.getLatitude(), item.getLongitude());
            		//System.out.println("dist: " + diff);
            		double maxDiff = item.getAccuracy() + lastAcc;
            		//System.out.println("Distance: " + diff + "; tolTotal " + maxDiff + "; tol1 " + lastAcc + "; tol2 " + json.getDouble("loc_accuracy"));
            		if (diff > maxDiff) {
            			this.locations.add(item);
                		lastLat = item.getLatitude();
        				lastLong = item.getLongitude();
        				lastAcc = item.getAccuracy();
        				this.totalDistance += diff;
            		}
            		else {
            			//if not, replace the old point, if the new is more accurate
            			if (item.getAccuracy() < lastAcc) {
            				this.locations.remove(this.locations.size()-1);
            				this.locations.add(item);
                    		lastLat = item.getLatitude();
            				lastLong = item.getLongitude();
            				lastAcc = item.getAccuracy();
            			}
            		}
            		
            		usedCurrentItem = true;
            	}
            }
            
            //always use the last item to connect the path with the end point
            if (!usedCurrentItem && this.locations.size() > 0) {
            	//replace the last set point with it
            	//since it is in the same inaccurracy area
            	
            	this.locations.remove(this.locations.size()-1);
				this.locations.add(item);
        		lastLat = item.getLatitude();
				lastLong = item.getLongitude();
				lastAcc = item.getAccuracy();
            	
            }
            
            
            //System.out.println("called w id: " + testUID + ", ct: " + testTime + ", d: " + this.totalDistance + ", " + this.getTotalDistance());
            
            rsLocation.close();
            psLocation.close();
    	}
    	
    	public double getTotalDistance() {
    		return this.totalDistance;
    	}
    	
    	public ArrayList<LocationGraphItem> getLocations() {
    		return this.locations;
    	}
    	
    	private class LocationGraphItem {
    		private double longitude;
    		private double latitude;
    		private double accuracy;
    		private long timeElapsed;
    		
    		public LocationGraphItem(long timeElapsed, double longitude, double latitude, double accuracy) {
    			this.longitude = longitude;
    			this.latitude = latitude;
    			this.timeElapsed = timeElapsed;
    			this.accuracy = accuracy;
    		}
    		
    		/**
    		 * @return The time elapsed since the begin of the test
    		 */
    		public long getTimeElapsed() {
    			return this.timeElapsed;
    		}
    		
    		public double getLongitude() {
    			return this.longitude;
    		}
    		
    		public double getLatitude() {
    			return this.latitude;
    		}
    		
    		/**
    		 * @return The accuracy of the measurement in meters
    		 */
    		public double getAccuracy() {
    			return this.accuracy;
    		}
    	}
    }

    public static class SignalGraph {
    	private static int LOWER_BOUND = -1500;
    	private static int MAX_TIME = 60000;
    	
    	private ArrayList<SignalGraphItem> signalList = new ArrayList<>();
    	
    	/**
    	 * Gets all signal measurements from a test
    	 * @param testUID the test uid
    	 * @param testTime the begin of the test
    	 * @throws SQLException
    	 */
    	public SignalGraph(long testUID, long testTime, java.sql.Connection conn) throws SQLException {
    		PreparedStatement psSignal = conn.prepareStatement("SELECT test_id, nt.name network_type, nt.group_name cat_technology, signal_strength, lte_rsrp, lte_rsrq, wifi_rssi, time "
            		+ "FROM signal "
            		+ "JOIN network_type nt "
            		+ "ON nt.uid = network_type_id "
            		+ "WHERE test_id = ? "
            		+ "ORDER BY time;");
            psSignal.setLong(1, testUID);
            
            ResultSet rsSignal = psSignal.executeQuery();
            
            boolean first = true;
            SignalGraphItem item = null;
            while (rsSignal.next()) {
            	long timeElapsed = rsSignal.getTimestamp("time").getTime() - testTime;
            	//there could be measurements taken before a test started
            	//in this case, only return the last one
            	if (first && timeElapsed > 0 && item != null) {
            		this.signalList.add(item);
            		first = false;
            	}
            	
            	//ignore measurements after a threshold of one minute
            	if (timeElapsed > MAX_TIME) 
            		break;
            	
            	
            	int signalStrength = rsSignal.getInt("signal_strength");
            	int lteRsrp = rsSignal.getInt("lte_rsrp");
            	int lteRsrq = rsSignal.getInt("lte_rsrq");
            	if (signalStrength == 0)
            		signalStrength = rsSignal.getInt("wifi_rssi");
            	
            	if (signalStrength > LOWER_BOUND)
            		item = new SignalGraphItem(Math.max(timeElapsed,0), rsSignal.getString("network_type"), signalStrength, lteRsrp, lteRsrq, rsSignal.getString("cat_technology"));
            	
            	
            	//put 5-let in the array if it is not the first one
            	if (!first || rsSignal.isLast()) {
            		if (timeElapsed < 0) {
            			item.timeElapsed = 1000;
            		}
            		this.signalList.add(item);
            	}
            }
            
            rsSignal.close();
            psSignal.close();
    	}
    	
    	public ArrayList<SignalGraphItem>getSignalList() {
    		return this.signalList;
    	}
    	
    	private class SignalGraphItem {
    		private long timeElapsed;
    		private String networkType;
    		private int signalStrength;
    		private int lteRsrp;
    		private int lteRsrq;
    		private String catTechnology;
    		
    		public SignalGraphItem(long timeElapsed, String networkType, int signalStrength, int lteRsrp, int lteRsrq, String catTechnology) {
    			this.timeElapsed = timeElapsed;
    			this.networkType = networkType;
    			this.signalStrength = signalStrength;
    			this.lteRsrp = lteRsrp;
    			this.lteRsrq = lteRsrq;
    			this.catTechnology = catTechnology;
    		}
    		
    		/**
    		 * @return The time elapsed since the begin of the test
    		 */
    		public long getTimeElapsed() {
    			return this.timeElapsed;
    		}
    		
    		/**
    		 * @return The type of the network, e.g. 
    		 */
    		public String getNetworkType() {
    			return this.networkType;
    		}

       		/**
    		 * @return The signal strength RSSI in dBm
    		 */
    		public int getSignalStrength() {
    			return this.signalStrength;
    		}

       		/**
    		 * @return The signal strength RSRP in dBm
    		 */
    		public int getLteRsrp() {
    			return this.lteRsrp;
    		}
    		
       		/**
    		 * @return The signal quality RSRQ in dB
    		 */
    		public int getLteRsrq() {
    			return this.lteRsrq;
    		}
    		
    		public String getCatTechnology() {
    			return this.catTechnology;
    		}

    	}
    }
}