package net.sf.fmj.media.protocol.httpauth;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.MediaLocator;
import javax.media.Time;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.PullDataSource;
import javax.media.protocol.PullSourceStream;
import javax.media.protocol.SourceCloneable;

import com.lti.utils.StringUtils;

import net.sf.fmj.utility.LoggerSingleton;

/**
 * HTTPAUTH (pseudo-) protocol handler DataSource.
 * This is a pseudo-protocol that allows the username and password for an http datasource to be specified in the URL.
 * Copied and modified from URLDataSource.
 * The syntax for an httpauth URL is derived from an http URL as follows:
 * http://myipcameraimage.com/cam
 * with username "user" and password "pass" would become
 * httpauth:user:pass@//myipcameraimage.com/cam
 * This is simply a convenience data source.  It allows things like FMJ studio to play IP camera streams that are password protected, without having to prompt the user.
 * 
 * @author Ken Larson
 *
 */
public class DataSource extends PullDataSource implements SourceCloneable
{
	private static final Logger logger = LoggerSingleton.logger;
	
	private URLConnection conn;

	private boolean connected = false;

	private String contentTypeStr;
	private ContentDescriptor contentType;

	protected URLSourceStream[] sources;
	
	
	public DataSource()
	{	super();
	}


	public DataSource(URL url)
	{	setLocator(new MediaLocator(url));
		
	}

	public javax.media.protocol.DataSource createClone()
	{
		final DataSource d;
		try
		{
			d = new DataSource(getLocator().getURL());
		} catch (MalformedURLException e)
		{
			logger.log(Level.WARNING, "" + e, e);
			return null;	// according to the API, return null on failure.
		}
		
		if (connected)
		{	
			try 
			{
				d.connect();
			} 
			catch (IOException e) 
			{	
				logger.log(Level.WARNING, "" + e, e);
				return null;	// according to the API, return null on failure.
			}
		}
		
		return d;	
	}
	
	@Override
	public PullSourceStream[] getStreams()
	{	
		if (!connected)
			throw new Error("Unconnected source.");
		return sources;
	}
	
	/** Strips trailing ; and anything after it.  Is generally only used for multipart content. */
	private String stripTrailer(String contentType)
	{	final int index = contentType.indexOf(";");
		if (index < 0)
			return contentType;
		final String result = contentType.substring(0, index);
		return result;
		
	}

	@Override
	public void connect() throws IOException
	{	
		// we allow a re-connection even if we are connected, due to an oddity in the way Manager works. See comments there 
		// in createPlayer(MediaLocator sourceLocator).
//
//		if (connected)
//			return;
		
		// example: httpauth:guest:guest@// + real url without http:
		final String remainder = getLocator().getRemainder();
		final int atIndex = remainder.indexOf('@');
		if (atIndex < 0)
			throw new IOException("Invalid httpauth url: expected: @");
		final int colonIndex = remainder.indexOf(':');
		if (colonIndex < 0 || colonIndex > atIndex)
			throw new IOException("Invalid httpaut url: expected: :");
		final String user = remainder.substring(0, colonIndex);
		final String pass = remainder.substring(colonIndex + 1, atIndex);
		
		final String realUrlStr = "http:" + getLocator().getRemainder().substring(atIndex + 1);
		
		conn = new URL(realUrlStr).openConnection();
		
		if (conn instanceof HttpURLConnection)
		{	// TODO: this is probably why JMF has explicit HTTP and FTP data sources - so we can check things explicitly.
			final HttpURLConnection huc = (HttpURLConnection) conn;
			if (user != null && !user.equals(""))
			{	huc.setRequestProperty("Authorization", "Basic " + StringUtils.byteArrayToBase64String((user + ":" + pass).getBytes()));
			}
			huc.connect();
			
			final int code = huc.getResponseCode();
			if (!(code >= 200 && code < 300))
			{	huc.disconnect();
				throw new IOException("HTTP response code: " + code);
			}	
			
			// TODO: what is the right place to apply ContentDescriptor.mimeTypeToPackageName?
			contentTypeStr = ContentDescriptor.mimeTypeToPackageName(stripTrailer(conn.getContentType()));
		}
		else
		{
			conn.connect();
			// TODO: what is the right place to apply ContentDescriptor.mimeTypeToPackageName?
			contentTypeStr = ContentDescriptor.mimeTypeToPackageName(conn.getContentType());
		}
		

		contentType = new ContentDescriptor(contentTypeStr);
		sources = new URLSourceStream[1];
		sources[0] = new URLSourceStream();
		                              
		connected = true;
	}

	@Override
	public String getContentType()
	{	return contentTypeStr;
	}

	@Override
	public void disconnect()
	{	if (!connected)
			return;
		if (conn != null)
		{
			if (conn instanceof HttpURLConnection)
			{
				final HttpURLConnection huc = (HttpURLConnection) conn;
				huc.disconnect();
			}
			// TODO: others
		}
		connected = false;
	}

	@Override
	public void start() throws java.io.IOException
	{	
		//throw new UnsupportedOperationException(); // TODO - what to do?
	}

	@Override
	public void stop() throws java.io.IOException
	{	//throw new UnsupportedOperationException(); // TODO - what to do?
	}

	@Override
	public Time getDuration()
	{	return Time.TIME_UNKNOWN;	// TODO: any case where we know the duration?
	}

	@Override
	public Object[] getControls()
	{	return new Object[0];
	}

	@Override
	public Object getControl(String controlName)
	{	return null;
	}

	class URLSourceStream implements PullSourceStream
	{

		private boolean endOfStream = false;
		public boolean endOfStream()
		{
			return endOfStream;
		}

		public ContentDescriptor getContentDescriptor()
		{
			return contentType;
		}

		public long getContentLength()
		{
			
			return conn.getContentLength();		// returns -1 if unknown, which is the same as LENGTH_UNKNOWN
		}

		public int read(byte[] buffer, int offset, int length) throws IOException
		{
			final int result = conn.getInputStream().read(buffer, offset, length);	// TODO: does this handle the requirement of not returning 0 unless passed in 0?
			if (result == -1) // end of stream
				endOfStream = true;
			
			return result;
		}

		public boolean willReadBlock()
		{
			try
			{
				return conn.getInputStream().available() <= 0;
			} catch (IOException e)
			{
				return true;
			}
		}

		public Object getControl(String controlType)
		{
			return null;
		}

		public Object[] getControls()
		{
			return new Object[0];
		}
	}
}
