/**
 * This package provides classes to handle video in Processing. The API is compatible with the built-in video library of Processing. 
 * GSVideo uses the multimedia toolkit GStreamer (http://www.gstreamer.net/)  through the gstreamer-java bindings by Wayne Meissener:
 * http://code.google.com/p/gstreamer-java/ 
 * @author Andres Colubri
 * @version 0.4.5
 *
 * Copyright (c) 2008 Andres Colubri
 *
 * This source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This code 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 GNU
 * General Public License for more details.
 * 
 * A copy of the GNU General Public License is available on the World
 * Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also
 * obtain it by writing to the Free Software Foundation,
 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package codeanticode.gsvideo;

import processing.core.*;

import java.nio.*;
import java.lang.reflect.*;
import java.util.ArrayList;

import org.gstreamer.*;
import org.gstreamer.elements.*;

/**
 * This class allows to create a custom GStreamer pipeline.
 */
public class GSPipeline extends PImage implements PConstants 
{
    /**
     * Creates an instance of GSPipeline using the pipeline specified in the string pipe.
     * @param parent PApplet
     * @param pipe String 
     */
    public GSPipeline(PApplet parent, String pipe) 
    {
        // this creates a fake image so that the first time this
        // attempts to draw, something happens that's not an exception
        super(1, 1, RGB);
		
        this.parent = parent;

        gpipe = null;
	
        GSVideo.init();

        // register methods
        parent.registerDispose(this);

        try 
        {
            customEventMethod = parent.getClass().getMethod("customEvent", new Class[] { GSPipeline.class });
        } 
        catch (Exception e) 
        {
            // no such method, or an error.. which is fine, just ignore
        }
	  
        gpipe = new Pipeline("GSPipelinePipeline");
		
        RGBDataSink videoSink = new RGBDataSink("rgb",
            new RGBDataSink.Listener() 
            {
                public void rgbFrame(int w, int h, IntBuffer buffer) 
                {
                    invokeEvent(w, h, buffer);
                }
            }	
        );
		
        ArrayList elems = new ArrayList();
        ArrayList propNames = new ArrayList();
        ArrayList propTypes = new ArrayList();
        ArrayList propValues = new ArrayList();
  
        // Splitting the pipe string into its constituent elements.
        int n0 = 0;
        int n = pipe.indexOf("!", n0);
        while (-1 < n)
        {
            extractElements(n0, n, pipe, elems, propNames, propTypes, propValues);
            n0 = n + 1;
            n = pipe.indexOf("!", n0);
        }
        extractElements(n0, pipe.length(), pipe, elems, propNames, propTypes, propValues);
		
        // Creating the array of elements and caps.
        String elem, lastElem;
        boolean fakeSink;
        boolean fileSink;
        boolean isCaps[];
        Element elements[];

		int elemNum = elems.size();
        lastElem = (String)elems.get(elemNum - 1);
        fakeSink = lastElem.equals("fakesink");
        fileSink = lastElem.equals("filesink");
         
        if (!(fakeSink || fileSink))
        {
            elements = new Element[elemNum + 1];       
            isCaps = new boolean[elemNum + 1];
            //PApplet.println("elemNum: " + elemNum + " elements.length: " + elements.length);
        }
        else
        {	
            elements = new Element[elemNum];
            isCaps = new boolean[elemNum];
        }
        
        for (int i = 0; i < elemNum; i++)
        {
            elem = (String)elems.get(i);

            if (elem.startsWith("video/"))
            {
                // Element ith is a cap.
                isCaps[i] = true;
				//parent.println("Creating capsfilter " + elem);				
                elements[i] = ElementFactory.make("capsfilter", "element" + i + "-capsfilter");
            }
            else
            {
                isCaps[i] = false;
				//parent.println("Creating element " + elem + " " + i);
                elements[i] = ElementFactory.make(elem, "element" + i + "-" + elem);
            }
        }
		
        // If the pipeline ends in a fakesink or filesink element, the RGBDataSink is not added at the end of it...
        if (!(fakeSink || fileSink))
        {
        	// ...otherwise it is.
        	
            // The last element is the video sink.
        	//PApplet.println("elemNum: " + elemNum + " elements.length: " + elements.length);
		    elements[elemNum] = videoSink;		    
		    isCaps[elemNum] = false;
		    elems.add("videoSink");
		    
		    elemNum++;
        }
        
        propNames.add(new ArrayList());
        propTypes.add(new ArrayList()); 
        propValues.add(new ArrayList()); 

        // Linking the elements to one another and adding them to the pipeline.
        Element src, dest;
        src = elements[0];
        gpipe.add(src);
        setProperties(src, (ArrayList)propNames.get(0), (ArrayList)propTypes.get(0), (ArrayList)propValues.get(0));
		elem = (String)elems.get(0);

        for (int i = 1; i < elemNum; i++)
        {
            dest = elements[i];
		    elem = (String)elems.get(i);
			
			gpipe.add(dest);
			
            src.link(dest);

            setProperties(dest, (ArrayList)propNames.get(i), (ArrayList)propTypes.get(i), (ArrayList)propValues.get(i));
			
			if (isCaps[i]) 
            {
                elem = (String)elems.get(i);
                dest.setCaps(new Caps(elem));
            }
			
            src = dest;
	    }

        gpipe.setState(State.PLAYING); 
    }

    /**
     * Returns "true" when a new video frame is available to read.
     * @return String[]
     */  
    public boolean available() 
    {
        return available;
    }

    /**
     * Reads the current video frame.
     */
    public void read() 
    {
        if (firstFrame) 
        {
            super.init(pipeWidth, pipeHeight, RGB);
            loadPixels();
            firstFrame = false;
        }		
        pipePixels.get(pixels);
        updatePixels();
	
        available = false;
    }	

    /**
     * Resumes the pipeline.
     */
    public void resume()
    {
        gpipe.setState(State.PLAYING);
    }

    /**
     * Stops the pipeline.
     */  
    public void stop()
    {
        gpipe.setState(State.PAUSED);  
    }

    /**
     * @invisible
     */
    protected void invokeEvent(int w, int h, IntBuffer buffer) 
    {
        available = true;  
        pipeWidth = w;
        pipeHeight = h;
        pipePixels = buffer;
        //parent.println("getting frame: " + w + " " + h);
	  
        // Creates a customEvent.
        if (customEventMethod != null) 
        {
            try 
            {
                customEventMethod.invoke(parent, new Object[] { this });
            } 
            catch (Exception e) 
            {
                System.err.println("error, disabling customEvent() for GSPipeline");
                e.printStackTrace();
                customEventMethod = null;
            }
        }
    }

    /**
     * @invisible
     */
    public void dispose() 
    {
    }

    /**
     * @invisible
     */
    protected void extractElements(int n0, int n, String pipe, ArrayList elems, ArrayList propNames, ArrayList propTypes, ArrayList propValues)
    {
        String elem = pipe.substring(n0, n).trim();
   
        ArrayList elemPropNames = new ArrayList();
        ArrayList elemPropTypes = new ArrayList();
        ArrayList elemPropValues = new ArrayList();

        String[] strList;
        if (elem.startsWith("video/"))
        {
            // The element is a cap filter. Don't split.
            strList = new String[1];
            strList[0] = elem;
        }
        else
        {
            String[] prop;
            strList = elem.split(" ");
            String value;

            for (int i = 1; i < strList.length; i++)
            {
                prop = strList[i].split("=");
                elemPropNames.add(prop[0]);
                value = prop[1];

                
                if (isIntNumber(value))
                {
                    elemPropTypes.add(new Integer(1));
                    elemPropValues.add(new Integer(value));
                    //parent.println("Integer " + prop[0] + " " + value);
                }              
                else
                {
                    elemPropTypes.add(new Integer(0));
                    elemPropValues.add(value);
                    //parent.println("String " + prop[0] + " " + value);
                }
            }
        }

        elems.add(strList[0]);
        propNames.add(elemPropNames);  
        propTypes.add(elemPropTypes);
        propValues.add(elemPropValues);
    }

    /**
     * @invisible
     */
    protected void setProperties(Element elem, ArrayList elemPropNames, ArrayList elemPropTypes, ArrayList elemPropValues)
    {
        for (int i = 0; i < elemPropNames.size(); i++)
        {
            int t = ((Integer)elemPropTypes.get(i)).intValue();
            if (t == 1)
            {
                int val = ((Integer)elemPropValues.get(i)).intValue();
                elem.set((String)elemPropNames.get(i), val);
            }
            else
            {
                String val = (String)elemPropValues.get(i);
                elem.set((String)elemPropNames.get(i), val);
            }
        }
    }

    /**
     * @invisible
     */
    protected boolean isIntNumber(String value)
    {
    	try 
    	{
    		int i = Integer.parseInt(value);
    		return true;
        }
    	catch (NumberFormatException nfe) 
    	{
    		return false;
    	}
    	
    	    	
    	
    	/*
    	PApplet.println("isIntNumber: " + value);

    	
    	for (int i = 0; i < value.length(); i++)
    	{
    	    char c = value.charAt(i);
    	    if (!((c == '0') || (c == '1') || (c == '2') || (c == '3') || (c == '4') ||
    	          (c == '5') || (c == '6') || (c == '7') || (c == '8') || (c == '9') ||
    	          ((c == '-') && (i == 0))))
    	    {
    	    	PApplet.println("not int");
    	    	return false;
    	    }	
    	    
    	}
    	return true;
    	*/
    	

    	/*
        if (value.equals("0")) return true;
        else return Integer.parseInt(value) != 0; // parseInt returns 0 if the string contains alphabetic characters.
        */
    }

    /**
     * @invisible
     */
    Method customEventMethod;

    /**
     * @invisible
     */
    boolean available;

    /**
     * @invisible
     */
    boolean firstFrame = true;
  
    /**
     * @invisible
     */
    int pipeWidth;

    /**
     * @invisible
     */
    int pipeHeight;

    /**
     * @invisible
     */
    IntBuffer pipePixels;
  
    /**
     * @invisible
     */
    Pipeline gpipe;
	
    /**
     * @invisible
     */
	Element elements[];
}
