package de.tudarmstadt.informatik.hostage.persistence;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.content.Context;
import android.content.SharedPreferences;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import de.tudarmstadt.informatik.hostage.Hostage;
import de.tudarmstadt.informatik.hostage.Listener;
import de.tudarmstadt.informatik.hostage.R;
import de.tudarmstadt.informatik.hostage.model.Profile;
import de.tudarmstadt.informatik.hostage.ui.activity.MainActivity;
import de.tudarmstadt.informatik.hostage.ui.adapter.ProfileManagerListAdapter;

/**
 * The profile manager is responsible for persisting and deleting profiles
 *
 * @author Alexander Brakowski
 * @created 10.02.14 20:24
 */
public class ProfileManager {

	/**
	 * The singleton instance holder
	 */
	private static ProfileManager INSTANCE = null;

	/**
	 * An list adapter, which the profile manager informs about data changes
	 */
	private ProfileManagerListAdapter mProfileListAdapter = null;

	/**
	 * Holds a reference to the currently active profile
	 */
	private Profile mCurrentActivatedProfile = null;

	/**
	 * Holds a reference to the random profile
	 */
	private Profile mRandomProfile = null;

	/**
	 * The profiles are being serialized and persisted into this file
	 */
	private static final String PERSIST_FILENAME = "hostage_profiles.json";

	/**
	 * Hold the current profile id, it will be incremented each time a new profile is added.
	 * The new profile will get the newly incremented value as an id.
	 */
	public int mIncrementValue = 1;

	/**
	 * Holds all the available profiles. The key in the map is the ID of the profile.
	 */
	public HashMap<Integer, Profile> mProfiles;

	private SharedPreferences mSharedPreferences;
	private SharedPreferences.Editor mSharedEditor;

	/**
	 * Since the profile manager should only have one instance in the whole app, we are using the singleton pattern.
	 * This method creates a new instance of the profile manager, if no instance already exists, and returns it.
	 *
	 * @return the singleton instance
	 */
	public static ProfileManager getInstance(){
		if(INSTANCE == null){
			INSTANCE = new ProfileManager();
		}

		if(INSTANCE.getNumberOfProfiles() == 0){
			INSTANCE.loadData();
		}

		return INSTANCE;
	}

	/**
	 * A private constructor, that can/should only be called by getInstance, since we want to enforce the usage of the singleton.
	 */
	private ProfileManager(){
		mProfiles = new HashMap<Integer, Profile>();

		String sharedPreferencePath = MainActivity.getContext().getString(R.string.shared_preference_path);
		mSharedPreferences = MainActivity.getContext().getSharedPreferences(sharedPreferencePath, Hostage.MODE_PRIVATE);
		mSharedEditor = mSharedPreferences.edit();
	}

	/**
	 * Reads all data from an inputstream and appends it to a string
	 *
	 * @param is The input stream to read the data from
	 * @return the whole data from the input stream as an string
	 */
	public String readAll( final InputStream is ) {
		if( null == is ) {
			return "";
		}

		StringBuilder sb = new StringBuilder();
		int rc;
		try {
			while( (rc = is.read()) >= 0 ){
				sb.append( (char) rc );
			}
		} catch (IOException e) {
			e.printStackTrace();
		}

		return sb.toString();
	}


	/**
	 * Reads all the data of the the profiles, that were persisted and unserializes them.
	 *
	 * The profiles were serialized into JSON and persisted into the android private file.
	 * See {@see ProfileManager#persistData}.
	 */
	public void loadData(){
		try {
            String UTF8 = "utf8";
            int BUFFER_SIZE = 8192;
            BufferedReader fbr = new BufferedReader(new InputStreamReader(MainActivity.getContext().openFileInput(PERSIST_FILENAME), UTF8), BUFFER_SIZE);

            StringBuilder sb = new StringBuilder();
            String line;
            while((line = fbr.readLine()) != null) {
                sb.append(line);
            }

            JSONArray arr = new JSONArray(sb.toString());
			fbr.close();

			for(int i=0; i<arr.length(); i++){
				JSONObject obj = arr.getJSONObject(i);

				Profile p = new Profile();
				p.fromJSON(obj);

				mProfiles.put(p.mId, p);

				if(p.mId > mIncrementValue){
					mIncrementValue = p.mId;
				}

				if(p.mActivated){
					activateProfile(p, false);
				}

				if(p.mIsRandom){
					this.mRandomProfile = p;
				}
			}

		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (StreamCorruptedException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (JSONException e) {
			e.printStackTrace();
		} finally {
			if(mProfiles.size() == 0){
				this.fillWithDefaultData();
			}

			if(this.mRandomProfile != null){
				randomizeProtocols(mRandomProfile);
			}
		}
	}

	/**
	 * All the profiles that are hold by the profile manager are being serialized into JSON and then written into an private android file.
	 */
	public void persistData(){
		try {
			int BUFFER_SIZE = 8192;
			String UTF8 = "utf8";

			FileOutputStream fout = MainActivity.getContext().openFileOutput(PERSIST_FILENAME, Context.MODE_PRIVATE);
			BufferedWriter fnw = new BufferedWriter(new OutputStreamWriter(fout, UTF8), BUFFER_SIZE);

			JSONArray arr = new JSONArray();
			for(Profile p: mProfiles.values()){
				arr.put(p.toJSON());
			}

			fnw.write(arr.toString());

			fnw.close();
			fout.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	/**
	 * Retrieves all the profiles as an List
	 * @return a list that holds all the profiles
	 */
	public List<Profile> getProfilesList(){
		return new ArrayList<Profile>(getProfilesCollection());
	}

	/**
	 * Retrieves all the profiles as an collection
	 * @return a collection of all the profiles
	 */
	public Collection<Profile> getProfilesCollection(){
		if(mProfiles.size() == 0 || mProfiles == null){
			this.loadData();
		}

		return mProfiles.values();
	}

	/**
	 * Retrieves an map of all the profiles. The key in the map is the ID of an profile.
	 * @return a map of profiles
	 */
	public Map<Integer, Profile> getMapProfiles(){
		return mProfiles;
	}

	/**
	 * Activates and deactivates protocols randomly in the given profile
	 *
	 * @param profile the profile to randomize the protocols for
	 */
	public void randomizeProtocols(Profile profile){
		LinkedList<String> protocols = new LinkedList<String>(Arrays.asList(MainActivity.getContext().getResources().getStringArray(R.array.protocols)));
		protocols.remove("GHOST");

		profile.mActiveProtocols.clear();

		Random rand = new Random();
		int numberOfProtocolsToActivate = rand.nextInt(protocols.size()) + 1;

		while(numberOfProtocolsToActivate-- > 0){
			int randomIndex = rand.nextInt(protocols.size());
			String protocol = protocols.get(randomIndex);

			profile.mActiveProtocols.put(protocol, true);
			protocols.remove(protocol);
		}

		persistData();
	}

	/**
	 * Adds or updates a given profile.
	 *
	 * @param profile the profile to persist
	 * @return the id the profile was assigned to
	 */
	public int persistProfile(Profile profile){
		if(profile.mId == -1){
			profile.mId = ++mIncrementValue;
		}

		mProfiles.put(profile.mId, profile);

		this.persistData();

		if(this.mProfileListAdapter != null){
			this.mProfileListAdapter.notifyDataSetChanged();
		}

		return profile.mId;
	}

	/**
	 * Retrieves the profile with the given id
	 *
	 * @param id the id of the profile
	 * @return the profile
	 */
	public Profile getProfile(int id){
		if(mProfiles.size() == 0){
			loadData();
		}

		if(this.mProfiles.containsKey(id)){
			return this.mProfiles.get(id);
		}

		return null;
	}

	/**
	 * Adds a profile
	 *
	 * @param profile the profile to add
	 */
	public void addProfile(Profile profile){
		this.addProfile(profile, true);
	}

	/**
	 * Adds a given profile to the profile manager.
	 *
	 * @param profile the profile to add
	 * @param persist true,  if the profile should also be persisted immediatly,
	 *                false, if the profile should just be added internally without being persisted
	 *                       (Note: you can still call persistData later to persist all the profiles)
	 */
	public void addProfile(Profile profile, boolean persist){

		if(profile.mId == -1){
			profile.mId = ++mIncrementValue;
		}

		mProfiles.put(profile.mId, profile);

		if(persist){
			persistData();
		}

		if(this.mProfileListAdapter != null){
			this.mProfileListAdapter.add(profile);
			this.mProfileListAdapter.notifyDataSetChanged();
		}
	}

	/**
	 * Deletes a given profile. These changes will be persisted immediatly.
	 *
	 * @param profile the profile to delete
	 */
	public void deleteProfile(Profile profile){
		if(this.mProfiles.containsKey(profile.mId)){
			Profile p = getProfile(profile.mId);
			this.mProfiles.remove(profile.mId);

			if(p.mActivated || this.mCurrentActivatedProfile.mId == p.mId){
				mCurrentActivatedProfile = mRandomProfile;
				mRandomProfile.mActivated = true;
			}

			this.persistData();
			//this.dbh.deleteProfile(profile.mId);

			if(this.mProfileListAdapter != null){
				this.mProfileListAdapter.remove(profile);
				this.mProfileListAdapter.notifyDataSetChanged();
			}
		}
	}

	/**
	 * Removes all profiles.
	 */
	public void clearProfiles(){
		mProfiles.clear();
		persistData();
	}

	/**
	 * Same as {@see activateProfile(Profile profile, boolean persist)} but with persist arg being always true
	 * @param profile the profile to active
	 */
	public void activateProfile(Profile profile){
		this.activateProfile(profile, true);
	}

	/**
	 * Makes a given profile active.
	 *
	 * @param profile the profile to activate
	 * @param persist indicates if the profile should be persisted after activating
	 */
	public void activateProfile(Profile profile, boolean persist){
		if(profile.equals(this.mCurrentActivatedProfile) || (mCurrentActivatedProfile != null && profile.mId == mCurrentActivatedProfile.mId)) return;

		if(this.mCurrentActivatedProfile != null){
			this.mCurrentActivatedProfile.mActivated = false;
			this.persistProfile(this.mCurrentActivatedProfile);
		}

		profile.mActivated = true;
		this.mCurrentActivatedProfile = profile;
		this.mProfiles.put(profile.mId, profile);

		mSharedEditor.putString("os", profile.mLabel);
		mSharedEditor.commit();

		if(persist) persistData();

		if(this.mProfileListAdapter != null){
			this.mProfileListAdapter.notifyDataSetChanged();
		}

		if(MainActivity.getInstance().getHostageService() != null){
			if(MainActivity.getInstance().getHostageService().hasRunningListeners()){
				List<String> protocolsToStart = profile.getActiveProtocols();
				if(profile.mGhostActive){
					protocolsToStart.add("GHOST");
				}

				for(Listener listener: MainActivity.getInstance().getHostageService().getListeners()){
					if(listener.isRunning()){
						if(protocolsToStart.contains(listener.getProtocolName()) && !listener.getProtocolName().equals("GHOST")){
							protocolsToStart.remove(listener.getProtocolName());
						} else {
							MainActivity.getInstance().getHostageService().stopListenerAllPorts(listener.getProtocolName());
						}
					}
				}

				MainActivity.getInstance().startMonitorServices(protocolsToStart);
			}
		}
	}

	/**
	 * Checks if the "random" profile is currently active
	 *
	 * @return true,  if active
	 *         false, otherwise
	 */
	public boolean isRandomActive(){
		return this.mCurrentActivatedProfile.equals(this.mRandomProfile);
	}

	/**
	 * Retrieves the "random" profile
	 *
	 * @return the "random" profile
	 */
	public Profile getRandomProfile(){
		return this.mRandomProfile;
	}

	/**
	 * Retrieves the currently active profile
	 *
	 * @return the active profile
	 */
	public Profile getCurrentActivatedProfile(){
		return mCurrentActivatedProfile;
	}

	/**
	 * Sets the list adapter that should also be managed by the profile manager
	 *
	 * @param profileListAdapter the list adapter to manage
	 */
	public void setProfileListAdapter(ProfileManagerListAdapter profileListAdapter){
		this.mProfileListAdapter = profileListAdapter;
	}


	/**
	 * Retrieves the list adapter, that is being managed by the profile manager
	 * @return the list adapter
	 */
	public ProfileManagerListAdapter getProfileListAdapter(){
		return this.mProfileListAdapter;
	}

	/**
	 * Retrieves the number of profiles
	 *
	 * @return the number of profiles
	 */
	public int getNumberOfProfiles(){
		return mProfiles.size();
	}



	/**
	 * Pick n numbers between 0 (inclusive) and k (inclusive)
	 * While there are very deterministic ways to do this,
	 * for large k and small n, this could be easier than creating
	 * an large array and sorting, i.e. k = 10,000
	 */
	public Set<Integer> pickRandom(int n, int s, int k) {
		Random random = new Random(); // if this method is used often, perhaps define random at class level
		Set<Integer> picked = new HashSet<Integer>();
		while(picked.size() < n) {
			picked.add(random.nextInt(k-s) + s);
		}
		return picked;
	}

	/**
	 * Fills the profiles manager with default profiles
	 */
	public void fillWithDefaultData(){
		Profile windowsSeven = new Profile(
				0,
				"Windows 7",
				MainActivity.getInstance().getString(R.string.profile_seven_desc),
				R.drawable.ic_profile_vista,
				false
		);

		windowsSeven.mActiveProtocols.put("SMB", true);
		windowsSeven.mGhostActive = true;
		windowsSeven.mGhostPorts = "135,5357";

		for(int i: pickRandom(3, 49152, 65535)){
			windowsSeven.mGhostPorts += "," + i;
		}

		windowsSeven.mActiveProtocols.put("ECHO", true);

		this.addProfile(windowsSeven, false);

		Profile windowsXP = new Profile(
				1,
				"Windows XP",
                MainActivity.getInstance().getString(R.string.profile_xp_desc),
				R.drawable.ic_profile_xp,
				false
		);

		windowsXP.mActiveProtocols.put("SMB", true);
		windowsXP.mGhostActive = true;
		windowsXP.mGhostPorts = "135";

		for(int i: pickRandom(3, 49152, 60000)){
			windowsXP.mGhostPorts += "," + i;
		}

		windowsXP.mActiveProtocols.put("ECHO", true);

		this.addProfile(windowsXP, false);

		Profile serverHTTP = new Profile(
				2,
				"Web Server Apache",
                MainActivity.getInstance().getString(R.string.profile_webserv_apache_desc),
				R.drawable.ic_profile_apache,
				false
		);

		serverHTTP.mActiveProtocols.put("HTTP", true);
		serverHTTP.mActiveProtocols.put("HTTPS", true);
		serverHTTP.mActiveProtocols.put("MySQL", true);

		this.addProfile(serverHTTP, false);

		Profile serverWeb = new Profile(
				3,
				"Web Server IIS",
                MainActivity.getInstance().getString(R.string.profile_webserv_iis_desc),
				R.drawable.ic_profile_apache,
				false
		);

		serverWeb.mActiveProtocols.put("HTTP", true);
		serverWeb.mActiveProtocols.put("HTTPS", true);
		serverWeb.mActiveProtocols.put("FTP", true);

		this.addProfile(serverWeb, false);

		Profile unixMachine = new Profile(
				4,
				"\"Hardened\" Linux system ",
                MainActivity.getInstance().getString(R.string.profile_linux_hard_desc),
				R.drawable.ic_profile_unix,
				false
		);

		unixMachine.mActiveProtocols.put("SSH", true);

		this.addProfile(unixMachine, false);

		Profile linuxMachine = new Profile(
				5,
				"Linux system",
                MainActivity.getInstance().getString(R.string.profile_linux_desc),
				R.drawable.ic_profile_linux,
				false
		);

		linuxMachine.mActiveProtocols.put("FTP", true);
		linuxMachine.mActiveProtocols.put("TELNET", true);
		linuxMachine.mActiveProtocols.put("HTTP", true);
		linuxMachine.mActiveProtocols.put("HTTPS", true);
		linuxMachine.mActiveProtocols.put("MySQL", true);

		this.addProfile(linuxMachine, false);

		Profile voipServer = new Profile(
				6,
				"VOIP Server",
                MainActivity.getInstance().getString(R.string.profile_voip_desc),
				R.drawable.ic_profile_asterisks,
				false
		);

		voipServer.mActiveProtocols.put("SIP", true);

		this.addProfile(voipServer, false);

		Profile randomProfile = new Profile(
				7,
				"Random",
                MainActivity.getInstance().getString(R.string.profile_random_desc),
				R.drawable.ic_launcher,
				false
		);

		randomProfile.mIsRandom = true;

		this.addProfile(randomProfile, false);

		Profile paranoidProfile = new Profile(
				8,
				"Paranoid",
                MainActivity.getInstance().getString(R.string.profile_paranoid_desc),
				R.drawable.ic_profile_paranoid,
				false
		);

		for(String protocol: MainActivity.context.getResources().getStringArray(R.array.protocols)){
			if(protocol.equals("GHOST")) continue;
			paranoidProfile.mActiveProtocols.put(protocol, true);
		}

		paranoidProfile.mActivated = true;
		this.addProfile(paranoidProfile, false);

		mIncrementValue = 8;

		this.activateProfile(paranoidProfile, false);

		persistData();
	}

}
