/***********************************************************************
 * mt4j Copyright (c) 2008 - 2009, C.Ruff, Fraunhofer-Gesellschaft All rights reserved.
 *  
 *   This program 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 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 ***********************************************************************/
package org.mt4j.components;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;

import javax.swing.Timer;

import org.mt4j.components.clusters.Cluster;
import org.mt4j.components.clusters.ClusterManager;
import org.mt4j.components.interfaces.IMTComponent3D;
import org.mt4j.input.IHitTestInfoProvider;
import org.mt4j.input.inputData.MTInputEvent;
import org.mt4j.input.inputProcessors.componentProcessors.dragProcessor.DragProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.rotateProcessor.RotateProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.scaleProcessor.ScaleProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapProcessor;
import org.mt4j.util.camera.Icamera;
import org.mt4j.util.math.Matrix;

import processing.core.PApplet;
import processing.core.PGraphics;
import processing.core.PGraphics3D;

/**
 * MTCanvas is the root node of the component hierarchy of a MT4j scene.
 * To make a mt4j component visible and interactable you have to add it to
 * the scene's canvas or to a component which is already attached to the canvas.
 * The canvas then recursivly updates and draws all attached components each frame.
 * 
 * @author Christopher Ruff
 */
public class MTCanvas extends MTComponent implements IHitTestInfoProvider{
	
	/** The cluster manager. */
	private ClusterManager clusterManager;
	
	/** The last time hit test. */
	private long lastTimeHitTest;
	
	/** The cache time delta. */
	private long cacheTimeDelta;
	
	/** The cache clear time. */
	private int cacheClearTime;
	
	/** The position to component. */
	private HashMap<Position, IMTComponent3D> positionToComponent;
	
	/** The timer. */
	private Timer timer;
	
	/** The use hit test cache. */
	private boolean useHitTestCache;
	
	/** The frustum culling switch. */
	private boolean frustumCulling;
	
	private int culledObjects = 0;

	private long lastUpdateTime;
	
	

	
	
	/**
	 * The Constructor.
	 *
	 * @param pApplet the applet
	 * @param attachedCamera the attached camera
	 */
	public MTCanvas(PApplet pApplet, Icamera attachedCamera) {
		this(pApplet, "unnamed MT Canvas", attachedCamera);
	}

	/**
	 * The Constructor.
	 * 
	 * @param pApplet the applet
	 * @param name the name
	 * @param attachedCamera the attached camera
	 */
	public MTCanvas(PApplet pApplet, String name, Icamera attachedCamera) {
		super(pApplet, name, attachedCamera);
		//Cache settings
		lastTimeHitTest = 0;
		cacheTimeDelta = 100;
		cacheClearTime = 20000;
		useHitTestCache = true;
		
		lastUpdateTime = 0;
		
		clusterManager = new ClusterManager(pApplet, this);
		
		positionToComponent = new HashMap<Position, IMTComponent3D>();
		
		//Schedule a Timer task to clear the object cache so it wont 
		//get filled infinitely
		timer = new Timer(cacheClearTime, new ActionListener(){
			public void actionPerformed(ActionEvent arg0) {
//				System.out.println("Hit test chache entries: " + positionToComponent.size() + " ... cleared!");
				positionToComponent.clear();
			}
		});
//		timer.start(); //FIXME TEST 
		this.useHitTestCache = false; //FIXME TEST DISABLE HIT CACHE
		
//		this.setCollidable(false);
		
		this.setGestureAllowance(RotateProcessor.class, false);
		this.setGestureAllowance(ScaleProcessor.class, false);
		this.setGestureAllowance(TapProcessor.class, false);
		this.setGestureAllowance(DragProcessor.class, false);
		
		this.setPickable(false);
		
		//Frustum culling default
		frustumCulling = false;
	}
	
	@Override
	protected void destroyComponent() {
		super.destroyComponent();
		
		if (this.timer != null && timer.isRunning()){
			timer.stop();
		}
		
		if (positionToComponent != null){
			positionToComponent.clear();
		}
	}
	

	/**
	 * Method for asking the canvas whether and which object is at the specified
	 * screen position.
	 * <p>
	 * IMPORTANT: this method returns the MTCanvas instance if no other object is hit.
	 * This means that the MTCanvas instance acts like the background => Gestures that
	 * are supposed to be performed on the background have to check if they hit the canvas.
	 * And the gestureevents should then have the canvas as their targetComponent!
	 * Also, you have to be careful in other gestures, as even when you dont hit an object, you will
	 * get the mtcanvas returned as the hit component - not null! 
	 * <p>Note: if the hit component is part of a cluster, the cluster is returned!
	 * 
	 * @param x the screen x coordinate
	 * @param y the screen y coordinate
	 * 
	 * @return the object at that position or this MTCanvas instance if no component was hit
	 */
	public IMTComponent3D getComponentAt(float x, float y) { 
		IMTComponent3D closest3DComp = null;
		try{
			long now = System.currentTimeMillis();
			if (useHitTestCache){
				if (now - lastTimeHitTest > cacheTimeDelta){ //If the time since last check surpassed => do new hit-test!
					//Benchmark the picking
//					long a = System.nanoTime();
					closest3DComp = this.pick(x, y).getNearestPickResult();
					//Benchmark the picking
//					long b = System.nanoTime();
//					System.out.println("Time for picking the scene: " + (b-a));
					/*
					for (MTBaseComponent c : pickResult.getPickList())
						System.out.println(c.getName());
					if (closest3DComp != null)
						System.out.println("Using: " + closest3DComp.getName());
					*/
					if (closest3DComp == null){
						closest3DComp = this;
					}
					positionToComponent.put(new Position(x,y), closest3DComp);
				}else{
					//Check whats in the cache
					IMTComponent3D cachedComp = positionToComponent.get(new Position(x,y));
					if (cachedComp != null){ //Use cached obj
						closest3DComp = cachedComp;
						positionToComponent.put(new Position(x,y), closest3DComp);
					}else{
						closest3DComp = this.pick(x, y).getNearestPickResult();
						if (closest3DComp == null){
							closest3DComp = this;
						}
						positionToComponent.put(new Position(x,y), closest3DComp);
					}
				}
			}else{//IF no hittest cache is being used
				closest3DComp = this.pick(x, y).getNearestPickResult();
				if (closest3DComp == null){
					closest3DComp = this;
				}
			}
			lastTimeHitTest = now;
			
	//		/*//TODO anders machen..z.b. geclusterte comps einfach als kinder von
			//�bergeordnetem clusterpoly machen? aber mit clusterPoly.setComposite(TRUE);
			//Clusterpoly pickable machen damit das hier nicht gebraucht wird?
			Cluster sel = this.getClusterManager().getCluster(closest3DComp);
			  if (sel != null){
				  closest3DComp = sel;
			  }
	//		 */
			  
//			  //FIXME TEST for stencil clipped scene windows -> we have to return the scenes canvas for some gestures!
//			  if (closest3DComp != null && closest3DComp instanceof mtClipSceneWindow)
		  
		}catch(Exception e){
			System.err.println("Error while trying to pick an object: ");
			e.printStackTrace();
		}
		/*
		if (closest3DComp != null)
			System.out.println("Picked: '" + closest3DComp.getName() + "' at pos (" + x + "," + y + ")");
		else
			System.out.println("Picked: '" + closest3DComp + "' at pos (" + x + "," + y + ")");
		*/
		return closest3DComp;
	}
	
	
	/* (non-Javadoc)
	 * @see com.jMT.input.IHitTestInfoProvider#isBackGroundAt(float, float)
	 */
	public boolean isBackGroundAt(float x, float y) {
		return this.getComponentAt(x, y).equals(this);
	}
	
	
	//FIXME TEST	
	@Override
	public void updateComponent(long timeDelta) {
		super.updateComponent(timeDelta);
		this.lastUpdateTime = timeDelta;
	}
	//FIXME TEST
	private boolean calledFromDrawComponent = false;
	//FIXME TEST
//	/* 
//	 * Actually this canvases drawComponent method should be called
//	 * ever because the canvas should not be added as a child to any component.
//	 * Anyway - this code will still make it possible to use it as a child of other components
//	 * (non-Javadoc)
//	 * @see org.mt4j.components.MTComponent#drawComponent(processing.core.PGraphics)
//	 */
//	@Override
//	public void drawComponent(PGraphics g) { //FIXME this would draw the canvas 2 times..
//		super.drawComponent(g);
//		
//		//Call the canvases scenes draw method to also draw
//		//stuff defined in an overrriden scenes draw method
//		if (this.getRenderer() instanceof MTApplication){
//			MTApplication app = (MTApplication)this.getRenderer();
//			Iscene[] scenes = app.getScenes();
//			for (int i = 0; i < scenes.length; i++) {
//				Iscene iscene = scenes[i];
//				if (iscene instanceof AbstractScene){
//					AbstractScene as = (AbstractScene)iscene;
//					if (as.getCanvas().equals(this)){
//						this.calledFromDrawComponent = true;
////						this.drawAndUpdateCanvas(g, this.lastUpdateTime);
//						as.drawAndUpdate(g, this.lastUpdateTime);
//						this.calledFromDrawComponent = false;
//					}
//				}
//			}
//		}
//		
////		this.calledFromDrawComponent = true;
////		this.drawAndUpdateCanvas(g, this.lastUpdateTime);
////		this.calledFromDrawComponent = false;
//	}
	
	
	/**
	 * Updates and then draws every visible object in the canvas.
	 * First calls the <code>updateComponent(long timeDelta)</code> method. Then
	 * the <code>drawComponent()</code> method of each object in the scene graph.
	 * Also handles the setting of cameras attached to the objects.
	 * @param graphics 
	 * 
	 * @param updateTime the time passed since the last update (in ms)
	 */
	public void drawAndUpdateCanvas(PGraphics graphics, long updateTime){
		this.culledObjects = 0;
		
		//FIXME THIS IS A HACK! WE SHOULD REPLACE CLUSTERS WITH NORMAL COMPONENTS INSTEAD!
		//Update cluster components 
		Cluster[] clusters = getClusterManager().getClusters();
		for (int i = 0; i < clusters.length; i++) {
			Cluster cluster = clusters[i];
			cluster.updateComponent(updateTime);
		}
		
		this.drawUpdateRecursive(this, updateTime, graphics);
//		System.out.println("Culled objects: " + culledObjects);
	}

	
	/**
	 * Draw the whole canvas update recursive.
	 * 
	 * @param currentcomp the currentcomp
	 * @param updateTime the update time
	 * @param graphics the renderer
	 */
	private void drawUpdateRecursive(MTComponent currentcomp, long updateTime, PGraphics graphics){
		if (currentcomp.isVisible()){
			//Update current component
			currentcomp.updateComponent(updateTime);
			
			if (currentcomp.getAttachedCamera() != null){
				//Saves transformations up to this object
				graphics.pushMatrix();
				
				//Resets the modelview completely with a new camera matrix
				currentcomp.getAttachedCamera().update();
				
				if (currentcomp.getParent() != null){
					//Applies all transforms up to this components parent
					//because the new camera wiped out all previous transforms
					Matrix m = currentcomp.getParent().getGlobalMatrix();
					PGraphics3D pgraphics3D = (PGraphics3D)graphics;
					pgraphics3D.modelview.apply(
							m.m00, m.m01, m.m02,  m.m03,
							m.m10, m.m11, m.m12,  m.m13,
							m.m20, m.m21, m.m22,  m.m23,
							m.m30, m.m31, m.m32,  m.m33
					);
				}
				
				//Apply local transform etc
				currentcomp.preDraw(graphics);
				
				//Check visibility with camera frustum
				if (frustumCulling){
					if (currentcomp.isContainedIn(currentcomp.getViewingCamera().getFrustum())){
						if (!this.calledFromDrawComponent){ //FIXME TEST
						// DRAW THE COMPONENT  \\
						currentcomp.drawComponent(graphics);
						}
					}else{
						culledObjects++;
						//System.out.println("Not visible: " + currentcomp.getName());
					}
				}else{
					if (!this.calledFromDrawComponent){ //FIXME TEST
					// DRAW THE COMPONENT  \\
					currentcomp.drawComponent(graphics);
					}
				}
				
				currentcomp.postDraw(graphics);

				//Draw Children
				for (MTComponent child : currentcomp.getChildList())
					drawUpdateRecursive(child, updateTime, graphics);

				currentcomp.postDrawChildren(graphics);
				
				//Restores the transforms of the previous camera etc
				graphics.popMatrix(); 
			}else{//If no custom camera was set
				//TODO in abstactvisiblecomp wird outine �ber gradients und clips
				//gezeichnet obwohl hier invisble war! FIXME!
				//evtl applymatrix unapply in eigene methode? dann nur das ausf�hren, kein pre/post draw!
				
				//TODO vater an kinder listener -> resize - new geometry -> resize own 
				
				currentcomp.preDraw(graphics);
				
				if (frustumCulling){
					//Check visibility with camera frustum
					if (currentcomp.isContainedIn(currentcomp.getViewingCamera().getFrustum())){
						// DRAW THE COMPONENT  \\
						currentcomp.drawComponent(graphics);
					}else{
						culledObjects++;
						//System.out.println("Not visible: " + currentcomp.getName());
					}
				}else{
					// DRAW THE COMPONENT  \\
					currentcomp.drawComponent(graphics);
				}
				
				currentcomp.postDraw(graphics);
					
				for (MTComponent child : currentcomp.getChildList())
					drawUpdateRecursive(child, updateTime, graphics);
				
				currentcomp.postDrawChildren(graphics);
			}
		}//if visible end
	}

	
	/* (non-Javadoc)
	 * @see org.mt4j.components.MTComponent#processInputEvent(org.mt4j.input.inputData.MTInputEvent)
	 */
	@Override
	public boolean processInputEvent(MTInputEvent inEvt) {
		//TODO not very elegant - better approach??
		if (inEvt.hasTarget()){
			if (!inEvt.getTargetComponent().equals(this)){ //Avoid recursion
				//Send directed event to the target component
				return inEvt.getTargetComponent().processInputEvent(inEvt);
			}
		}
		
//		return true;  //this.handleEvent
		//handle in superclass
		
		//The MTCanvas get events targeted at him AND events that have no target!
		return super.processInputEvent(inEvt);
	}
	
		
	
	/**
	 * Gets the cluster manager.
	 * 
	 * @return the cluster manager
	 */
	public ClusterManager getClusterManager() {
		return clusterManager;
	}


	/**
	 * Sets the cluster manager.
	 * 
	 * @param selectionManager the new cluster manager
	 */
	public void setClusterManager(ClusterManager selectionManager) {
		this.clusterManager = selectionManager;
	}


	/**
	 * Gets the cache time delta.
	 * 
	 * @return the cache time delta
	 */
	public long getCacheTimeDelta() {
		return cacheTimeDelta;
	}

	/**
	 * If repeated calls to getObjectAt(float x, float y) in MTCanvas class
	 * are called during the provided cacheTimeDelta, the Canvas looks into his
	 * cache instead of querying all objects again
	 * Default value is: 80.
	 * 
	 * @param cacheTimeDelta the cache time delta
	 */
	public void setCacheTimeDelta(long cacheTimeDelta) {
		this.cacheTimeDelta = cacheTimeDelta;
	}

	/**
	 * Checks if is use hit test cache.
	 * 
	 * @return true, if is use hit test cache
	 */
	public boolean isUseHitTestCache() {
		return useHitTestCache;
	}

	
	/**
	 * The canvas can be set to look into a hit test cache if
	 * repeated calls to getComponentAt() with the same coordinates
	 * during a short period of time are made.
	 * This period of time can be set with
	 * <code>setCacheTimeDelta(long cacheTimeDelta)</code>
	 * <p>
	 * This is useful for example when a click is made many gestureanalyzers
	 * call getObjectAt() almost concurrently.
	 * 
	 * @param useHitTestCache the use hit test cache
	 */
	public void setUseHitTestCache(boolean useHitTestCache) {
		if (useHitTestCache && !timer.isRunning())
			timer.start();
		else if (!useHitTestCache && timer.isRunning())
			timer.stop();
		
		this.useHitTestCache = useHitTestCache;
	}


	/**
	 * Gets the cache clear time.
	 * 
	 * @return the cache clear time
	 */
	public int getCacheClearTime() {
		return cacheClearTime;
	}

	/**
	 * Sets the time intervals in ms in which the canvas clears its hit test cache
	 * Default value is: 20000 ms
	 * <p>
	 * This is important to prevent the hit test cache from growing indefinitely.
	 * 
	 * @param cacheClearTime the cache clear time
	 */
	public void setCacheClearTime(int cacheClearTime) {
		timer.setDelay(cacheClearTime);
		this.cacheClearTime = cacheClearTime;
	}


	public boolean isFrustumCulling() {
		return frustumCulling;
	}

	public void setFrustumCulling(boolean frustumCulling) {
		this.frustumCulling = frustumCulling;
	}
	
	
	

	
	
	/**
	 * Class used for the pickobject cache.
	 */
	private class Position{
		
		/** The y. */
		float x,y;
		
		/**
		 * Instantiates a new position.
		 * 
		 * @param x the x
		 * @param y the y
		 */
		public Position(float x, float y){
			this.x = x;
			this.y = y;
		}
		
		/**
		 * Gets the x.
		 * 
		 * @return the x
		 */
		public float getX() {return x;}
		
		/**
		 * Sets the x.
		 * 
		 * @param x the new x
		 */
		public void setX(float x) {this.x = x;}
		
		/**
		 * Gets the y.
		 * 
		 * @return the y
		 */
		public float getY() {return y;}
		
		/**
		 * Sets the y.
		 * 
		 * @param y the new y
		 */
		public void setY(float y) {this.y = y;}
		
		/* (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object arg0) {
			return (arg0 instanceof Position && ((Position)arg0).getX() == this.getX() && ((Position)arg0).getY() == this.getY());
		}
		
		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			return ((int)x+(int)y);
		}
	}
}
