package Localization;

import java.io.FileReader;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Scanner;

/*
Code for implementing Multi-Thread Server is from:
http://tutorials.jenkov.com/java-multithreaded-servers/multithreaded-server.html
*/
public class server implements Runnable
{
	protected int          serverPort 	= 9254;
	protected ServerSocket serverSocket = null;
	protected boolean      isStopped    = false;
	protected Thread       runningThread= null;
	public static boolean  preprocessed = false;
	public static boolean  multi_phone = false;
	
	public server(int port)
	{
		serverPort = port;
	}

	public void run()
	{
		synchronized(this)
		{
			this.runningThread = Thread.currentThread();
		}
		openServerSocket();
		while(!isStopped())
		{
			Socket clientSocket = null;
			try
			{
				clientSocket = this.serverSocket.accept();
			}
			catch (IOException e)
			{
				if(isStopped())
				{
					System.out.println("Server Stopped.") ;
					return;
				}
				throw new RuntimeException("Error accepting client connection", e);
			}
			new Thread(new LocalizationThread(clientSocket)).start();
		}
		System.out.println("Server Stopped.") ;
	}

	private synchronized boolean isStopped()
	{
		return this.isStopped;
	}

	public synchronized void stop()
	{
		this.isStopped = true;
		try
		{
			this.serverSocket.close();
		}
		catch (IOException e)
		{
			throw new RuntimeException("Error closing server", e);
		}
	}

	private void openServerSocket()
	{
		try
		{
			this.serverSocket = new ServerSocket(this.serverPort);
		}
		catch (IOException e)
		{
			throw new RuntimeException("Cannot open port " + this.serverPort, e);
		}
	}
	
	public static void main(String args [])
	{
		Scanner inputReader = new Scanner(System.in);
		int port = 9254;
		
		try 
		{
			// Finally, load user/password credentials
			Properties login = new Properties();
			try (FileReader in = new FileReader("./login.properties")) 
			{
			    login.load(in);
			}
			LocalizationLUT.username = login.getProperty("username");
			LocalizationLUT.password = login.getProperty("password");
			
			// Check if LUT exists
			server.preprocessed = LocalizationLUT.isProcessed();
			// If made, update vector size now!
			if(server.preprocessed)
			{
				Distance.VECTOR_SIZE = LocalizationLUT.getVectorSize(Distance.FSF);
				System.out.println("NEW VECTOR SIZE: " + Distance.VECTOR_SIZE);
			}
			
			// Custom Port if needed?
			if (args.length == 1)
			{
				port = Integer.parseInt(args[0]);
				if(port <= 1024)
				{
					System.err.println("Invalid Port! " + port + " use value over 1024!");
					System.exit(1);
				}
				else if(port > 65535)
				{
					System.err.println("Invalid Port! " + port + " use value below 65535!");
					System.exit(1);					
				}
			}
		}
		catch (ClassNotFoundException | SQLException e)
		{
			e.printStackTrace();
			System.exit(1);
		}
		catch (NumberFormatException nfe)
		{
			System.out.println("Please enter a valid custom port number");
			System.exit(1);
		}
		catch (IOException e) 
		{
			System.out.println("Missing Login file");
			System.exit(1);
		}

		server Localizationserver = new server(port);
		new Thread(Localizationserver).start();
		
		// Create the Training Table...
		if(LocalizationLUT.createTrainingTable())
		{
			System.out.println("Created Database and Training Table!");
		}
		else
		{
			System.out.println("Failed to create Training Table! It probably already exists!!!");
		}
		
		System.out.println("==================FIU Indoor Localization Server Online=========================");
		
		// Get arguments from command line...
		while(true)
		{
			try
			{
				String input = inputReader.nextLine();
				input = input.trim();
				String [] commands = input.split(" ");
				
				System.out.println("Database preprocessed? " + server.preprocessed);
				System.out.println("Vector size: " + Distance.VECTOR_SIZE);
				System.out.println("APs in Training Data: " + LocalizationLUT.getVectorSize(0.0));
				System.out.println("N_F: " + LocalizationLUT.getX("BWY_FL_03").length);
				System.out.println("Current value of K is: " + Distance.k);
				
				//Clear CLI
				if (commands[0].equalsIgnoreCase("clr"))
				{
					System.out.println("\033[2J\033[;H");
					System.out.flush();
				}
				else if (commands[0].equalsIgnoreCase("print"))
				{
					if (server.preprocessed)
					{
						LocalizationLUT.printTrainingData();
						if(server.multi_phone)
						{
							MultiphoneLocalization.printLUT();
						}
						else
						{
							LocalizationLUT.printLUT();				
						}
					}
					else
					{
						System.out.println("Lookup Tables not processed yet!");
					}
				}
				else if (commands[0].equals("frequency"))
				{
					HashMap<String, Integer> frequency_map = LocalizationLUT.getCommonMac();
					int idx = 1;
					// Get sorted
					Map<String, Integer> sorted_map = sortByComparator(frequency_map, true);
					for (String AP: frequency_map.keySet())
					{
						System.out.println(idx + ", MAC: " + AP + " was detected " + sorted_map.get(AP) + " times");
						++idx;
					}
					tukey_summary(frequency_map.values().toArray(new Integer[frequency_map.size()]));				
				}
				// Change k
				else if(commands[0].equals("k"))
				{
					Distance.k = Integer.parseInt(commands[1]);
					System.out.println("Updated value of K is: " + Distance.k);
				}
				// Test FSF
				else if (commands[0].equalsIgnoreCase("test-FSF"))
				{
					double fsf = Double.parseDouble(commands[1]);
					if (fsf < 0 || fsf > 1)
					{
						System.out.println("Invalid FSF value! " +  fsf);
						continue;
					}
					else
					{
						System.out.println("Given FSF value: " + fsf + " minimum AP match is: " + LocalizationLUT.getVectorSize(fsf));
					}
				}
				// Change FSF
				else if (commands[0].equalsIgnoreCase("FSF"))
				{
					double fsf = Double.parseDouble(commands[1]);
					if (fsf < 0 || fsf > 1)
					{
						System.out.println("Invalid FSF value! " +  fsf);
						continue;
					}
					else
					{
						Distance.FSF = fsf;
						if(server.preprocessed)
						{
							System.out.println("Please note for number of columns to change, you must re-build new Lookup table!");
						}
					}
				}
				else if(commands[0].equalsIgnoreCase("process"))
				{
					if(server.multi_phone)
					{
						if(LocalizationThread.multiprocess())
						{
							System.out.println("Preprocessing all Lookup Tables successful!");
						}
						else
						{
							System.out.println("Preprocessing all Lookup Tables failed!");
						}
					}
					else
					{
						if(LocalizationThread.process())
						{
							System.out.println("Preprocessing one Lookup Table successful!");
						}
						else
						{
							System.out.println("Preprocessing all Lookup Tables failed!");
						}
					}
				}
				else if(commands[0].equalsIgnoreCase("reset"))
				{
					if(LocalizationLUT.reset())
					{
						System.out.println("RESET SUCESSFUL!");
						server.preprocessed = false;
					}
					else
					{
						System.out.println("RESET FAILED!");
					}
				}
				else if (commands[0].equalsIgnoreCase("switch"))
				{
					if(server.multi_phone)
					{
						System.out.println("Server is switcthed to 1 LUT");
						server.multi_phone = false;
					}
					else
					{
						System.out.println("Server is switched to 1 LUT per Phone");
						server.multi_phone = true;
					}
				}
				else if (commands[0].equalsIgnoreCase("exit"))
				{
					break;
				}
			}
			catch (NumberFormatException nfe)
			{
				//nfe.printStackTrace();
				continue;
			} 
			catch (ClassNotFoundException e)
			{
				e.printStackTrace();
			}
			catch (SQLException e)
			{
				e.printStackTrace();
			}
		}
		System.out.println("Stopping Localization Server...");
		inputReader.close();
		Localizationserver.stop();
	}
	
	/*
	 * This method is to sort a HashMap by its values
	 * It will return a new Hashmap sorted either in ascending or
	 * descending order with respect to its value
	 */

	private static Map<String, Integer> sortByComparator
	(HashMap<String, Integer> unsortMap, final boolean order)
	{
		List<Entry<String, Integer>> list = 
				new LinkedList<Entry<String, Integer>>(unsortMap.entrySet());
		// Sorting the list based on values
		Collections.sort(list, new Comparator<Entry<String, Integer>>()
		{
			public int compare(Entry<String, Integer> o1,
					Entry<String, Integer> o2)
			{
				if (order)
				{
					return o1.getValue().compareTo(o2.getValue());
				}
				else
				{
					return o2.getValue().compareTo(o1.getValue());
				}
			}
		});

		// Maintaining insertion order with the help of LinkedList
		Map<String, Integer> sortedMap = new LinkedHashMap<String, Integer>();
		for (Entry<String, Integer> entry : list)
		{
			sortedMap.put(entry.getKey(), entry.getValue());
		}
		return sortedMap;
	}

	
	private static void tukey_summary(Integer [] arr)
	{
		Arrays.sort(arr);
		System.out.println("Min:          " + Percentile(arr, 0));
		System.out.println("25% quartile: " + Percentile(arr, 25));
		System.out.println("50% quartile: " + Percentile(arr, 50));
		System.out.println("75% quartile: " + Percentile(arr, 75));
		System.out.println("Max:          " + Percentile(arr, 100));
	}
	
    private static long Percentile(Integer [] latencies, double Percentile)
    {
        int Index = (int) Math.ceil((Percentile/100.0) * latencies.length);
        System.out.println("Index for " + Percentile + " is: " + Index);
        if(Index == 0)
        {
        	return latencies[0];
        }
        else
        {
        	return latencies[Index - 1];	
        }
    }
}
