/* 
 * 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;

import java.io.IOException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Locale;

import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.text.TextUtils;
import android.util.Log;
import de.avm.android.fritzapp.com.discovery.BoxFinder;
import de.avm.android.fritzapp.com.discovery.BoxInfo;
import de.avm.android.fritzapp.gui.SettingsTestActivity;
import de.avm.android.tr064.Tr064Boxinfo;
import de.avm.android.tr064.Tr064Capabilities;
import de.avm.android.tr064.exceptions.BaseException;
import de.avm.android.tr064.exceptions.DataMisformatException;
import de.avm.android.tr064.model.CallLog;
import de.avm.android.tr064.model.PhoneBook;
import de.avm.android.tr064.model.TamInfo;
import de.avm.android.tr064.model.VoIPClientInfo;
import de.avm.android.tr064.model.VoIPInfoEx;
import de.avm.android.tr064.model.WLANInfo;
import de.avm.android.tr064.soap.GetCallList;
import de.avm.android.tr064.soap.GetCountWLANDevices;
import de.avm.android.tr064.soap.GetPhonebook;
import de.avm.android.tr064.soap.GetPhonebookList;
import de.avm.android.tr064.soap.GetPhonebookName;
import de.avm.android.tr064.soap.GetTamInfo;
import de.avm.android.tr064.soap.GetVoIPConfClientInfo;
import de.avm.android.tr064.soap.GetVoIPConfInfoEx;
import de.avm.android.tr064.soap.GetVoIPConfNumberOfClients;
import de.avm.android.tr064.soap.GetWLANStatus;
import de.avm.android.tr064.soap.ISoapCredentials;
import de.avm.android.tr064.soap.SetVoIPConfClientInfo;

public class DataHub
{
	private static final String TAG = "DataHub";

	public static final String SOAP_USERNAME = "dslf-config";

	private static final int NO_FIXURL_VERSION_MAJOR = 5;
	private static final int NO_FIXURL_VERSION_MINOR = 20;
	public static final int CALLLOG_ALL = 0;
	public static final int FIRST_TAM_NUMBER_DIGIT = 0;	// 0...9 
	public static final int MAX_TAMS = 5; 					// 1...9
			// FIRST_TAM_NUMBER_DIGIT + MAX_TAMS <= 10!! 
	public static final int MAX_VOIPCLIENTS = 10;
	
	public interface OnCompleteListener<RESULT>
	{
		/**
		 * Callback for asynchronous methods 
		 * 
		 * @param result result or null
		 * @param error exception or null
		 */
		void onComplete(RESULT result, Exception error);
	};

	private abstract class GetItemRunnable implements Runnable
	{
		protected int mItemIndex;
		
		private GetItemRunnable(int itemIndex)
		{
			mItemIndex = itemIndex;
		}
	}

	/**
	 * multiple parallel requests for client list are aggregated
	 */
	private static class InfosRequests<RESULT>
			extends ArrayList<OnCompleteListener<RESULT>>
			implements OnCompleteListener<RESULT>
	{
		private static final long serialVersionUID = 1L;

		/**
		 * @param listener for new request
		 * @return true if first entry added
		 */
		public synchronized boolean add(
				OnCompleteListener<RESULT> listener)
		{
			super.add(listener);
			return size() == 1;
		}
		
		@SuppressWarnings("unchecked")
		public void onComplete(RESULT result, Exception error)
		{
			ArrayList<OnCompleteListener<RESULT>> list = null;
			synchronized (this)
			{
				list = (ArrayList<OnCompleteListener<RESULT>>)clone();
				clear();
			}
			for (OnCompleteListener<RESULT> listener : list)
			{
				try { listener.onComplete(result, error); }
				catch (Exception e) { }
			}
		}
	}

	private static InfosRequests<TamInfo[]> mTamInfoRequests =
			new InfosRequests<TamInfo[]>();
	private static InfosRequests<VoIPClientInfo[]> mVoIPClientsRequests =
			new InfosRequests<VoIPClientInfo[]>();

	public static class SoapCredentials implements ISoapCredentials
	{
		private Context mContext;
		
		public SoapCredentials(Context context)
		{
			mContext = context;
		}

		public String getHost()
		{
			return ComSettingsChecker.getLocationHost();
		}
		
		public int getPort(boolean ssl)
		{
			return (ssl) ? ComSettingsChecker.getLocationSslPort():
					ComSettingsChecker.getLocationPort();
		}
		
		public boolean isSuppressSSL()
		{
			return SettingsTestActivity.isNoHttps(mContext);
		}

		public String getUsername()
		{
			return getFritzboxUser();
		}
		
		public String getPassword()
		{
			return getFritzboxPass();
		}

		public KeyStore getTrustedServerCertificates()
		{
			return getFritzboxCerts();
		}
		
		public boolean shouldFixDownloadUrl()
		{
			JasonBoxinfo jasonBoxInfo = ComSettingsChecker.getJasonBoxinfo();
			return ((jasonBoxInfo != null) &&
					(jasonBoxInfo.compareVersion(NO_FIXURL_VERSION_MAJOR,
							NO_FIXURL_VERSION_MINOR) < 0));
		}
	};
			
	/**
	 * Gets the phone book list from the fritzbox.
	 * 
	 * @param c
	 *            a valid context
	 * 
	 * @return the phone book list
	 */
	public String[] getPhoneBookList(Context c)
			throws DataMisformatException, BaseException, IOException
	{
		return new GetPhonebookList(new SoapCredentials(c))
				.getQualifiedResult();
	}

	/**
	 * Gets the phone book name from the fritzbox.
	 * 
	 * @param id
	 *            the id
	 * @param c
	 *            a valid context
	 * 
	 * @return the phone book
	 */
	public String getPhoneBookName(String id, Context c)
			throws DataMisformatException, BaseException, IOException
	{
		return new GetPhonebookName(new SoapCredentials(c), id)
				.getQualifiedResult();
	}

	/**
	 * Gets the phone book from the fritzbox.
	 * 
	 * @param id
	 *            the id
	 * @param c
	 *            a valid context
	 * 
	 * @return the phone book
	 */
	public PhoneBook getPhoneBook(String id, Context c)
			throws DataMisformatException, BaseException, IOException
	{
		return new GetPhonebook(new SoapCredentials(c), id)
				.getQualifiedResult();
	}

	/**
	 * Get Info on WLAN-Connection of this mobile phone to connected box
	 * 
	 * @return Info or null, if not available
	 * @throws BaseException 
	 */
	public WLANInfo getWLANInfo(Context c)
			throws BaseException
	{
		BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		if (boxInfo == null) return null;
		Tr064Boxinfo tr64Info = boxInfo.getTr064Boxinfo();
		if (tr64Info == null) return null;

		WLANInfo wlanInfo = null;
		
		WifiManager wifiManager = (WifiManager) c.getSystemService(Context.WIFI_SERVICE);
		WifiInfo wifiInfo = wifiManager.getConnectionInfo();
		String mac = wifiInfo.getMacAddress();

		boolean specific = tr64Info.getTr064Capabilities().has(
				Tr064Capabilities.Capability.WLAN_CONF_SPECIFIC);
		for (int interfaceIndex = 1; (wlanInfo == null) &&
				(interfaceIndex <= tr64Info.getWlanInterfacesCount());
				interfaceIndex++)
		{
			if (specific)
				wlanInfo = getWLANInfoSpecific(c, interfaceIndex, mac);
			else
				wlanInfo = getWLANInfo(c, interfaceIndex, mac);
		}
		
		if (wlanInfo != null) wlanInfo.setSsid(wifiInfo.getSSID());
		return wlanInfo;
	}

	/**
	 * Gets Information about the WLAN Quality from the fritzbox
	 * enumerating all devices
	 * 
	 * @param c
	 *            a valid context
	 * 
	 * @return the call log
	 */
	private WLANInfo getWLANInfo(Context c, int interfaceIndex, String mac)
			throws BaseException
	{
		int devicesCount = 0;
		try
		{
			devicesCount = new GetCountWLANDevices(new SoapCredentials(c),
					interfaceIndex).getQualifiedResult();
		}
		catch (IOException e)
		{
			e.printStackTrace();
		}
		
		// get info on device with mac
		for (int deviceIndex = 0; deviceIndex < devicesCount; deviceIndex++)
		{
			try
			{
				WLANInfo result = new GetWLANStatus(new SoapCredentials(c),
						interfaceIndex, deviceIndex).getQualifiedResult();
				if ((result != null) && result.isAuthState() &&
					(result.getMacAdress().equalsIgnoreCase(mac)))
					return result;
			}
			catch (Exception exp)
			{
				exp.printStackTrace();
			}
		}
		return null;
	}
	

	/**
	 * Gets Information about the WLAN Quality from the fritzbox
	 * querying mac (newer box firmware)
	 * 
	 * @param c
	 *            a valid context
	 * 
	 * @return the call log
	 */
	private WLANInfo getWLANInfoSpecific(Context c, int interfaceIndex, String mac)
			throws BaseException
	{
		try
		{
			WLANInfo result = new GetWLANStatus(new SoapCredentials(c),
					interfaceIndex, mac.toLowerCase(Locale.US)) .getQualifiedResult();
			if ((result != null) && result.isAuthState())
				return result;
		}
		catch (Exception exp)
		{
			exp.printStackTrace();
		}
		return null;
	}

	/**
	 * Gets the call log from the fritzbox
	 * 
	 * @param c
	 *            a valid context
	 * @param maxSize
	 *            max count of most recent entries
	 *            to get (use CALLLOG_ALL to get all)
	 * 
	 * @return the call log
	 */
	public CallLog getCallLog(Context c, int maxSize)
			throws DataMisformatException, BaseException, IOException
	{
		return new GetCallList(new SoapCredentials(c), maxSize)
				.getQualifiedResult();
	}

	/**
	 * Gets info on all configured TAMs asynchronously
	 * (multiple requests before completion will be aggregated)
	 * 
	 * @param c
	 *            a valid context
	 * @param listener
	 * 			  callback for receiving result
	 */
	public void getTamInfosAsync(final Context c,
			final OnCompleteListener<TamInfo[]> listener)
	{
		if (!mTamInfoRequests.add(listener)) return;

		// it's not running, so start it
		new Thread(new Runnable()
		{
			public void run()
			{
				// start requesting client info in parallel
				final TamInfo[] tamInfos = new TamInfo[MAX_TAMS];
				for (int ii = 0; ii < MAX_TAMS; ii++) tamInfos[ii] = null;
				final boolean[] aborted = new boolean[1];
				aborted[0] = false;
				for (int ii = 0; ii < MAX_TAMS; ii++)
				{
					new Thread(new GetItemRunnable(ii)
					{
						public void run()
						{
							TamInfo result = null;
							Exception error = null;
							try
							{
								result = new GetTamInfo(new SoapCredentials(c),
										mItemIndex).getQualifiedResult();
							}
							catch (IOException e)
							{
								// connection problem -> dummy not to be displayed
								Log.e(TAG, "Failed to get info on TAM index " +
										mItemIndex, e);
								result = new TamInfo(mItemIndex);
							}
							catch (Exception e) { error = e; }
							synchronized(tamInfos)
							{
								if (!aborted[0])
								{
									if (error != null)
									{
										aborted[0] = true;
										try
										{
											mTamInfoRequests.onComplete(null,
													error);
										}
										catch (Exception e) { }
									}
									else tamInfos[mItemIndex] = result;
									
									for (TamInfo info : tamInfos)
										if (info == null) return; // still pending
									// done
									try
									{
										mTamInfoRequests.onComplete(
												tamInfos.clone(), null);
									}
									catch (Exception e) { }
								}
							}
						}
					}).start();
				}
			}
		}).start();
	}

	/**
	 * Gets extended info on VoIP device
	 * 
	 * @param c
	 *            a valid context
	 * @return the extended info
	 */
	public VoIPInfoEx getVoIPConfInfoEx(Context c)
			throws DataMisformatException, BaseException, IOException
	{
		return new GetVoIPConfInfoEx(new SoapCredentials(c))
				.getQualifiedResult();
	}
	
	/**
	 * Gets number of configured VoIP clients
	 * 
	 * @param c
	 *            a valid context
	 * @return the number of configured VoIP clients
	 */
	public int getVoIPConfNumberOfClients(Context c)
			throws DataMisformatException, BaseException, IOException
	{
		return new GetVoIPConfNumberOfClients(new SoapCredentials(c))
				.getQualifiedResult().intValue();
	}
	
	/**
	 * Gets info on all configured VoIP client asynchronously
	 * (multiple requests before completion will be aggregated)
	 * 
	 * @param c
	 *            a valid context
	 * @param listener
	 * 			  callback for receiving result
	 */
	public void getVoIPConfClientsAsync(final Context c,
			final OnCompleteListener<VoIPClientInfo[]> listener)
	{
		if (!mVoIPClientsRequests.add(listener)) return;

		// it's not running, so start it
		new Thread(new Runnable()
		{
			public void run()
			{
				// get number of clients
				int number = 0;
				try
				{
					number = new GetVoIPConfNumberOfClients(new SoapCredentials(c))
							.getQualifiedResult().intValue();
				}
				catch(Exception e)
				{
					e.printStackTrace();
					try { mVoIPClientsRequests.onComplete(null, e); }
					catch (Exception exp) { }
					return;
				}
				if (number < 1)
				{
					try { mVoIPClientsRequests.onComplete(null, null); }
					catch (Exception e) { }
					return;
				}
				
				// start requesting client info in parallel
				final VoIPClientInfo[] clientInfos = new VoIPClientInfo[number];
				for (int ii = 0; ii < number; ii++) clientInfos[ii] = null;
				final boolean[] aborted = new boolean[1];
				aborted[0] = false;
				for (int ii = 0; ii < number; ii++)
				{
					new Thread(new GetItemRunnable(ii)
					{
						public void run()
						{
							VoIPClientInfo result = null;
							Exception error = null;
							try { result = getVoIPConfClientInfo(c, mItemIndex); }
							catch (Exception e)
							{
								e.printStackTrace();
								error = e;
							}
							synchronized(clientInfos)
							{
								if (!aborted[0])
								{
									if (error != null)
									{
										aborted[0] = true;
										try
										{
											mVoIPClientsRequests.onComplete(null,
													error);
										}
										catch (Exception e) { }
									}
									else clientInfos[mItemIndex] = result;
									
									for (VoIPClientInfo info : clientInfos)
										if (info == null) return; // still pending
									// done
									try
									{
										mVoIPClientsRequests.onComplete(
												clientInfos.clone(), null);
									}
									catch (Exception e) { }
								}
							}
						}
					}).start();
				}
			}
		}).start();
	}
	
	/**
	 * Gets info on an configured VoIP client
	 * 
	 * @param c
	 *            a valid context
	 * @param index
	 * 			  VoIP client index
	 * @return the VoIP client info
	 */
	public VoIPClientInfo getVoIPConfClientInfo(Context c, int index)
			throws DataMisformatException, BaseException, IOException
	{
		Tr064Capabilities capabilities = ComSettingsChecker.getTr064Capabilities();
		return  new GetVoIPConfClientInfo(new SoapCredentials(c), index,
				((capabilities != null) &&
						capabilities.has(Tr064Capabilities.Capability.VOIP_CONF_ID)))
				.getQualifiedResult();
	}

	/**
	 * Overwrites a VoIP client
	 * 
	 * @param c
	 *          a valid context
	 * @param index
	 * 			  VoIP client index
	 * @param password
	 * 			user password
	 * @param name
	 * 			  user friendly name of VoIP client
	 * @param outgoingNumber
	 * 			outgoing number or null
	 * @param id
	 * 			  additional id stored to config, ignored if not supported
	 * 			  by connected box
	 * @return true if successful
	 */
	public boolean setVoIPConfClientInfo(Context c, int index, String password,
			String name, String outgoingNumber, String id)
			throws DataMisformatException, BaseException, IOException
	{
		Tr064Capabilities capabilities = ComSettingsChecker.getTr064Capabilities();
		SetVoIPConfClientInfo soap = null;
		if ((capabilities != null) &&
				capabilities.has(Tr064Capabilities.Capability.VOIP_CONF_ID))
			soap = new SetVoIPConfClientInfo(new SoapCredentials(c), index,
					password, name, outgoingNumber, id);
		else
			soap = new SetVoIPConfClientInfo(new SoapCredentials(c), index,
					password, name, outgoingNumber);
		return soap.getQualifiedResult();
	}

	/**
	 * Add a VoIP client
	 * 
	 * @param c
	 *            a valid context
	 * @param password
	 * 			  user password
	 * @param name
	 * 			  user friendly name of VoIP client
	 * @param id
	 * 			  additional id stored to config, ignored if not supported
	 * 			  by connected box
	 * @return the added VoIP client's index or -1 if
	 * 		   MAX_VOIPCLIENTS has been reached already
	 */
	public int addVoIPConfClientInfo(Context c, String password, String name,
			String id)
			throws DataMisformatException, BaseException, IOException
	{
		int count = getVoIPConfNumberOfClients(c);
		if ((count < MAX_VOIPCLIENTS) && setVoIPConfClientInfo(c, count,
				password, name, null, id))
			return count;
		
		return -1;
	}

	/**
	 * Gets the fritzbox username of currently connected box.
	 * 
	 * @return the fritzbox user
	 */
	public static String getFritzboxUser()
	{
		String username = null;
		BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		if (boxInfo != null) username = boxInfo.getBoxUsername();
		if (TextUtils.isEmpty(username)) return SOAP_USERNAME;
		return username;
	}
	
	/**
	 * Gets the fritzbox password of currently connected box.
	 * 
	 * @return the fritzbox pass
	 */
	public static String getFritzboxPass()
	{
		BoxInfo boxInfo = ComSettingsChecker.getBoxInfo();
		return (boxInfo == null) ? "" : boxInfo.getBoxPassword();
	}

	/**
	 * Gets the trusted certificate of currently connected box.
	 * 
	 * @return the fritzbox certificate
	 */
	public static KeyStore getFritzboxCerts()
	{
		BoxFinder finder = ComSettingsChecker.getBoxFinder();
		if (finder.isInitialized())
			return finder.getBoxes().getCertificates();
		return null;
	}
}