/* 
 * Copyright 2015 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.tr064;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Properties;

import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.BasicManagedEntity;
import org.apache.http.conn.ManagedClientConnection;
import org.apache.http.impl.client.AbstractHttpClient;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

import android.text.TextUtils;

import de.avm.android.tr064.exceptions.BaseException;
import de.avm.android.tr064.exceptions.DataMisformatException;
import de.avm.android.tr064.net.DefaultHttpClientFactory;
import de.avm.android.tr064.net.GzipHttpRequestInterceptor;
import de.avm.android.tr064.net.GzipHttpResponseInterceptor;
import de.avm.android.tr064.net.IFingerprintPinningStore;
import de.avm.android.tr064.net.NetworkInterfaceAddressHelper;
import de.avm.android.tr064.net.SoapSSLClientFactory;
import de.avm.android.tr064.sax.SAXAppSetupScpdHandler;
import de.avm.android.tr064.sax.SAXDeviceConfigScpdHandler;
import de.avm.android.tr064.sax.SAXDeviceInfoScpdHandler;
import de.avm.android.tr064.sax.SAXHostsScpdHandler;
import de.avm.android.tr064.sax.SAXLanConfigSecurityScpdHandler;
import de.avm.android.tr064.sax.SAXMyFritzScpdHandler;
import de.avm.android.tr064.sax.SAXOnTelScpdHandler;
import de.avm.android.tr064.sax.SAXRemoteAccessScpdHandler;
import de.avm.android.tr064.sax.SAXScpdHandler;
import de.avm.android.tr064.sax.SAXTamScpdHandler;
import de.avm.android.tr064.sax.SAXTr064DescHandler;
import de.avm.android.tr064.sax.SAXUserInterfaceScpdHandler;
import de.avm.android.tr064.sax.SAXVoIPConfScpdHandler;
import de.avm.android.tr064.sax.SAXWanCommonIfaceConfigScpdHandler;
import de.avm.android.tr064.sax.SAXWanIPConnectionScpdHandler;
import de.avm.android.tr064.sax.SAXWanPPPConnection;
import de.avm.android.tr064.sax.SAXWlanConfScpdHandler;
import de.avm.android.tr064.soap.ISoapCredentials;

/**
 * Wrapper for jason_boxinfo.xml
 */
public class Tr064Boxinfo
{
	private static final String TAG = "Tr064Boxinfo";
	
	private static final String DEFAULT_SCHEME = "http";
	private static final int DEFAULT_PORT = 49000;
	private static final String DEFAULT_FILEPATH_TR64 = "/tr64desc.xml";
	private static final String REMOTE_SCHEME = "https";
	private static final String REMOTE_FILEPATH_PREFIX = "/tr064";
	
	private URI mUri = null;
	private String mLocalHostsAddress = null;
	private Tr064Capabilities mTr064Capabilities = new Tr064Capabilities();
	private String mUdn = "";
	private String mManufacturer = "";
	private String mModel = "";
	private String mFriendlyName = "";
	private String mWlanSignalRangeMin = "";
	private String mWlanSignalRangeMax = "";
	private int mWlanInterfacesCount = 0;

	private AbstractHttpClient mHttpClient = null;
	
	/**
	 * Creates instance from info retrieved from box within LAN (no authentication)
	 * @param uri URI of box' TR-064 description
	 * @param timeout timeout for HTTP requests (0 for default)
	 * @return URI to info file on host
	 * @throws URISyntaxException host is invalid
	 * @throws BaseException
	 * @throws DataMisformatException
	 */
	public static Tr064Boxinfo createInstance(URI uri, int timeout)
		throws  URISyntaxException, BaseException, DataMisformatException
	{
		return new Tr064Boxinfo(false, uri, null, null, null, timeout,
				Tr064Capabilities.ALL);
	}
	
	/**
	 * Creates instance from info retrieved from box within LAN (no authentication)
	 * @param uri URI of box' TR-064 description
	 * @param timeout timeout for HTTP requests (0 for default)
	 * @param capabilities check only for these capabilities
	 * (reduces SCDP loading/parsing)
	 * @return URI to info file on host
	 * @throws URISyntaxException host is invalid
	 * @throws BaseException
	 * @throws DataMisformatException
	 */
	public static Tr064Boxinfo createInstance(URI uri, int timeout,
			Tr064Capabilities capabilities)
		throws  URISyntaxException, BaseException, DataMisformatException
	{
		return new Tr064Boxinfo(false, uri, null, null, null, timeout,
				capabilities);
	}
	
	/**
	 * Creates default URI of box' TR-064 description for
	 * 'manual discovery'
	 * 
	 * @param host
	 * 		a host name or IP address
	 * @return
	 * 		the URI
	 * @throws IllegalArgumentException
	 * 		failed to create URI from host parameter
	 */
	public static URI createDefaultUri(String host)
	{
		try
		{
			return new URI(DEFAULT_SCHEME, "", host, DEFAULT_PORT,
					DEFAULT_FILEPATH_TR64, "", "");
		}
		catch (URISyntaxException e)
		{
			throw new IllegalArgumentException(e);
		}
	}
	
	/**
	 * Creates instance from info retrieved from box over LAN or WAN (SSL and authentication)
	 * @param credentials provides host, port an authentication info
	 * @param timeout timeout for HTTP requests (0 for default)
	 * @return URI to info file on host
	 * @throws URISyntaxException host is invalid
	 * @throws BaseException
	 * @throws DataMisformatException
	 */
	public static Tr064Boxinfo createInstance(ISoapCredentials credentials, int timeout)
		throws  URISyntaxException, BaseException, DataMisformatException
	{
		return createInstance(credentials, timeout, Tr064Capabilities.ALL);
	}
	
	/**
	 * Creates instance from info retrieved from box over LAN or WAN (SSL and authentication)
	 * @param credentials provides host, port an authentication info
	 * @param timeout timeout for HTTP requests (0 for default)
	 * @param capabilities check only for these capabilities
	 * (reduces SCDP loading/parsing)
	 * @return URI to info file on host
	 * @throws URISyntaxException host is invalid
	 * @throws BaseException
	 * @throws DataMisformatException
	 */
	public static Tr064Boxinfo createInstance(ISoapCredentials credentials, int timeout,
			Tr064Capabilities capabilities)
		throws  URISyntaxException, BaseException, DataMisformatException
	{
		if (credentials.isRemote())
		{
			URI uri = new URI(REMOTE_SCHEME, "", credentials.getHost(), credentials.getPort(true),
					REMOTE_FILEPATH_PREFIX + DEFAULT_FILEPATH_TR64, "", "");
			return new Tr064Boxinfo(true, uri, credentials.getUsername(), credentials.getPassword(),
					credentials.getPinningStore(), timeout, capabilities);
		}
		
		return new Tr064Boxinfo(false, createDefaultUri(credentials.getHost()), null, null,
				null, timeout, capabilities);
	}

	/**
	 * Gets URI of loaded description with IP address in it
	 * 
	 * @return the URI
	 */
	public URI getUri()
	{
		return mUri;
	}
	
	/**
	 * Gets the IP address the loaded description has been requested from
	 * (my IP address)
	 * 
	 * @return the IP address
	 */
	public String getLocalHostsAddress()
	{
		return mLocalHostsAddress;
	}
	
	public Tr064Capabilities getTr064Capabilities()
	{
		return mTr064Capabilities;
	}

	public String getUdn()
	{
		return mUdn;
	}

	public String getManufacturer()
	{
		return mManufacturer;
	}

	public String getModel()
	{
		return mModel;
	}
	
	public String getFriendlyName()
	{
		return mFriendlyName;
	}

	public String getWlanSignalRangeMin()
	{
		return mWlanSignalRangeMin;
	}

	public String getWlanSignalRangeMax()
	{
		return mWlanSignalRangeMax;
	}
	
	public int getWlanInterfacesCount()
	{
		return mWlanInterfacesCount;
	}
	
	/**
	 * Creates instance from XML string
	 * @param tr64Uri URI of box' TR-064 description
	 * @param username not null to set auth info
	 * @param password not null to set auth info
	 * @param pinningStore
	 * 		provides pinning config
	 * @param timeout timeout for HTTP requests (0 for default)
	 * @param capabilities capabilities to check for
	 * @throws URISyntaxException host or an uri in a description files is invalid
	 * @throws BaseException
	 * @throws FactoryConfigurationError Error creating XML parser
	 * @throws DataMisformatException
	 */
	private Tr064Boxinfo(boolean remote, URI tr64Uri, String username, String password,
			final IFingerprintPinningStore pinningStore, int timeout,
			Tr064Capabilities capabilities)
		throws URISyntaxException, BaseException, DataMisformatException
	{
		Tr064Log.d(TAG, "new instance for " + ((remote) ? "(remote) " : "") + tr64Uri);
		mUri = tr64Uri;

		// getting mLocalHostsAddress from HTTP connection may not work with HTTP proxies!
		// using Adbock Plus' proxy ManagedClientConnection.getLocalAddress() and
		// ManagedClientConnection.getRemoteAddress() both returning 127.0.0.1
		Properties properties = System.getProperties(); 
		String proxy = properties.getProperty("http.proxyHost");
		if (!TextUtils.isEmpty(proxy))
		{
			Tr064Log.d(TAG, "Not using proxy (" + proxy + ").");
			properties.setProperty("http.proxyHost", "");
		}

		// get local address for this
		InetAddress localHostsAddress = NetworkInterfaceAddressHelper
				.getInterfaceAddress(tr64Uri.getHost());
		if (localHostsAddress != null)
			mLocalHostsAddress = localHostsAddress.getHostAddress();
		
		try
		{
			if (tr64Uri.getScheme().equalsIgnoreCase(REMOTE_SCHEME))
			{
				if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password))
					mHttpClient = SoapSSLClientFactory.getClientWithDigestAuth(
							tr64Uri.getPort(), username, password, pinningStore,
							timeout);
				else
					mHttpClient = SoapSSLClientFactory.getClient(tr64Uri.getPort(),
							pinningStore, timeout);
			}
			else
			{
				// if not SSL, not using authentication!
				mHttpClient = DefaultHttpClientFactory.create(timeout);
			}
			
			mHttpClient.addRequestInterceptor(new GzipHttpRequestInterceptor());
			mHttpClient.addResponseInterceptor(new GzipHttpResponseInterceptor());

			if (mLocalHostsAddress == null)
			{
				mHttpClient.addRequestInterceptor(new HttpRequestInterceptor()
				{
					public void process(HttpRequest request, HttpContext context)
							throws HttpException, IOException
					{
						if (mLocalHostsAddress == null)
						{
							// get remote IP address
					        ManagedClientConnection conn = (ManagedClientConnection)context
			        				.getAttribute(ExecutionContext.HTTP_CONNECTION);
					        if (conn != null)
					        	mLocalHostsAddress = conn.getLocalAddress().getHostAddress();
						}
					}
				});
			}
			
			SAXParserFactory factory = SAXParserFactory.newInstance();
			factory.setNamespaceAware(true);
			SAXParser parser = factory.newSAXParser();
			XMLReader reader = parser.getXMLReader();
			SAXTr064DescHandler tr64Handler = new SAXTr064DescHandler();
			reader.setContentHandler(tr64Handler);
			DownloadInputSource inputSource = null;
			try
			{
				inputSource = new DownloadInputSource(tr64Uri);
				reader.parse(inputSource);
			}
			finally
			{
				if (inputSource != null) inputSource.release();
			}

			mUdn = tr64Handler.getUdn(); 
			mManufacturer = tr64Handler.getManufacturer(); 
			mModel = tr64Handler.getModel();
			mFriendlyName = tr64Handler.getFriendlyName();
			
			// OnTel1
			parseScpd(parser, new SAXOnTelScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getOnTelPath());
			
			// WLANConfiguration
			SAXWlanConfScpdHandler wlanConfHandler = new SAXWlanConfScpdHandler(); 
			if (parseScpd(parser, wlanConfHandler, capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getWlanConfPath()))
			{
				mWlanSignalRangeMin = wlanConfHandler.getSignalRangeMin();
				mWlanSignalRangeMax = wlanConfHandler.getSignalRangeMax();
				mWlanInterfacesCount = tr64Handler.getWlanInterfacesCount();
			}
			
			// VoIP1
			parseScpd(parser, new SAXVoIPConfScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getVoIPPath());
			
			// TAM1
			parseScpd(parser, new SAXTamScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getTamPath());
			
			// DeviceInfo1
			parseScpd(parser, new SAXDeviceInfoScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getDeviceInfoPath());
			
			// DeviceConfig1
			parseScpd(parser, new SAXDeviceConfigScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getDeviceConfigPath());
			
			// WANPPPConnection1
			parseScpd(parser, new SAXWanPPPConnection(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getWanPPPConnectionPath());
			
			// MyFritz
			parseScpd(parser, new SAXMyFritzScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getMyFritzPath());
			
			// LANConfigSecurity1
			parseScpd(parser, new SAXLanConfigSecurityScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getLanConfigSecurityPath());
			
			// WANCommonInterfaceConfig1
			parseScpd(parser, new SAXWanCommonIfaceConfigScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getWanCommonIfaceConfigPath());
			
			// RemoteAccess1
			parseScpd(parser, new SAXRemoteAccessScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getRemoteAccessPath());
			
			// WANIPConnection1
			parseScpd(parser, new SAXWanIPConnectionScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getWanIPConnectionPath());
			
			// UserInterface1
			parseScpd(parser, new SAXUserInterfaceScpdHandler(), capabilities, tr64Uri,
					((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getUserInterfacePath());

            // Hosts1
            parseScpd(parser, new SAXHostsScpdHandler(), capabilities, tr64Uri,
                    ((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getHostsPath());

            // Hosts1
            parseScpd(parser, new SAXAppSetupScpdHandler(), capabilities, tr64Uri,
                    ((remote) ? REMOTE_FILEPATH_PREFIX : "") + tr64Handler.getAppSetupPath());

			Tr064Log.d(TAG, tr64Uri.getHost() + " has " + mTr064Capabilities);
		}
		catch (FactoryConfigurationError exp)
		{
			throw new BaseException("Unhandled configuration Exception", exp);
		}
		catch (ParserConfigurationException exp)
		{
			throw new BaseException("Unhandled configuration Exception", exp);
		}
		catch (SAXException exp)
		{
			throw new DataMisformatException("Invalid Interface Description Data", exp);
		}
		catch (IOException exp)
		{
			throw new DataMisformatException("Invalid Interface Description Data", exp);
		}
		finally
		{
			mHttpClient = null;
		}
	}

	private boolean parseScpd(SAXParser parser, SAXScpdHandler saxHandler,
			Tr064Capabilities capabilities, final URI tr64Uri, String devicePath)
	{
		if (!TextUtils.isEmpty(devicePath))
		{
			DownloadInputSource inputSource = null;
			try
			{
				if (saxHandler.isCapabilitiyOfInterface(capabilities))
				{
					XMLReader reader = parser.getXMLReader();
					reader.setContentHandler(saxHandler);
					inputSource = new DownloadInputSource(tr64Uri.resolve(devicePath));
					reader.parse(inputSource);
					mTr064Capabilities.add(saxHandler.getCapabilities());
					return true;
				}
			}
			catch(Exception exp)
			{
				Tr064Log.e(TAG, "Failed to parse SCPD: " + devicePath, exp);
			}
			finally
			{
				if (inputSource != null) inputSource.release();
			}
		}

		return false;
	}
	
	private class DownloadInputSource extends InputSource
	{
		private HttpEntity mEntity;
		
		public DownloadInputSource(URI uri)
				throws IOException, IllegalStateException
		{
			if (mHttpClient == null)
				throw new IllegalStateException("No HTTP client instance available.");

			HttpResponse httpResponse = mHttpClient.execute(new HttpGet(uri));
			mEntity = httpResponse.getEntity(); 
			try
			{
				if (httpResponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
					setByteStream(mEntity.getContent());
			}
			catch(IOException e)
			{
				release();
				throw e;
			}
			catch(IllegalStateException e)
			{
				release();
				throw e;
			}
		}
		
		public void release()
		{
			if (mEntity != null)
			{
				try { mEntity.consumeContent(); }
				catch(Throwable e) { }
				if (BasicManagedEntity.class.isAssignableFrom(mEntity.getClass()))
				{
					try { ((BasicManagedEntity)mEntity).releaseConnection(); }
					catch(Throwable e) { }
				}
			}
		}
	}
}
