/* 
 * 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.util.concurrent.Semaphore;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;

import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
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.com.discovery.BoxInfoList;
import de.avm.android.fritzapp.gui.SettingsExtendedActivity;
import de.avm.android.fritzapp.service.BoxService;
import de.avm.android.tr064.Tr064Boxinfo;
import de.avm.android.tr064.Tr064Capabilities;
import de.avm.android.tr064.Tr064Capabilities.Capability;
import de.avm.android.tr064.discovery.FritzBoxDiscovery;
import de.avm.android.tr064.exceptions.BaseException;
import de.avm.android.tr064.exceptions.DataMisformatException;
import de.avm.android.tr064.exceptions.SoapException;
import de.avm.android.tr064.exceptions.SslCertificateException;
import de.avm.android.tr064.exceptions.SslErrorException;
import de.avm.android.tr064.net.GzipHttpRequestInterceptor;
import de.avm.android.tr064.net.GzipHttpResponseInterceptor;
import de.avm.android.tr064.soap.GetAnonymousLogin;
import de.avm.android.tr064.soap.GetPhonebookList;
import de.avm.android.tr064.soap.GetSecurityPort;

/*
 * Testing the current status of connection
 */
public class ComSettingsChecker
{
	private static final String TAG = "ComSettingsChecker";

	public static final int SIP_NOTREGISTERED = 0;
	public static final int SIP_AWAY = 1;
	public static final int SIP_IDLE = 2;
	public static final int SIP_AVAILABLE = 3;

	private static final int MIN_VERSION_MAJOR = 4;
	private static final int MIN_VERSION_MINOR = 86;
	public static final Tr064Capabilities MANDATORY_TR064_CAPABILITIES;

	// for info and TR-064
	protected static BoxFinder mBoxFinder = new BoxFinder();
	protected static String mUdn = "";
	protected static ConnectionProblem mLastError = ConnectionProblem.FRITZBOX_MISSING;
	protected static String mUdnSwitchTo = "";
	protected static int mLocationSslPort = 0;
	protected static boolean mHasAnonymousLogin = true;
	protected static JasonBoxinfo mJasonBoxinfo = null;
	
	// for SIP client
	private static int mSipState = SIP_NOTREGISTERED;
//	private static String mSipError = "";
	
	// for network change detection
	private static Integer mLastCheckedNetworkType = null;
	private static String mLastCheckedSsid = null;
	
	static
	{
		MANDATORY_TR064_CAPABILITIES = new Tr064Capabilities();
		MANDATORY_TR064_CAPABILITIES.add(Capability.HTTPS);
		MANDATORY_TR064_CAPABILITIES.add(Capability.CALLLIST);
		MANDATORY_TR064_CAPABILITIES.add(Capability.PHONEBOOK);
		MANDATORY_TR064_CAPABILITIES.add(Capability.WLAN_CONF);
}

	public static boolean hasNetworkChanged(Context context)
	{
		boolean result = false;

		NetworkInfo info = ((ConnectivityManager)context
				.getSystemService(Context.CONNECTIVITY_SERVICE))
				.getActiveNetworkInfo();
		if ((info != null) && info.isConnectedOrConnecting() &&
				((info.getType() == ConnectivityManager.TYPE_WIFI) ||
					!SettingsExtendedActivity.isWlanOnly(context)))
		{
			if ((mLastCheckedNetworkType == null) ||
					(info.getType() != mLastCheckedNetworkType.intValue()))
			{
				result = true;
			}
			else if (info.getType() == ConnectivityManager.TYPE_WIFI)
			{
				WifiInfo wifiInfo = ((WifiManager)context
						.getSystemService(Context.WIFI_SERVICE))
						.getConnectionInfo();
				result = (mLastCheckedSsid == null) || (wifiInfo == null) ||
						!mLastCheckedSsid.equals(wifiInfo.getSSID());
			}
		}
		else result = (mLastCheckedNetworkType != null);
		
		return result;
	}
	
	public static void initialize(Context context)
	{
		clearBoxInfo();
		mBoxFinder.initialize(context.getApplicationContext());
	}

	public static BoxInfoList getBoxes()
	{
		return mBoxFinder.getBoxes();
	}
	
	public static BoxFinder getBoxFinder()
	{
		return mBoxFinder;
	}
	
	/**
	 * @return true if TR-064 or SIP connected
	 */
	public static boolean isConnected()
	{
		return (getBoxInfo() != null) || (mSipState == SIP_AVAILABLE);
	}
	
	public static ConnectionProblem getLastError()
	{
		return mLastError;
	}

	public static BoxInfo getBoxInfo()
	{
		if ((mUdn.length() > 0) && mBoxFinder.isInitialized())
			return mBoxFinder.getBoxes().get(mUdn, true);
		return null;
	}
	
	public static void SaveBoxInfo(Context context)
	{
		if (mBoxFinder.isInitialized())
			mBoxFinder.getBoxes().save(context.getApplicationContext());
	}
	
	/**
	 * Gets the UDN of the box found
	 * @return IP or domain name address
	 */
	public static String getUdn()
	{
		BoxInfo boxInfo = getBoxInfo();
		if (boxInfo != null) return boxInfo.getUdn();
		return "";
	}
	
	/**
	 * Gets Host address of box found
	 * @return IP or domain name address
	 */
	public static String getLocationHost()
	{
		BoxInfo boxInfo = getBoxInfo();
		if (boxInfo != null) return boxInfo.getLocation().getHost();
		return "";
	}
	
	/**
	 * Gets port for TR-064-SSL
	 * 
	 * @return the port
	 */
	public static int getLocationPort()
	{
		BoxInfo boxInfo = getBoxInfo();
		if (boxInfo != null) return boxInfo.getLocation().getPort();
		return 0;
	}
	
	/**
	 * Gets port for TR-064-SSL
	 * 
	 * @return the port
	 */
	public static int getLocationSslPort()
	{
		return mLocationSslPort;
	}
	
	/**
	 * Gets if username is needed for TR-064 authentication
	 * 
	 * @return true if no username needed
	 */
	public static boolean hasAnonymousLogin()
	{
		return mHasAnonymousLogin;
	}

	/**
	 * Get jason info of box found 
	 * @return JasonBoxinfo instance
	 */
	public static JasonBoxinfo getJasonBoxinfo()
	{
		return mJasonBoxinfo;
	}
	
	/**
	 * Gets my IP address in communication with connected box
	 * 
	 * @return IP address null
	 */
	public static String getMyIpAddress()
	{
		BoxInfo boxInfo = getBoxInfo();
		if (boxInfo != null)
		{
			Tr064Boxinfo tr64Info = boxInfo.getTr064Boxinfo();
			if (tr64Info != null) return tr64Info.getLocalHostsAddress();
		}
		return null;
	}

	/**
	 * @return TR-064 capability info
	 */
	public static Tr064Capabilities getTr064Capabilities()
	{
		BoxInfo boxInfo = getBoxInfo();
		if (boxInfo != null)
		{
			Tr064Boxinfo tr64Info = boxInfo.getTr064Boxinfo();
			if (tr64Info != null) return tr64Info.getTr064Capabilities();
		}
		return null;
	}
	
	public static int getSipState()
	{
		return mSipState;
	}

	public static void updateSipState(Context context, int sipState, String error)
	{
		Intent intent = new Intent(context, BoxService.class);
		intent.putExtra(BoxService.EXTRA_COMMAND,
				BoxService.Command.SETSIP.ordinal());
		intent.putExtra(BoxService.EXTRA_SIPSTATE, sipState);
		intent.putExtra(BoxService.EXTRA_SIPERROR, error);
		context.startService(intent);
	}
	
	/**
	 * To be called from service only!
	 * 
	 * @param sipState
	 * 		new state
	 * @param error
	 * 		error associated with state change
	 * @return
	 * 		true, if state has been changed
	 */
	public static boolean setSipState(int sipState, String error)
	{
		if (mSipState != sipState)
		{
			mSipState = sipState;
//			mSipError = error;
			return true;
		}
		return false;
	}
	
	/**
	 * Gets the UDN of the box to switch to if switch is pending
	 * @return IP or domain name address
	 */
	public static String getUdnSwitchTo()
	{
		return mUdnSwitchTo;
	}
	
	/**
	 * To be called from service only!
	 * 
	 * @param udn
	 * 		box to switch to
	 */
	public static void switchToBox(String udn)
	{
		mUdnSwitchTo = udn;
	}

	/**
	 * Check connection on Startup.
	 * 
	 * @param c
	 *            a valid context
	 * @param newSearch
	 *            true to invoke a new box discovery, false to use
	 *            results of last discovery, if available 
	 * 
	 * @return a ConnectionProblem or NO_PROBLEM if everything fine
	 */
	public static ConnectionProblem checkStartUpConnection(Context c,
			boolean newSearch)
	{
		ConnectionProblem result = checkNetworkConnectivity(c);
		if ((result == ConnectionProblem.OK) &&
				!isFritzBoxReachable(c, newSearch))
			result = ConnectionProblem.FRITZBOX_MISSING;
		if (result == ConnectionProblem.OK)
			result = isVersionCompatible(c);

		if (result == ConnectionProblem.OK)
		{
			Tr064Capabilities capabilities = getTr064Capabilities();
			if (capabilities == null)
				// might occur with weak WLAN, don't return FRITZBOX_NOTR064!!
				// try again later
				result = ConnectionProblem.FRITZBOX_MISSING;
			else if (capabilities.equals(Tr064Capabilities.EMPTY))
				result = ConnectionProblem.FRITZBOX_NOTR064;
			else if (!capabilities.has(MANDATORY_TR064_CAPABILITIES))
			{
				Log.d(TAG, "Mandatory capabilities failed. Available: " +
						capabilities);
				result = ConnectionProblem.FRITZBOX_VERSION;
			}
			else
				result = isConnectionPossible(c);
		}

		mLastError = result;
		switch (result)
		{
			case OK:
			case FRITZBOX_PASSWORD:
				return result;
				
			case CERTIFICATE_ERROR:
				if (SslCertificateException.getCertificateChain(
						result.getCause()) != null)
					return result;
				break;
		}
		
		clearBoxInfo();
		return result;
	}

	/**
	 * Checks network connection and connection type
	 * 
	 * @param context
	 * 		valid context
	 * @return
	 * 		NO_PROBLEM - connection ok
	 * 		WLAN_DISCONNECT - no network or no WLAN with WLAN-only
	 * 		WLAN_OFF - WLAN configured off with WLAN-only
	 */
	private static ConnectionProblem checkNetworkConnectivity(Context context)
	{
		ConnectionProblem result = ConnectionProblem.WLAN_DISCONNECT;
		mLastCheckedSsid = null;
		
		NetworkInfo networkInfo = ((ConnectivityManager)context
				.getSystemService(Context.CONNECTIVITY_SERVICE))
				.getActiveNetworkInfo();
		if ((networkInfo != null) && networkInfo.isConnectedOrConnecting())
		{
			mLastCheckedNetworkType = Integer.valueOf(networkInfo.getType());
			if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI)
			{
				WifiInfo wifiInfo = ((WifiManager)context
						.getSystemService(Context.WIFI_SERVICE))
						.getConnectionInfo();
				if (wifiInfo != null) mLastCheckedSsid = wifiInfo.getSSID();
				result = ConnectionProblem.OK;
			}
			else if (SettingsExtendedActivity.isWlanOnly(context))
			{
				if (!((WifiManager)context.getSystemService(Context.WIFI_SERVICE))
						.isWifiEnabled())
					result = ConnectionProblem.WLAN_OFF;
			}
			else result = ConnectionProblem.OK;
		}
		else mLastCheckedNetworkType = null;

		if (result != ConnectionProblem.OK)
		{
			mBoxFinder.cancelSearch();
			mBoxFinder.getBoxes().reset();
		}
		
//		Log.d(TAG, "checkNetworkConnectivity() - mLastCheckedNetworkType == " +
//				mLastCheckedNetworkType);
//		Log.d(TAG, "  mLastCheckedSsid == " + mLastCheckedSsid);
//		Log.d(TAG, "  return: " + result);
		return result;
	}

	/**
	 * Discovers FRITZ!Boxes on the network.
	 * Gets IP/Port of configured box. If no box configured, select
	 * automatically, if only one available.
	 * 
	 * @param c
	 *            a valid context
	 * @param newSearch
	 *            if true or currently not connected invoke a new box discovery, otherwise use
	 *            results of last discovery, if available
	 * 
	 * @return true, if FRITZ!Box is reachable
	 */
	protected static boolean isFritzBoxReachable(Context c, boolean newSearch)
	{
		// if currently not connected do a new search
		newSearch |= TextUtils.isEmpty(mUdn);
		
 		// get available boxes
		// TODO re-design for not using synchronous searchFritzboxes() 
		BoxInfoList boxes = searchFritzboxes(newSearch);
		
		BoxInfo selected = null;
		if (boxes.getCountOfUsables() > 0)
		{
			if (!TextUtils.isEmpty(mUdnSwitchTo))
			{
				// switch to another box
				selected = boxes.get(mUdnSwitchTo, true);
			}
			else if (!TextUtils.isEmpty(mUdn))
			{
				// currently selected box still available?
				selected = boxes.get(mUdn, true);
				if ((selected != null) &&
						(!selected.isAvailable() || !selected.isAutoConnect()))
					selected = null;
			}

			if (selected == null)
			{
				// the lately connected of the available boxes
				selected = boxes.getUsableMru();
			}

			if ((selected == null) && !boxes.hasPreferences())
			{
				String[] udns = boxes.usablesToArray();
				if (udns.length == 1)
					// have never been connected before, one available only -> take it
					selected = boxes.get(udns[0], true);
			}
		}
		
		if (selected != null)
		{
			// check for TR-064 features needed
			Tr064Boxinfo tr64Info = selected.getTr064Boxinfo(); 
			if (tr64Info == null)
			{
				try
				{
					tr64Info = Tr064Boxinfo.createInstance(selected.getLocation()
							.toURI(), FritzBoxDiscovery.DEFAULT_TIMEOUT);
					selected.updateTr064Boxinfo(tr64Info);
				}
				catch(Exception exp)
				{
					exp.printStackTrace();
					selected = null;
				}
			}
		}

		mUdnSwitchTo = "";
		if (selected != null)
		{
			mUdn = selected.getUdn();
			boxes.updateMru(c, selected);
			return true;
		}
		mUdn = "";
		return false;
	}

	/**
	 * Checks if the version of the FRITZ!Box is compatible.
	 * 
	 * @param c
	 *            a valid context
	 * 
	 * @return NO_PROBLEM, if connection is possible
	 */
	protected static ConnectionProblem isVersionCompatible(Context c)
	{
		ConnectionProblem result = ConnectionProblem.FRITZBOX_MISSING;
		
		BoxInfo boxInfo = mBoxFinder.getBoxes().get(mUdn, true);
		if (boxInfo == null) return result;

		JasonBoxinfo jasonBoxInfo = null;
		String locationHost = boxInfo.getLocation().getHost();
		if (locationHost.length() > 0)
		{
//			Log.d(TAG, "isVersionCompatible: locationHost==" + locationHost);
			DefaultHttpClient client = null;
			try
			{
//				Log.d(TAG, "isVersionCompatible: JasonBoxinfo.createUri==" +
//						JasonBoxinfo.createUri(locationHost).toString());
				// Info über Box holen
				client = new DefaultHttpClient();
				client.addRequestInterceptor(new GzipHttpRequestInterceptor());
				client.addResponseInterceptor(new GzipHttpResponseInterceptor());
				HttpResponse httpResponse = client
						.execute(new HttpGet(JasonBoxinfo.createUri(locationHost)));
				if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
					jasonBoxInfo = new JasonBoxinfo(EntityUtils
							.toString(httpResponse.getEntity()));
			}
			catch(Exception exp)
			{
				exp.printStackTrace();
				result.setCause(exp);
			}
			finally
			{
				if (client != null) client.getConnectionManager().shutdown();
			}
		}

		if (jasonBoxInfo != null)
		{
			// check for minimal version
			try
			{
				if (jasonBoxInfo.compareVersion(MIN_VERSION_MAJOR,
						MIN_VERSION_MINOR) < 0)
				{
					Log.d(TAG, "FRITZ!Box firmware too old: " +
							jasonBoxInfo.getVersion());
					result = ConnectionProblem.FRITZBOX_VERSION;
					jasonBoxInfo = null;
				}
				else result = ConnectionProblem.OK;
			}
			catch(Exception exp)
			{
				Log.w(TAG, "Invalid FRITZ!Box firmware version", exp);
				jasonBoxInfo = null; // invalid content?
				result.setCause(exp);
			}
		}

		mJasonBoxinfo = jasonBoxInfo;
		return result;
	}

	/**
	 * Checks if a qualified connection to the SOAP endpoint is possible.
	 * Has to be called before TR-064 could be used (gets SSL port from box).
	 * Only needed if TR-064 will be used.
	 * 
	 * @param c
	 *            a valid context
	 * 
	 * @return NO_PROBLEM, if connection is possible
	 */
	protected static ConnectionProblem isConnectionPossible(Context c)
	{
		ConnectionProblem result = ConnectionProblem.FRITZBOX_MISSING;
		try
		{
			// SSL port
			DataHub.SoapCredentials soapCredentials = new DataHub.SoapCredentials(c);
			mLocationSslPort = new GetSecurityPort(soapCredentials)
					.getQualifiedResult();
			
			// login type
			Tr064Capabilities capabilities = getTr064Capabilities();
			if ((capabilities != null) && capabilities.has(
					Tr064Capabilities.Capability.SSO))
				mHasAnonymousLogin = new GetAnonymousLogin(soapCredentials)
						.getQualifiedResult();
			else
				mHasAnonymousLogin = true;
			
			// probe credentials
			new GetPhonebookList(soapCredentials).getQualifiedResult();
			result = ConnectionProblem.OK;
		}
		catch (SoapException exp)
		{
			// valid response with SOAP error
			if (SoapException.SOAPERROR_NOT_AUTHORIZED.equals(exp.getSoapError()))
			{
				result = ConnectionProblem.FRITZBOX_PASSWORD;
			}
			else
			{
				// guessing interfaces not fully implemented
				result = ConnectionProblem.FRITZBOX_NOTR064;
				BoxInfo boxInfo = mBoxFinder.getBoxes().get(mUdn, false);
				if (boxInfo != null) boxInfo.updateTr064Boxinfo(null);
			}
			result.setCause(exp);
			exp.printStackTrace();
		}
		catch (DataMisformatException exp)
		{
			// valid SOAP response with unexpected content
			// guessing interfaces not fully implemented
			result = ConnectionProblem.FRITZBOX_NOTR064;
			result.setCause(exp);
			BoxInfo boxInfo = mBoxFinder.getBoxes().get(mUdn, false);
			if (boxInfo != null) boxInfo.updateTr064Boxinfo(null);
			exp.printStackTrace();
		}
		catch (SslErrorException exp)
		{
			// bug in SSL on Android 2.2 (broken pipe on closing SSL connection)
			// see http://code.google.com/p/android/issues/detail?id=8625
			result =  (SslErrorException.isCertificateError(exp)) ?
					ConnectionProblem.CERTIFICATE_ERROR : ConnectionProblem.SSL_ERROR;
			result.setCause(exp);
			exp.printStackTrace();
		}
		catch (BaseException exp)
		{
			// no SOAP response
			// guessing interfaces not available but box is
			result = ConnectionProblem.FRITZBOX_NOTR064;
			result.setCause(exp);
			BoxInfo boxInfo = mBoxFinder.getBoxes().get(mUdn, false);
			if (boxInfo != null) boxInfo.updateTr064Boxinfo(null);
			if ((exp.getCause() != null) &&
				HttpResponseException.class.equals(exp.getCause().getClass()))
			{
				HttpResponseException responseExp = (HttpResponseException)exp.getCause(); 
				if (responseExp.getStatusCode() == HttpStatus.SC_UNAUTHORIZED)
				{
					result = ConnectionProblem.FRITZBOX_PASSWORD;
					result.setCause(responseExp);
				}
				else exp.printStackTrace();
			}
			else exp.printStackTrace();
		}
		catch (Exception exp)
		{
			// or no response
			// some problem with connection
			result.setCause(exp);
			exp.printStackTrace();
		}
		return result;
	}
	
	/**
	 *	Clears all box info data
	 */
	public static void clearBoxInfo()
	{
		mUdn = "";
		mUdnSwitchTo = "";
		mLocationSslPort = 0;
		mJasonBoxinfo = null;
		mLastCheckedNetworkType = null;
		mLastCheckedSsid = null;
	}
	
	// TODO here we need it synchronously, should be re-designed for using it asynchronously 
	private static BoxInfoList searchFritzboxes(boolean newSearch)
	{
		if (!mBoxFinder.hasSearched() || newSearch)
		{
			final Semaphore sema = new Semaphore(0);
			try
			{
				mBoxFinder.startSearch(new BoxFinder.OnSearchDoneListener()
				{
					public void onSearchDone()
					{
						sema.release();
					}
				}, null);
				sema.acquire();
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}
		}

		return mBoxFinder.getBoxes();
	}
}