/* 
 * Copyright 2012 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify 
 * it under the terms of the GNU General Public License ("License") as 
 * published by the Free Software Foundation  (version 3 of the License). 
 * This software is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the copy of the 
 * License you received along with this software for more details.
 */

package de.avm.android.fritzapp.com.discovery;

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.ListIterator;

import org.json.JSONArray;
import org.json.JSONObject;
import org.json.JSONTokener;

import de.avm.android.tr064.Tr064Boxinfo;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;

/**
 * List of boxes (known from preferences and/or discovered)
 */
@SuppressWarnings("serial")
public class BoxInfoList extends LinkedList<BoxInfo>
{
	private static final String TAG = "BoxInfoList";
	
	public static final String PREF_BOXINFOLIST = "boxes";
	private static final int PREF_BOXINFO_MAXITEMS = 10;
	public static final String MANUFACTURER = "AVM";
	public static final String CERTIFICATES_FILENAME = "certs";

	private KeyStore mCertificates = null;
	
	private static SharedPreferences getPrefs(Context context)
	{
		return PreferenceManager.getDefaultSharedPreferences(context);
	}

	public synchronized KeyStore getCertificates()
	{
		return mCertificates;
	}

	public synchronized boolean hasCertificate(BoxInfo boxInfo)
	{
		if (mCertificates == null)
		{
			try
			{
				return mCertificates.containsAlias(boxInfo.getUdn());
			}
			catch (KeyStoreException e)
			{
			}
		}
		return false;
	}
	
	/**
	 * Set or delete the certificate of a box
	 * 
	 * @param boxInfo
	 * 		the box
	 * @param certificate
	 * 		the certificate to store, or null to remove previously stored certificate
	 */
	public synchronized void setCertificate(BoxInfo boxInfo,
			Certificate certificate)
	{
		// TODO reactivate certificate validation by user (now accept valid and self signed)
//		if (mCertificates == null)
//			throw new IllegalStateException("Certificates key store not loaded");
//		
//		try
//		{
//			if (certificate == null)
//			{
//				if (mCertificates.containsAlias(boxInfo.getUdn()))
//					mCertificates.deleteEntry(boxInfo.getUdn());
//			}
//			else mCertificates.setCertificateEntry(boxInfo.getUdn(), certificate);
//		}
//		catch (KeyStoreException e)
//		{
//			Log.e(TAG, "Failed to store certificate in or delete certificate from key store", e);
//		}
	}
	
	public synchronized boolean hasAvailables()
	{
		for(BoxInfo info : this)
			if (info.isAvailable()) return true;
		return false;
	}

	/**
	 * Checks if there are items with preferences
	 * @return true if there are items with preferences
	 */
	public synchronized boolean hasPreferences()
	{
		for(BoxInfo info : this)
			if (info.hasPreferences()) return true;
		return false;
	}
	
	public synchronized int getCountOfUsables()
	{
		int count = 0;
		for(BoxInfo info : this)
			if (info.isAvailable() && info.isTr64() &&
					info.isAutoConnect())
				count++;
		return count;
	}
	
	/**
	 * Gets UDNs of all entries of available TR-064 devices
	 * @return the UDNs
	 */
	public synchronized String[] usablesToArray()
	{
		StringBuilder udns = new StringBuilder();
		for(BoxInfo info : this)
			if (info.isAvailable() && info.isTr64() &&
					info.isAutoConnect())
			{
				if (udns.length() > 0) udns.append('\n');
				udns.append(info.getUdn());
			}
		return udns.toString().split("\\n");
	}
	
	/**
	 * Gets most recently used of available boxes, if any
	 * 
	 * @return
	 * 		the item
	 */
	public synchronized BoxInfo getUsableMru()
	{
		BoxInfo boxInfo = null;
		for(BoxInfo info : this)
			if (info.hasPreferences() && info.isAvailable() &&
					info.isAutoConnect() &&
					((boxInfo == null) || (info.getMru() > boxInfo.getMru())))
				boxInfo = info;
		return boxInfo;
	}
	
	/**
	 * Gets Item by UDN
	 * 
	 * @param udn
	 *		the UDN
	 * @param availableOnly
	 *		true to get the object only if box is present 
	 * @return
	 * 		item or null
	 */
	public synchronized BoxInfo get(String udn, boolean availableOnly)
	{
		for(BoxInfo info : this)
			if (info.getUdn().equals(udn))
			{
				if (!availableOnly || info.isAvailable()) return info;
				break;
			}
		return null;
	}
	
	/**
	 * Loads list from preferences
	 * 
	 * @param context
	 * 		the context for accessing preferences
	 */
	public synchronized void load(Context context)
	{
		clear();
		try
		{
			String str = getPrefs(context).getString(PREF_BOXINFOLIST, "");
			if (str.length() > 0)
			{
				Object json = new JSONTokener(str).nextValue();
				if (json.getClass().equals(JSONArray.class))
				{
					for(int ii = 0; ii < ((JSONArray)json).length(); ii++)
					{
						try
						{
							JSONObject jsonInfo = ((JSONArray)json)
									.getJSONObject(ii);
							add(new BoxInfo(jsonInfo));
						}
						catch (Exception e)
						{
							e.printStackTrace();
						}
					}
				}
			}
		}
		catch (Exception e)
		{
			Log.e(TAG, "Cannot load from preferences.");
			e.printStackTrace();
			clear();
		}
		
		// TODO reactivate certificate validation by user (now accept valid and self signed)
//		// load or create key store
//		KeyStore.PasswordProtection protArg = new KeyStore.PasswordProtection(
//				context.getPackageName().toCharArray());
//		try
//		{
//			mCertificates = KeyStore.Builder.newInstance("PKCS12", null,
//					context.getFileStreamPath(CERTIFICATES_FILENAME),
//					protArg).getKeyStore();
//		}
//		catch(Exception e)
//		{
//			mCertificates = null;
//			Log.e(TAG, "Failed to load trusted certificates", e);
//		}
//		if (mCertificates == null)
//		{
//			try
//			{
//				mCertificates = KeyStore.Builder.newInstance("PKCS12", null,
//						protArg).getKeyStore();
//			}
//			catch(Exception e)
//			{
//				Log.e(TAG, "Failed to create certificate's store", e);
//			}
//		}
	}
	
	/**
	 * Saves list to preferences
	 * (only items with TR-064 )
	 * 
	 * @param context
	 * 		the context for accessing preferences
	 */
	public synchronized void save(Context context)
	{
		JSONArray json = new JSONArray();
		for(BoxInfo info : this)
		{
			JSONObject jsonInfo = info.getJson();
			if (jsonInfo != null) json.put(jsonInfo);
		}

		Editor editor = getPrefs(context).edit();
		editor.putString(PREF_BOXINFOLIST, json.toString());
		editor.commit();
		
		// TODO reactivate certificate validation by user (now accept valid and self signed)
//		FileOutputStream file = null;
//		try
//		{
//			if (mCertificates != null)
//			{
//				// clean up
//				ArrayList<String> udns = Collections
//						.list(mCertificates.aliases());
//				for (String udn : udns)
//				{
//					BoxInfo info = this.get(udn, false);
//					if ((info == null) || (!info.hasPreferences() &&
//							!info.isAvailable()))
//					{
//						try { mCertificates.deleteEntry(udn); }
//						catch (Exception e) { }
//					}
//				}
//				// save key store file
//				file = context.openFileOutput(CERTIFICATES_FILENAME,
//						Context.MODE_PRIVATE);
//				// we don't need password protection...
//				mCertificates.store(file,
//						context.getPackageName().toCharArray());
//			}
//		}
//		catch(Exception e)
//		{
//			Log.e(TAG, "Failed to save trusted certificates", e);
//		}
//		finally
//		{
//			if (file != null)
//			{
//				try { file.close(); }
//				catch (IOException e) { }
//			}
//		}
	}
	
	/**
	 * Removes all discovery info 
	 */
	public synchronized void reset()
	{
		if (size() > 0)
			for (ListIterator<BoxInfo> iterator = listIterator(0);
					iterator.hasNext();)
			{
				BoxInfo info = iterator.next();
				if (info.hasPreferences())
					info.reset();
				else
					iterator.remove();
			}
	}
	
	/**
	 * Sets MRU to current time
	 * 
	 * @param context
	 * 		the context for accessing preferences
	 * @param info
	 * 		update this box' MRU 
	 */
	public synchronized void updateMru(Context context, BoxInfo info)
	{
		if (contains(info) && info.isTr64())
		{
			if (!info.hasPreferences())
			{
				// keep only PREF_BOXINFO_MAXITEMS in preferences
				LinkedList<BoxInfo> list = new LinkedList<BoxInfo>(); 
				for(BoxInfo boxInfo : this)
					if (boxInfo.hasPreferences()) list.add(boxInfo);
				if (list.size() >= PREF_BOXINFO_MAXITEMS)
				{
					// get rid of oldest ones (if currently available on network, keep
					// it in list without MRU time)
					Collections.sort(list, new Comparator<BoxInfo>()
							{
								public int compare(BoxInfo object1, BoxInfo object2)
								{
									if (object1.getMru() < object2.getMru())
										return -1;
									if (object1.getMru() > object2.getMru())
										return 1;
									return 0;
								}
							});
					for (int ii = list.size() -1 ; ii >= PREF_BOXINFO_MAXITEMS - 1; ii--)
					{
						BoxInfo boxInfo = list.get(ii);
						if (boxInfo.isAvailable())
							boxInfo.updateMru(false); 
						else
							remove(boxInfo);
					}
				}
			}
			info.updateMru(true);
			save(context);
		}
	}

	/**
	 * Imports box setting
	 * (sets MRU time stamp of new entry to current time and saves list)
	 * 
	 * @param context
	 * 		the context for accessing preferences
	 * @param udn
	 *		the UDN of device which is not in this list
	 * @param boxPassword
	 * @param voipName
	 * @param voipPassword
	 * @param voipTitle
	 * @param tam
	 */
	public synchronized void importBox(Context context,
			String udn, String boxPassword,
			String voipName, String voipPassword, String voipTitle,
			String tam)
	{
		if (get(udn, false) == null)
		{
			try
			{
				// IP address doesn't have to be correct
				BoxInfo boxInfo = new BoxInfo(true, udn,
						Tr064Boxinfo.createDefaultUri("192.168.178.1").toURL(),
						"", null);
				boxInfo.setBoxPassword(boxPassword);
				boxInfo.setVoipCredentials(voipName, voipPassword);
				boxInfo.setVoIPTitle(voipTitle);
				boxInfo.setTam(tam);
				add(boxInfo);
				
				updateMru(context, boxInfo);
			}
			catch (Exception e)
			{
				Log.e(TAG, "Cannot import old box' preferences.");
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * Updates list on discovery 
	 * 
	 * @param tr064Boxinfo
	 * 		parsed TR-064 info to store along
	 * @return
	 * 		true if data changed which has to be saved to preferences
	 */
	public boolean foundBox(Tr064Boxinfo tr064Boxinfo)
	{
		try
		{
			String friendlyName = tr064Boxinfo.getFriendlyName();
			if (TextUtils.isEmpty(friendlyName))
				friendlyName = tr064Boxinfo.getModel();
			return foundBox(true, tr064Boxinfo.getUdn(),
					tr064Boxinfo.getUri().toURL(),
					tr064Boxinfo.getManufacturer() + " " + friendlyName, 
					tr064Boxinfo);
		}
		catch (MalformedURLException e)
		{
			e.printStackTrace();
		}
		return false;
	}
	
	/**
	 * Updates list on discovery 
	 * 
	 * @param isTr64
	 * 		true for TR-064 UDN
	 * @param udn
	 * 		the UDN
	 * @param location
	 *		the URL of device description
	 * @param server
	 * 		the server info string
	 * @return
	 * 		true if data changed which has to be saved to preferences
	 */
	public synchronized boolean foundBox(boolean isTR64, String udn,
			URL location, String server)
	{
		return foundBox(isTR64, udn, location, server, null);
	}

	private synchronized boolean foundBox(boolean isTR64, String udn,
			URL location, String server, Tr064Boxinfo tr064Boxinfo)
	{
		String host = location.getHost();
		// TODO exclusion of IPv6 addresses not tested
		if (host.contains(":")) return false; 
		// check manufacturer
		boolean isAvm = false;
		for(String token : server.split(" "))
			if (token.compareToIgnoreCase(MANUFACTURER) == 0)
			{
				isAvm = true;
				break;
			}
		if (!isAvm) return false;

		BoxInfo boxTr64 = null;
		BoxInfo boxNoTr64 = null;
		for (BoxInfo info : this)
		{
			if (info.getUdn().equals(udn))
			{
				// already in list
				if (isTR64)
					boxTr64 = info;
				else
					boxNoTr64 = info;
			}
			else if (info.isAvailable() && (info.isTr64() != isTR64) &&
					(info.getLocation().getHost().equals(host)))
			{
				// other interface already found on this host
				if (isTR64)
					boxNoTr64 = info;
				else
					boxTr64 = info;
			}
		}
		
		if (isTR64 && (boxNoTr64 != null))
		{
			remove(boxNoTr64); // now we have the better one
		}
		if (!isTR64 && (boxTr64 != null) && boxTr64.isAvailable())
		{
			return false; // we already have the better one
		}
		
		// add or update
		BoxInfo box = (isTR64) ? boxTr64 : boxNoTr64; 
		if (box != null)
		{
			box.updateLocation(location, server);
			if (tr064Boxinfo != null)
				box.updateTr064Boxinfo(tr064Boxinfo);
		}
		else add(new BoxInfo(isTR64, udn, location, server, tr064Boxinfo));

		return isTR64;
	}
}
