package net.sf.fmj.media.parser;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.media.BadHeaderException;
import javax.media.Buffer;
import javax.media.Duration;
import javax.media.Format;
import javax.media.IncompatibleSourceException;
import javax.media.ResourceUnavailableException;
import javax.media.Time;
import javax.media.Track;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.DataSource;
import javax.media.protocol.PullDataSource;
import javax.media.protocol.PullSourceStream;

import net.sf.fmj.media.AbstractDemultiplexer;
import net.sf.fmj.media.AbstractTrack;
import net.sf.fmj.media.PullSourceStreamInputStream;
import net.sf.fmj.utility.FormatArgUtils;
import net.sf.fmj.utility.LoggerSingleton;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

import com.lti.utils.StringUtils;
import com.lti.utils.synchronization.CloseableThread;
import com.lti.utils.synchronization.ProducerConsumerQueue;

/**
 * Parser for FMJ's XML movie format.
 * @author Ken Larson
 *
 */
public class XmlMovieParser extends AbstractDemultiplexer 
{
	private static final Logger logger = LoggerSingleton.logger;
	

	private ContentDescriptor[] supportedInputContentDescriptors = new ContentDescriptor[] {
			new ContentDescriptor("video.xml")
	};
	
	
	private PullDataSource source;
	
	private PullSourceStreamTrack[] tracks;
	
	public XmlMovieParser()
	{	super();
	}
	
	
	@Override
	public ContentDescriptor[] getSupportedInputContentDescriptors()
	{
		return supportedInputContentDescriptors;
	}

	
	@Override
	public Track[] getTracks() throws IOException, BadHeaderException
	{
		return tracks;
	}

	
	@Override
	public void setSource(DataSource source) throws IOException, IncompatibleSourceException
	{
		final String protocol = source.getLocator().getProtocol();
		
		if (!(source instanceof PullDataSource))
			throw new IncompatibleSourceException();
		
		this.source = (PullDataSource) source;
		
	}
	

	private XmlMovieSAXHandler xmlMovieSAXHandler;
	private XmlMovieSAXParserThread xmlMovieSAXParserThread;
	
	@Override
	public void open() throws ResourceUnavailableException
	{
		try
		{
			//source.connect(); // TODO: assume source is already connected
			source.start();	// TODO: stop/disconnect on stop/close.
		
			final PullSourceStream[] streams = source.getStreams();
			
			// only first stream supported.
			if (streams.length > 1)
				logger.warning("only 1 stream supported, " + streams.length + " found");
			
			final InputStream is = new PullSourceStreamInputStream(streams[0]);
			xmlMovieSAXHandler = new XmlMovieSAXHandler();
			xmlMovieSAXParserThread = new XmlMovieSAXParserThread(xmlMovieSAXHandler, is);
			xmlMovieSAXParserThread.start();	// TODO: stop when done
			
			Format[] formats = xmlMovieSAXHandler.readTracksInfo();
			
			
			tracks = new PullSourceStreamTrack[formats.length];
			
			for (int i = 0; i < formats.length; ++i)
			{
				tracks[i] = new VideoTrack(i, formats[i]);
	
			}
		
		} catch (IOException e)
		{
			logger.log(Level.WARNING, "" + e, e);
			throw new ResourceUnavailableException("" + e);
		} catch (SAXException e)
		{
			logger.log(Level.WARNING, "" + e, e);
			throw new ResourceUnavailableException("" + e);
		} catch (InterruptedException e)
		{
			logger.log(Level.WARNING, "" + e, e);
			throw new ResourceUnavailableException("" + e);
		}

		super.open();

	}

	@Override
	public void close()
	{

		if (tracks != null)
		{
			for (int i = 0; i < tracks.length; ++i)
			{
				if (tracks[i] != null)
				{
					tracks[i].deallocate();
					tracks[i] = null;
				}
			}
			tracks = null;
		}

		
		super.close();
	}

	// @Override
	@Override
	public void start() throws IOException
	{

	}
	
	// TODO: should we stop data source in stop?
//	// @Override
//	public void stop()
//	{
//		try 
//		{
//			source.stop();
//		} catch (IOException e) 
//		{
//			logger.log(Level.WARNING, "" + e, e);
//		}
//	}
	
	
	
	@Override
	public boolean isPositionable()
	{
		return false;	// TODO
	}
	
//	@Override
//	public Time setPosition(Time where, int rounding)
//	{
//	}

	
	@Override
	public boolean isRandomAccess()
	{
		return super.isRandomAccess();	// TODO: can we determine this from the data source?
	}
	
 

	private abstract class PullSourceStreamTrack extends AbstractTrack
	{
		public abstract void deallocate();

	}
	


	
	private class VideoTrack extends PullSourceStreamTrack
	{
		// TODO: track listener
		
		private final int track;
		private final Format format;
		

		
		public VideoTrack(int track, Format format) throws ResourceUnavailableException
		{
			super();
		
			this.track = track;
			this.format = format;
		}
		
		
		@Override
		public void deallocate()
		{
		}

		/**
		 * 
		 * @return nanos skipped, 0 if unable to skip.
		 * @throws IOException
		 */
		public long skipNanos(long nanos) throws IOException
		{
			return 0;	// TODO
			
		}
		
		public boolean canSkipNanos()
		{
			return false;
		}

		@Override
		public Format getFormat()
		{
			return format;
		}

		
		
		
		@Override
		public void readFrame(Buffer buffer)
		{
			Buffer b;
			try
			{
				b = xmlMovieSAXHandler.readBuffer(track);
			} catch (SAXException e)
			{
				throw new RuntimeException(e);
			} catch (IOException e)
			{
				throw new RuntimeException(e);
			} catch (InterruptedException e)
			{
				throw new RuntimeException(e);
			}
			buffer.copy(b);


		}

		
		@Override
		public Time mapFrameToTime(int frameNumber)
		{
			return TIME_UNKNOWN;	
		}

		@Override
		public int mapTimeToFrame(Time t)
		{	
			return FRAME_UNKNOWN;		
		}
		
	
		
		@Override
		public Time getDuration()
		{
			return Duration.DURATION_UNKNOWN;	// TODO
		}

	}

}


/**
 * SAX callback for FMJ's XML movie format.
 * @author Ken Larson
 *
 */
class XmlMovieSAXHandler extends DefaultHandler
{

	private final ProducerConsumerQueue qMeta = new ProducerConsumerQueue();

	private final Map<Integer, ProducerConsumerQueue> qBuffers = new HashMap<Integer, ProducerConsumerQueue>();

	private final Map<Integer, Format> formatsMap = new HashMap<Integer, Format>();

	private static String getStringAttr(Attributes atts, String qName) throws SAXException
	{
		final int index = atts.getIndex(qName);
		if (index < 0)
			throw new SAXException("Missing attribute: " + qName);
		return getStringAttr(atts, qName, null);
	}
	private static String getStringAttr(Attributes atts, String qName, String defaultResult) throws SAXException
	{
		final int index = atts.getIndex(qName);
		if (index < 0)
			return defaultResult;
		final String s = atts.getValue(index);
		return s;
	}
	
	private static int getIntAttr(Attributes atts, String qName) throws SAXException
	{
		final int index = atts.getIndex(qName);
		if (index < 0)
			throw new SAXException("Missing attribute: " + qName);
		return getIntAttr(atts, qName, 0);
	}
	private static int getIntAttr(Attributes atts, String qName, int defaultResult) throws SAXException
	{
		final int index = atts.getIndex(qName);
		if (index < 0)
			return defaultResult;
		final String s = atts.getValue(index);
		try
		{
			return Integer.parseInt(s);
		}
		catch (NumberFormatException e)
		{	throw new SAXException("Expected integer: " + s, e);
		}
	}
	
	private static long getLongAttr(Attributes atts, String qName) throws SAXException
	{
		final int index = atts.getIndex(qName);
		if (index < 0)
			throw new SAXException("Missing attribute: " + qName);
		return getLongAttr(atts, qName, 0);
	}
	private static long getLongAttr(Attributes atts, String qName, long defaultResult) throws SAXException
	{
		final int index = atts.getIndex(qName);
		if (index < 0)
			return defaultResult;
		final String s = atts.getValue(index);
		try
		{
			return Long.parseLong(s);
		}
		catch (NumberFormatException e)
		{	throw new SAXException("Expected long: " + s, e);
		}
	}
	
	private int currentTrack = -1;
	private Buffer currentBuffer;
	private StringBuilder currentDataChars;
	
	private int state = INIT;
	
	private static final int INIT = 0;
	private static final int AWAIT_BUFFER = 10;
	private static final int AWAIT_DATA = 11;
	private static final int READ_DATA = 12;
	
	
	@Override
	public void endDocument() throws SAXException
	{
		
		if (qBuffers != null)
		{
			for (ProducerConsumerQueue q : qBuffers.values())
			{
				if (q != null)
				{	
					final Buffer eomBuffer = new Buffer();
					eomBuffer.setEOM(true);
					// TODO: set format/
					try
					{
						q.put(eomBuffer);
					} catch (InterruptedException e)
					{
						throw new SAXException(e);
					}
				}
			}
		}
	}
	public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException
	{

		// TODO: verify element nesting
		try
		{
			if (localName.equals("XmlMovie"))
			{	final String version = atts.getValue(atts.getIndex("version"));
				if (!version.equals("1.0"))
					throw new SAXException("Expection XmlMovie version 1.0");
			}
			else if (localName.equals("Track"))
			{
				// TODO: catch exceptions
				final int index = getIntAttr(atts, "index");
				final String formatStr = getStringAttr(atts, "format");
				Format format = FormatArgUtils.parse(formatStr);
				formatsMap.put(index, format);
				qBuffers.put(index, new ProducerConsumerQueue());
			}
			
			else if (localName.equals("Buffer"))
			{
				currentTrack = getIntAttr(atts, "track");
				final long sequenceNumber = getLongAttr(atts, "sequenceNumber", Buffer.SEQUENCE_UNKNOWN);
				final long timeStamp = getLongAttr(atts, "timeStamp");
				final long duration = getLongAttr(atts, "duration", -1L);
				final int flags = getIntAttr(atts, "flags", 0);
				
				final String formatStr = getStringAttr(atts, "format", null);
				
				final Format format = formatStr == null ? formatsMap.get(currentTrack) : FormatArgUtils.parse(formatStr);

				Buffer buffer = new Buffer();
				buffer.setSequenceNumber(sequenceNumber);
				buffer.setTimeStamp(timeStamp);
				buffer.setDuration(duration);
				buffer.setFlags(flags);
				buffer.setFormat(format);
				
				currentBuffer = buffer;	// data will be set when we get the data element
				currentDataChars = new StringBuilder();
				state = AWAIT_DATA;
//					
//				b.append("<Data>");
//				b.append(StringUtils.byteArrayToHexString((byte[]) buffer.getData(), buffer.getLength(), buffer.getOffset()));
//				b.append("</Data>");
//				b.append("</Buffer>\n");
			}
			else if (localName.equals("Data"))
			{
				if (state != AWAIT_DATA)
					throw new SAXException("Not expecting Data element");
				state = READ_DATA;
			}
			// if (namespaceURI.equals("http://recipes.org") &&
			// localName.equals("ingredient")) {
			// String n = atts.getValue("","name");
			// if (n.equals("flour")) {
			// String a = atts.getValue("","amount"); // assume 'amount'
			// exists
			// amount = amount + Float.valueOf(a).floatValue();
			// }
			// }
		}
		catch (SAXException e)
		{	throw e;
		}
		catch (Exception e)
		{
			throw new SAXException(e);
		}
	}

	@Override
	public void characters(char[] ch, int start, int length) throws SAXException
	{
		if (state == READ_DATA)
		{
			String s = new String(ch, start, length);
			currentDataChars.append(s);
		}
		else
		{	
			String s = new String(ch, start, length);
			s = s.trim();
			if (s.length() > 0)
				throw new SAXException("characters unexpected, state=" + state + " chars=" + s);
		}
	}
	@Override
	public void endElement(String uri, String localName, String name) throws SAXException
	{
		if (localName.equals("Tracks"))
		{ 	// done with track info
			if (formatsMap.size() == 0)
				throw new SAXException("No tracks");
			try
			{
				final Format[] formatsArray = new Format[formatsMap.size()];
				for (int i = 0; i < formatsArray.length; ++i)
				{	
					final Format format = formatsMap.get(i);
					if (format == null)
						throw new SAXException("Expected format for track " + i);
					formatsArray[i] = format;
				
				}
				qMeta.put(formatsArray);
			} catch (InterruptedException e)
			{
				e.printStackTrace();
			}

		}
		else if (localName.equals("Data"))
		{
			byte[] data = StringUtils.hexStringToByteArray(currentDataChars.toString());
			currentBuffer.setData(data);
			currentBuffer.setOffset(0);
			currentBuffer.setLength(data.length);
			
			try
			{
				qBuffers.get(currentTrack).put(currentBuffer);
			} catch (InterruptedException e)
			{
				throw new SAXException(e);	// TODO: there should be an InterruptedSAXException...
			}
			currentBuffer = null;
			currentTrack = -1;
			currentDataChars = null;
			
			state = AWAIT_BUFFER;
		}
	}
	
	public void postError(Exception e) throws InterruptedException
	{
		if (qMeta != null)
			qMeta.put(e);
		if (qBuffers != null)
		{
			for (ProducerConsumerQueue q : qBuffers.values())
			{
				if (q != null)
					q.put(e);
			}
		}
		

	}
	
	public Format[] readTracksInfo() throws SAXException, IOException, InterruptedException
	{
		final Object o = qMeta.get();
		if (o instanceof Format[])
			return (Format[]) o;
		else if (o instanceof SAXException)
			throw (SAXException) o;
		else if (o instanceof IOException)
			throw (IOException) o;
		else
			throw new RuntimeException("Unknown object in queue: " + o);
		
	}
	
	public Buffer readBuffer(int track)  throws SAXException, IOException, InterruptedException
	{
		final Object o = qBuffers.get(track).get();
		if (o instanceof Buffer)
			return (Buffer) o;
		else if (o instanceof SAXException)
			throw (SAXException) o;
		else if (o instanceof IOException)
			throw (IOException) o;
		else
			throw new RuntimeException("Unknown object in queue: " + o);
	}

}

/**
 * Thread to parse FMJ's XML movie format, results handled by XmlMovieSAXHandler.  XmlMovieSAXHandler can be used to read parsed results.
 * @author Ken Larson
 *
 */
class XmlMovieSAXParserThread extends CloseableThread
{
	private final XmlMovieSAXHandler handler;
	private final InputStream is;
	

	public XmlMovieSAXParserThread(XmlMovieSAXHandler handler, InputStream is)
	{
		super();
		this.handler = handler;
		this.is = is;
	}


	public void run()
	{
		try
		{
			try
			{
				final XMLReader parser = XMLReaderFactory.createXMLReader();
				parser.setContentHandler(handler);
		
				parser.parse(new InputSource(is));
			}
			catch (SAXException e)
			{	handler.postError(e);
			} catch (IOException e)
			{
				handler.postError(e);
			}

		}
		catch (InterruptedException e)
		{	// exit thread
		}
		finally
		{	setClosed();
		}

	}
}

