package gr.uoc.csd.hy439.positioning.fingerprint;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;

import android.content.Context;
import android.location.Location;
import android.net.wifi.ScanResult;
import android.os.Environment;
import android.util.FloatMath;
import android.util.Log;

/**
 * This algorithm returns the mean (x,y) of the "nearest" K training fingerprints.
 * <p>The distance of a runtime fingerprint from the training phase ones is defined as TODO: </p>
 */
public class PercentilesAlgorithm extends FingerprintAlgorithm {	
	public static final int K= 4;

	/** 
	 * <h1>Not needed for this algorithm.</h1>
	 * <p>Format: {@code <cellX_cellY, <MAC, rssiList>>}</p>
	 * <p>Could be needed in case we want to compare the distributions of RSSI measurements between the
	 * runtime fingerprint and the training phase ones, for each AP.</p>
	 */
	private HashMap<String, HashMap<String, LinkedList<Integer>>> trainingValues;
	
	/**
	 * this HashMap is needed in order to estimate new positions
	 * <p>Format: {@code <cellX_cellY, <MAC, rssiList>>}</p>
	 */
	private HashMap<String, HashMap<String, LinkedList<Integer>>> trainingMedian;
	
	/**
	 * Contains the MAC addresses of the Access Points with which the model was trained. 
	 */
	private ArrayList<String> APs_total;	
	private Location location;
	
	PercentilesAlgorithm(Context context) {
		super(context);
		trainingValues= null; // TODO: check if this is wrong.
	}
	
	protected void onCreate() {
		// nothing to initialize yet...
	}
	
	/**
	 * Loads the model from files.
	 * <p>Restores the fields
	 * {@link PercentilesAlgorithm#trainingValues trainingValues},
	 * {@link PercentilesAlgorithm#trainingMedian trainingMedian} and
	 * {@link PercentilesAlgorithm#APs_total APs_total}
	 * from the respective files.</p>
	 */
	protected void retrieveModel() throws Exception {
		long startTime= System.currentTimeMillis();
		Log.d(this.getClass().getName(), "try loading the trainingMedian and APs_total fields from file");
	    ObjectInputStream trainingMedianObjectInputStream = new ObjectInputStream(new FileInputStream(new File(FILES_PREFIX+"trainingMedian")));
		//ObjectInputStream trainingMedianObjectInputStream = new ObjectInputStream(context.getResources().openRawResource(R.raw.fingerprint_training_median));
	    trainingMedian = (HashMap<String, HashMap<String, LinkedList<Integer>>>) trainingMedianObjectInputStream.readObject();
	    trainingMedianObjectInputStream.close();
	    ObjectInputStream APs_totalObjectInputStream = new ObjectInputStream(new FileInputStream(new File(FILES_PREFIX+"APs_total")));
	    //ObjectInputStream APs_totalObjectInputStream = new ObjectInputStream(context.getResources().openRawResource(R.raw.fingerprint_aps_total));
	    APs_total = (ArrayList<String>) APs_totalObjectInputStream.readObject();
	    APs_totalObjectInputStream.close();
	    Log.d(this.getClass().getName(), "Read duration: "+(System.currentTimeMillis()-startTime)+"ms");		    
		if (trainingMedian.isEmpty())
	    	throw new IOException("trainingMedian is empty. It was not loaded from the file.");
	}
	
	/**
	 *  Trains the Model
	 *  <p>Reads raw measurement files with {@code <timestamp, BSSID, RSSI>} lines, and populates the fields
	 *  {@link PercentilesAlgorithm#trainingValues trainingValues},
	 *  {@link PercentilesAlgorithm#trainingMedian trainingMedian} and
	 *  {@link PercentilesAlgorithm#APs_total APs_total}</p>
	 */
	protected void trainModel() {
		long tic= System.currentTimeMillis();
		trainingValuesCalculation();
		trainingMedianCalculation();
		convertTrainingMedianToWGS84();
		Log.d(this.getClass().getName(), "Training duration: "+(System.currentTimeMillis()-tic)+"ms");
	}
	
	/**
	 * Part of the training procedure.
	 * <p>Reads raw measurement files with {@code <timestamp, BSSID, RSSI>} lines, and populates the fields
	 * {@link PercentilesAlgorithm#trainingValues trainingValues} and
	 * {@link PercentilesAlgorithm#APs_total APs_total}.
	 */
	private void trainingValuesCalculation() {
		trainingValues = new HashMap<String, HashMap<String, LinkedList<Integer>>>();
		APs_total = new ArrayList<String>();
		Log.i(this.getClass().getName(), FILES_PREFIX+TRAINING_DATA_DIR_NAME);
		File folder = new File(FILES_PREFIX+TRAINING_DATA_DIR_NAME);
		File[] listOfFiles = folder.listFiles();
		BufferedReader in;
		//int numOfFiles = 0;
		for (File file : listOfFiles) {
			trainingValues.put(file.getName(), new HashMap<String, LinkedList<Integer>>());
			ArrayList<String> APs = new ArrayList<String>();
			// Log.d(" file!!", file.getPath());
			if (/* file.isDirectory() */true) {
				// reading the APs now
				//numOfFiles++;
				try {
					// Log.e("PercentilesAlgorithm", "File count" + numOfFiles);
					in = new BufferedReader(new FileReader(file.getPath()));
					String line;
					String macStr, rssiStr;
					int numOfAPs = 0, index = 0;
					while ((line = in.readLine()) != null) {// checking and save according to appropriate AP
						// Create HashMapB trainingValues
						String s[] = line.split(" ");
						macStr = s[1];// for the MAC address
						rssiStr = s[2];
						// Prwti fora vriskei auti ti MAC
						if (!APs.contains(macStr)) {
							APs.add(macStr);
							if (!APs_total.contains(macStr)) {
								APs_total.add(macStr);
							}
							// Dimiourgia domis LinkedList<Integer> kai prosthesi timis RSSI
							trainingValues.get(file.getName()).put(APs.get(numOfAPs).toString(),new LinkedList<Integer>());
							trainingValues.get(file.getName()).get(APs.get(numOfAPs)).add(Integer.parseInt(rssiStr));
							numOfAPs++;
						} else {
							index = APs.indexOf(macStr);
							trainingValues.get(file.getName()).get(APs.get(index)).add(Integer.parseInt(rssiStr));
						}
					}
					in.close();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}// end of for listofFile
	}
	
	/**
	 * Part of the training procedure.
	 * <p>Reads the {@link PercentilesAlgorithm#trainingValues trainingValues} field and populates
	 * the {@link PercentilesAlgorithm#trainingMedian trainingMedian}.</p>
	 */
	private void trainingMedianCalculation() {
		trainingMedian = new HashMap<String, HashMap<String, LinkedList<Integer>>>();
		Object[] keys = trainingValues.keySet().toArray();
		int keysSize = keys.length;
		int sizeTrain = 0/*, per = 0, num = 0, tmp, next_tmp, numOfFiles = 0*/;
		//float R = 0, perNum = 0;
		int indexOfMedian = 0, median = 0;
		for (int i = 0; i < keysSize; i++) {
			trainingMedian.put(keys[i].toString(), new HashMap<String, LinkedList<Integer>>());
			//Object[] keysAPs = trainingValues.get(keys[i]).keySet().toArray();
			for (int j = 0; j < APs_total.size(); j++) {
				trainingMedian.get(keys[i]).put(APs_total.get(j).toString(), new LinkedList<Integer>());
				// Log.d("PercentilesAlgorithm", "check if create");
				// Log.d("PercentilesAlgorithm","trainingValues.get(keys[i]).get(APs_total.get(j)) ="+trainingValues.get(keys[i]).get(APs_total.get(j)));
				if (trainingValues.get(keys[i]).get(APs_total.get(j)) == null) {
					trainingValues.get(keys[i]).put(APs_total.get(j).toString(), new LinkedList<Integer>());
					trainingValues.get(keys[i]).get(APs_total.get(j)).add(-100);
					// Log.e("PercentilesAlgorithm", "Mpike mia fora na valei -100");
				}
				sizeTrain = trainingValues.get(keys[i]).get(APs_total.get(j)).size();
				// Log.d("PercentilesAlgorithm","Size ="+sizeTrain);
				if (sizeTrain > 0) {
					Collections.sort(trainingValues.get(keys[i]).get(APs_total.get(j)));
					indexOfMedian = Math.round(sizeTrain / 2);
					median = trainingValues.get(keys[i]).get(APs_total.get(j)).get(indexOfMedian);
					// Log.d("PercentilesAlgorithm","Median ="+median);
					trainingMedian.get(keys[i]).get(APs_total.get(j)).add(median);
				} else {
					// trainingMedian.get(keys[i]).put(APs_total.get(j)), new )
					int t = -100;
					trainingMedian.get(keys[i]).get(APs_total.get(j)).add(t);
				}
				// Log.d("PercentilesAlgorithm","Median ="+trainingMedian.get(keys[i]).get(APs_total.get(j)));
			}
		}
	}
	
	/**
	 * Part of the training procedure.
	 * <p>Converts the {@link PercentilesAlgorithm#trainingMedian trainingMedian} keys
	 * from cellX_cellY.txt to latitude_longitude format.</p>
	 */
	private void convertTrainingMedianToWGS84() {
		//String key=trainingMedian.keySet().iterator().next();
		//Log.i(this.getClass().getName(), "Before conversion: "+key+" -> "+trainingMedian.get(key).keySet().iterator().next()+" -> ...");
		String strLine;
        String[] item;
        String cellId;
        String cellCoords;
        HashMap<String, LinkedList<Integer>> value;
		try {
			BufferedReader br= new BufferedReader(new InputStreamReader(new FileInputStream(new File(FILES_PREFIX+XY_TO_LATLON))));
			while ((strLine = br.readLine()) != null) {
				item = strLine.split("	");
				cellId= item[0]+"_"+item[1];
				cellCoords= item[2]+"_"+item[3];
				value= trainingMedian.get(cellId);
				if (value != null) {
					trainingMedian.put(cellCoords, value);
					trainingMedian.remove(cellId);
					/*
					try {
						Log.i(this.getClass().getName(), "After conversion: "+cellCoords+" -> "+trainingMedian.get(cellCoords).keySet().iterator().next()+" -> ...");
					} catch (NullPointerException npe) {
						Log.i(this.getClass().getName(), "After conversion: "+cellCoords);
					}
					*/
				}
			}
			br.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Saves the model in the external storage, for faster later restoration.
	 */
	protected void saveModel() throws IOException {
		long startTime= System.currentTimeMillis();
        ObjectOutputStream trainingValuesObjectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(FILES_PREFIX+"trainingValues")));
        trainingValuesObjectOutputStream.writeObject(trainingValues);
        trainingValuesObjectOutputStream.flush();
        trainingValuesObjectOutputStream.close();
        ObjectOutputStream trainingMedianObjectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(FILES_PREFIX+"trainingMedian")));
        trainingMedianObjectOutputStream.writeObject(trainingMedian);
        trainingMedianObjectOutputStream.flush();
        trainingMedianObjectOutputStream.close();
        ObjectOutputStream APs_totalObjectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(FILES_PREFIX+"APs_total")));
        APs_totalObjectOutputStream.writeObject(APs_total);
        APs_totalObjectOutputStream.flush();
        APs_totalObjectOutputStream.close();
        Log.d(this.getClass().getName(), "Write duration: "+(System.currentTimeMillis()-startTime)+"ms");
	}
	
	/**
	 * Estimates the device's location from a list of ScanResults
	 */
	@Override
	Location estimateLocation(ArrayList<ScanResult> scanResults) throws OutOfTrainingAreaException {
		long tic= System.currentTimeMillis(); // measure how much time is needed for one estimation
		ArrayList<String> run_APs = new ArrayList<String>();
		ArrayList<Integer> run_values = new ArrayList<Integer>();
		/*
		 * Read ScanResults
		 */
		String bssid;
		int rssiValue;
		for (ScanResult scanResult : scanResults) {
			bssid = scanResult.BSSID;
			rssiValue = scanResult.level;
			if (!run_APs.contains(bssid)) {
				run_APs.add(bssid);
			}
			run_values.add(rssiValue); // TODO: maybe put this line inside the above if block?
		}		
		/*
		 * If there is no common AP between training fingerprints and the runtime fingerprint,
		 * no cell estimation can be done. So, we throw an OutOfTrainingAreaException.
		 */
		ArrayList<String> tmp= (ArrayList<String>) run_APs.clone();
		tmp.retainAll(APs_total);
		if (tmp.isEmpty())
			throw new OutOfTrainingAreaException("No common AP in training and runtime fingerprints.");
		/*
		 * Calculate the weight for each training phase fingerprint
		 */
		Object[] keys = trainingMedian.keySet().toArray();
		int keysSize = keys.length;
		String[][] weight = new String[keysSize][2];
		for (int i = 0; i < keysSize; i++) {
			float sum = 0;
			for (int j = 0; j < run_APs.size(); j++) {
				//Log.e("this.getClass().getName(), size_nameAPs=", Integer.toString(run_APs.size()));
				/*
				 * Check if this AP is known from the training phase
				 */
				String nameAPs = run_APs.get(j)/*.toString()*/;
				int train_value;
				if (nameAPs == null)
					train_value= -97; // random low value in dBm
				else {
					try {
						train_value= trainingMedian.get(keys[i]).get(nameAPs).getFirst();
					} catch (NullPointerException npe) {
						/*
						 * New AP in the training area. Ignore it and proceed with the next
						 * runtime fingerprint AP.
						 */
						continue;
					}
				}
				/*
				 * TODO: Is the code below ok?
				 * Why could trainingMedian.get(keys[i]).get(nameAPs) be empty?			 
				try {
					train_value= trainingMedian.get(keys[i]).get(nameAPs).getFirst();
				} catch (NullPointerException npe) {
					train_value= -97; // random low value in dBm
					//npe.printStackTrace();
				}
				*/
				// Log.e("trainingMedian.get(keys[i]).get(nameAPs).size()=", Integer.toString(trainingMedian.get(keys[i]).get(nameAPs).size()));
				// Log.e("trainingMedian.get(keys[i]).get(nameAPs)=", Boolean.toString(trainingMedian.get(keys[i]).containsKey(nameAPs)));
				sum = (float) (sum + (Math.pow(run_values.get(j) - train_value, 2)));
			}
			weight[i][0] = keys[i].toString();
			weight[i][1] = Float.toString((float) FloatMath.sqrt(sum));
		}		
		/*
		 * Sort the 2-D weight array
		 */
		long sort_tic= System.currentTimeMillis(); 
	 	weight=MergeSort.mergeSort(weight);
	 	long sort_tac= System.currentTimeMillis();
	 	/*
	 	 * Slow sort algortithm. TODO: delete it.
	 	 */
	 	/*for (int pass = 1; pass < keysSize; pass++) {
			for (int i = 0; i < keysSize - pass; i++) {
				if (Double.parseDouble(weight[i][1]) > Double.parseDouble(weight[i + 1][1])) {
					// swapping elements for weight[i][1]
					String stemp = weight[i][1];
					weight[i][1] = weight[i + 1][1];
					weight[i + 1][1] = stemp;
					// swapping elements for weight[i][0]
					String stemp0 = weight[i][0];
					weight[i][0] = weight[i + 1][0];
					weight[i + 1][0] = stemp0;
				}
			}
		}*/		
		
		/*
		 * Calculate the centroid of the top K positions
		 */
		String Centroidcells;
		float centroidX=0, centroidY=0;
		float medianX=0,  medianY=0;				
		//String stringParsing[], stringParsing1[], stringParsing2[];
		for (int l=0; l<K; l++) /* we need 4 times for the 4 smallest weights-cells */ {
			Centroidcells = weight[l][0];// for 4 smallest weights
			//Centroidcells= Centroidcells.split("cell")[1]; // Remove "cell" from "cellx_y.txt"
			//Centroidcells= Centroidcells.split("\\.")[0]; // Remove ".txt" from "x_y.txt"
			centroidX = centroidX + Float.parseFloat(Centroidcells.split("_")[0]);
			centroidY = centroidY + Float.parseFloat(Centroidcells.split("_")[1]);
			if(l==0) /* Except from the centroid, keep also the top-1 cell */ {
				medianX=centroidX;
				medianY=centroidY;
			}
		}
		centroidX = centroidX / K;
		centroidY = centroidY / K;		
		location= new Location(FingerprintService.PROVIDER);
		location.setLatitude(centroidX);
		location.setLongitude(centroidY);
		location.setTime(System.currentTimeMillis());
		// TODO: accuracy should be calculated from the radius of the top-K points
		location.setAccuracy(3);
		Log.d(this.getClass().getName(), "Estimated location= <"+centroidX+", "+centroidY+">. Total time= "+(System.currentTimeMillis()-tic)+"ms. Sort time= "+(sort_tac-sort_tic)+"ms.");
		return location;
	}
	
	/**
	 * TODO: complete the implementation of this method.
	 * TODO: this method should be implemented in the file FingerprintAlgorithm
	 */
	public Location testPerformance(ArrayList<ScanResult> scanResults) throws OutOfTrainingAreaException {
		//File run_folder= new File(umapDir, "runtime");
		//File[] run_listOfFiles = run_folder.listFiles();		
		String exported_line = null;
		/*
		 * Each run_listOfFiles file should contain a runtime fingerprint (one measurement for each AP).
		 * For each file, make a location estimation and write a line tin the it in a file.
		 * Then, add ground truth location in each line and feed this file to the matlab script to plot the error CDF.
		 * We should change K and call 2 times the estimateLocation(..) to support median_ and centroid_ values
		 */
		try /* estimate the location */ {
			estimateLocation(scanResults);
			double medianX= location.getLatitude();
			double centroidX= location.getLatitude();
			double medianY= location.getLongitude();
			double centroidY= location.getLongitude(); 
			exported_line="RX RY"+" "+medianX+" "+medianY+" "+centroidX+" "+centroidY+" "+"\n"; 
		}catch (OutOfTrainingAreaException e) {
			exported_line="% "+e.getMessage();
			throw e;
		}
		try /* write the estimation in the file */{
			FileWriter fw = new FileWriter(new File(FILES_PREFIX+"fingerprint_performance.txt"), true);
			fw.write(exported_line);
			fw.close();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		return location;
        
	}

}