package madynes.flowoid.exporter;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;

import android.content.Context;
import android.content.res.Resources;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;

public class Driver {
	private static final int MAX_RESTART_COUNT = 3;
	private int resource;
	private Context context;
	private String netInterface, filters;
	private Integer pid;
	private static ArrayList<Driver> drivers;
	private static int statDriverRestarts = 0;
	private HandlerThread driversCheckingThread;
	private Handler driversCheckingHandler;
	private Runnable driversCheckingRun;
	private int restartCount;
	
	/**
	 * Defines a new driver (packet headers sniffer) associated to an exec inside raw/.
	 * 
	 * @param resource an Android resource (e.g. R.raw.driver_ip)
	 * @param context an Android context (e.g. the Exporter Service)
	 * @param netInterface the network interface to listen packets
	 */
	public Driver(Context context, int resource, String netInterface, String filters) {
		this.resource = resource;
		this.context = context;
		this.netInterface = netInterface;
		this.filters = filters;
		drivers = new ArrayList<Driver>();
		restartCount = 0;
		pid = -1;
	}
	
	/**
	 * Instanciates, installs and starts a new Driver.
	 *
	 * @param context an Android context
	 * @param resource see the constructor definition
	 * @param netInterface the network interface to listen packets
	 * @return the new Driver or null if its installing and/or starting failed
	 */
	public static Driver use(Context context, int resource, String netInterface, String filters) {
		Driver driver = new Driver(context, resource, netInterface, filters);
		
		boolean driverState = driver.install() && driver.start();
		
		if(driverState) {
			synchronized (drivers) {
				drivers.add(driver);
			}
			
			return driver;
		}
		
		return null;
	}
	
	/**
	 * Stops all drivers usable.
	 */
	public static void flushUsedDrivers() {
		for(Driver driver : drivers) {
			driver.stop();
		}
		
		drivers.clear();
		statDriverRestarts = 0;
	}
	
	/**
	 * Creates the thread in charge of regularly checking if all of
	 * the drivers are still running and restarting them if necessary.
	 */
	private void createDriversCheckingTimer() {
		if(ExporterService.getPrefCheckDriversInterval() == 0) {
			return;
		}
	
		if(driversCheckingHandler != null) {
			driversCheckingHandler.removeCallbacks(driversCheckingRun);
			driversCheckingThread.quit();
			driversCheckingHandler = null;
		}
		
		driversCheckingThread = new HandlerThread("DriversCheckingThread");
		driversCheckingThread.start();
		driversCheckingHandler = new Handler(driversCheckingThread.getLooper());
		
		driversCheckingRun = new Runnable() {
			@Override
			public void run() {
				if(!ExporterService.isRunning()) {
					stop();
					
				} else {
					Log.d("THREAD", "Checking drivers");
	
					synchronized (drivers) {
						for(Driver driver : drivers) {
							if(!driver.isWorking()) {
								if(driver.getRestartCount() >= MAX_RESTART_COUNT) {
									driver.stop();
									
								} else {
									driver.incrementRestartCount();
									driver.start();
									
									statDriverRestarts++;
								}
	
							} else {
								driver.resetRestartCount();
							}
						}
					}
				}
					
				if(driversCheckingHandler != null) {
					driversCheckingHandler.postDelayed(this, ExporterService.getPrefCheckDriversInterval() * 1000);
				}
			}
		};

		driversCheckingHandler.post(driversCheckingRun);
	}
	
	/**
	 * Returns the number of automatic Driver restart processed.
	 * 
	 * @return number of restarts
	 */
	public static int getStatDriverFails() {
		return statDriverRestarts;
	}
	
	/**
	 * Returns the filename corresponding to the driver without any path.
	 * 
	 * @return Binary filename
	 */
	public String getFilename() {
		String[] path = context.getResources().getString(resource).split("/");
		
		return path[path.length - 1];
	}
	
	/**
	 * Starts the driver as root on the specified network interface.
	 * 
	 * @return false if the starting failed
	 */
	public boolean start() {
		if(canRunRootCommands()) {
			boolean startState;
			
			if(pid != -1) {
				stop();
			}
			
			String logFile = context.getFilesDir().getAbsolutePath() + "/driver_ip.log";
			
			startState = executeAsRoot(context.getFilesDir().getAbsolutePath()
					+ "/" + getFilename()
					+ " " + netInterface
					+ " '" + filters + "' 2>> " + logFile + " &");
			
			Log.d("DRIVER", "Starting - " + pid);
			
			createDriversCheckingTimer();
			
			return startState;
		}
		
		return false;
	}
	
	/**
	 * Stops the driver.
	 * 
	 * @return false if the stopping failed
	 */
	public boolean stop() {
		if(pid != -1 && driversCheckingHandler != null) {
			driversCheckingHandler.removeCallbacks(driversCheckingRun);
			driversCheckingThread.quit();
			driversCheckingHandler = null;
			
			Log.d("DRIVER", "Stopping - " + pid);
			
			return executeAsRoot("kill -9 " + pid);
		}
		
		return true;
	}
	
	/**
	 * Checks if the Driver is still working.
	 * 
	 * @return true if the Driver is currently running
	 */
	public boolean isWorking() {
		return new File("/proc/" + pid).exists();
	}
	
	/**
	 * Copies the driver binary from raw/ to files/ and make it executable.
	 * 
	 * @return false if the installing failed
	 */
	public boolean install() {
		Resources resources = context.getResources();
		
		try {
			InputStream probe = resources.openRawResource(resource);
			OutputStream installProbe = context.openFileOutput(getFilename(), 0);
			copyFile(probe, installProbe);
			probe.close();
			installProbe.close();
			chmodFile(getFilename(), "744");
			
		} catch (FileNotFoundException e) {
			if(e.getMessage().matches(".*file busy.*")) {
				Log.w("DRIVER", "Found current activity on the Driver file. Not overriden.");
				return true;
			}

			e.printStackTrace();
			return false;
			
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}

		return true;
	}
	
	/**
	 * Copies a binary flow from a stream to another one.
	 * 
	 * @param in input stream
	 * @param out output stream
	 * @throws IOException copying failed
	 */
	private void copyFile(InputStream in, OutputStream out) throws IOException {
		byte[] buffer = new byte[1024];
		int read;
		
		while((read = in.read(buffer)) != -1) {
			out.write(buffer, 0, read);
		}
	}
	
	/**
	 * Changes permissions on a system file.
	 * 
	 * @param File file on which permissions should be changed
	 * @param mode new permissions to apply
	 */
	private void chmodFile(String File, String mode){
		try {
			// Executes the command.
			Process process = Runtime.getRuntime().exec(
					//"/system/bin/chmod "
					"chmod "
							+ mode
							+ " "
							+ context.getFilesDir().getAbsolutePath()
							+ "/"
							+ File);
			
			BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
			
			int read;
			char[] buffer = new char[4096];
			StringBuffer output = new StringBuffer();
			
			while ((read = reader.read(buffer)) > 0) {
				output.append(buffer, 0, read);
			}
			
			reader.close();
			
			// Waits for the command to finish.
			process.waitFor();
			
		} catch (IOException e) {
			throw new RuntimeException(e);
			
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
	}
	
	/**
	 * Increments the restart count.
	 */
	public void incrementRestartCount() {
		restartCount++;
	}

	/**
	 * Resets the restart count
	 */
	public void resetRestartCount() {
		restartCount = 0;
	}

	/**
	 * Returns the restart count.
	 * 
	 * @return restart count
	 */
	public int getRestartCount() {
		return restartCount;
	}
	
	/* 
	 * Adapted from http://muzikant-android.blogspot.fr/2011/02/how-to-get-root-access-and-execute.html
	 * TODO: Ask license and recode if needed. 
	 */
	
	/**
	 * Tests if the device is rooted.
	 * 
	 * @return false if the device seems to be not rooted
	 */
	public boolean canRunRootCommands() {
		boolean retval = false;
		Process suProcess;

		try	{
			suProcess = Runtime.getRuntime().exec("su");

			DataOutputStream output = new DataOutputStream(suProcess.getOutputStream());
			DataInputStream input = new DataInputStream(suProcess.getInputStream());

			if(null != output && null != input) {
				output.writeBytes("id\n");
				output.flush();

				@SuppressWarnings("deprecation")
				String currUid = input.readLine();

				boolean exitSu = false;
				
				if (null == currUid) {
					retval = false;
					exitSu = false;
					Log.d("ROOT", "Can't get root access or denied by user");
					
				} else if(true == currUid.contains("uid=0")) {
					retval = true;
					exitSu = true;
					Log.d("ROOT", "Root access granted");
					
				} else {
					retval = false;
					exitSu = true;
					Log.d("ROOT", "Root access rejected: " + currUid);
				}

				if(exitSu) {
					output.writeBytes("exit\n");
					output.flush();
				}
			}

		} catch (Exception e) {
			retval = false;
			Log.d("ROOT", "Root access rejected [" + e.getClass().getName() + "] : " + e.getMessage());
		}

		return retval;
	}

	/**
	 * Executes a system command as root.
	 * 
	 * @param command command to execute
	 * @return false if the system returned an error
	 */
	@SuppressWarnings("deprecation")
	public boolean executeAsRoot(String command) {
		boolean retval = false;

		try {
			synchronized (pid) {
				Process suProcess = Runtime.getRuntime().exec("su");
	
				DataInputStream input = new DataInputStream(suProcess.getInputStream());
				DataOutputStream output = new DataOutputStream(suProcess.getOutputStream());
	
				output.writeBytes(command + "\n");
				output.writeBytes("echo $!\n");
				output.flush();
				
				if(command.endsWith("&")) {
					pid = Integer.parseInt(input.readLine());
				}
	
				output.writeBytes("exit\n");
				output.flush();
	
				try {
					int suProcessRetval = suProcess.waitFor();
					retval = (suProcessRetval == 0);
					
				} catch (Exception ex) {
					Log.e("ROOT", "Error executing root action", ex);
				}
			}
		} catch (IOException ex) {
			Log.w("ROOT", "Can't get root access", ex);
		} catch (SecurityException ex) {
			Log.w("ROOT", "Can't get root access", ex);
		} catch (Exception ex) {
			Log.w("ROOT", "Error executing internal operation", ex);
		}
		
		return retval;
	}
}
