/**
 * Squidy Interaction Library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 * 
 * Squidy Interaction Library 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Squidy Interaction Library. If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * 2009 Human-Computer Interaction Group, University of Konstanz.
 * <http://hci.uni-konstanz.de>
 * 
 * Please contact info@squidy-lib.de or visit our website
 * <http://www.squidy-lib.de> for further information.
 */

package org.squidy.designer.knowledgebase;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Stack;

import javax.swing.JEditorPane;
import javax.swing.text.html.HTMLEditorKit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.squidy.SquidyException;
import org.squidy.common.util.ReflectionUtil;
import org.squidy.database.RemoteUpdatable;
import org.squidy.database.RemoteUpdatableSessionProvider;
import org.squidy.database.Session;
import org.squidy.database.SessionFactory;
import org.squidy.database.SessionFactoryProvider;
import org.squidy.designer.Designer;
import org.squidy.designer.DesignerPreferences;
import org.squidy.designer.component.CropScroll;
import org.squidy.designer.components.pdf.PDFPane;
import org.squidy.designer.constant.Constants;
import org.squidy.designer.piccolo.JComponentWrapper;
import org.squidy.designer.shape.VisualShape;
import org.squidy.designer.shape.ZoomShape;
import org.squidy.designer.util.FontUtils;
import org.squidy.designer.util.ImageUtils;
import org.squidy.designer.util.ShapeUtils;
import org.squidy.designer.zoom.ActionShape;
import org.squidy.designer.zoom.ContainerShape;
import org.squidy.designer.zoom.TitledShape;
import org.squidy.designer.zoom.impl.InformationShape;
import org.squidy.manager.data.Processor;
import org.squidy.manager.data.Processor.Status;
import org.squidy.manager.model.Piping;
import org.squidy.manager.model.Processable;

import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.nodes.PImage;
import edu.umd.cs.piccolo.util.PAffineTransform;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PPaintContext;
import edu.umd.cs.piccolo.util.PPickPath;
import edu.umd.cs.piccolo.util.PStack;

/**
 * <code>KnowledgeBaseItem</code>.
 * 
 * <pre>
 * Date: Feb 15, 2009
 * Time: 7:20:34 PM
 * </pre>
 * 
 * @author Roman R&auml;dle, <a
 *         href="mailto:Roman.Raedle@uni-konstanz.de">Roman.
 *         Raedle@uni-konstanz.de</a>, University of Konstanz
 * @version $Id: RepositoryItem.java 772 2011-09-16 15:39:44Z raedle $
 * @since 1.0.0
 */
public class RepositoryItem<T extends ZoomShape<?>> extends TitledShape<T> implements Instantiable<Processable> {

	/**
	 * Generated serial version UID.
	 */
	private static final long serialVersionUID = 687637552092351418L;

	// Logger to info, error, debug,... messages.
	private static Log LOG = LogFactory.getLog(RepositoryItem.class);

	public static final String ITEM_SELECTION = "ITEM_SELECTION";

	private Class<? extends Processable> processableClass;
	
	private boolean stable = true;

	/**
	 * @return the processableClass
	 */
	public Class<? extends Processable> getProcessableClass() {
		return processableClass;
	}

	private PImage image;

	private CropScroll cropScroll;
	private String information;
	
	/**
	 * @param goalDirectedZoom
	 */
	public RepositoryItem(Class<? extends Processable> processable) throws SquidyException {
		setBounds(Constants.DEFAULT_NODE_BOUNDS);

		setTitle("N/A");

		this.processableClass = processable;

		if (processable.isAnnotationPresent(Processor.class)) {
			Processor processor = processable.getAnnotation(Processor.class);
			setTitle(processor.name());
			stable = processor.status().equals(Status.STABLE);
			information = processor.description();
			try {
				image = new PImage(ImageUtils.getProcessorIconURL(processor));
			} catch (Exception e) {
				if (LOG.isErrorEnabled()) {
					LOG.error(e.getMessage(), e);
				}
			}
		}

		if (image != null) {
			image.setScale(10);
			image.setPickable(false);
			image.setChildrenPickable(false);
			addChild(image);
		}
		addInputEventListener(new KnowledgeBaseEventHandler());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.squidy.designer.knowledgebase.Instantiable#getInstance()
	 */
	public Processable getInstance() {
		return ReflectionUtil.createInstance(processableClass);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.squidy.designer.zoom.ZoomShape#zoomBegan()
	 */
	@Override
	protected void zoomBegan() {
		super.zoomBegan();

		if (cropScroll == null) {
			initPane();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.squidy.designer.zoom.ZoomShape#layoutSemanticsZoomedIn()
	 */
	@Override
	protected void layoutSemanticsZoomedIn() {
		super.layoutSemanticsZoomedIn();

		//ShapeUtils.setApparent(image, false);
		ShapeUtils.setApparent(cropScroll, true);
		
		image.setScale(1.0);
		image.setOffset(40, 40);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.squidy.designer.zoom.ZoomShape#layoutSemanticsZoomedOut()
	 */
	@Override
	protected void layoutSemanticsZoomedOut() {
		super.layoutSemanticsZoomedOut();

		ShapeUtils.setApparent(image, true);
		ShapeUtils.setApparent(cropScroll, false);
		
		if (cropScroll != null) {
			cropScroll.removeFromParent();
			cropScroll = null;
		}
		PBounds bounds = getBoundsReference();
		PBounds imageBounds = image.getBoundsReference();
		image.setScale(10);
		image.offset(bounds.getCenterX() - (imageBounds.getCenterX() * image.getScale()), 150);
	}
	
	private static Font fontName = internalFont.deriveFont(50f);
	private static Font fontSource = internalFont.deriveFont(30f);
	
	/* (non-Javadoc)
	 * @see org.squidy.designer.shape.ZoomShape#getZoomedOutDrawPaint()
	 */
	@Override
	protected Paint getZoomedOutDrawPaint() {
		return stable ? super.getZoomedOutDrawPaint() : DesignerPreferences.STATUS_UNSTABLE;
	}
	
	/* (non-Javadoc)
	 * @see org.squidy.designer.shape.ZoomShape#paintShapeZoomedIn(edu.umd.cs.piccolo.util.PPaintContext)
	 */
	@Override
	protected void paintShapeZoomedIn(PPaintContext paintContext) {
		super.paintShapeZoomedIn(paintContext);
		
		Graphics2D g = paintContext.getGraphics();
		
		String information = "N/A";
		if (information != null) {

			PBounds bounds = getBoundsReference();
			double x = bounds.getX();
			double width = bounds.getWidth();

			String source = "Source: " + information;

			g.setFont(fontName);
			g.drawString(getProcessableClass().getSimpleName(), (int) (x + 100), 80);

			g.setFont(fontSource);
			g.drawString(source,
					(int) (x + width - FontUtils.getWidthOfText(g.getFontMetrics(), source)) - 20, 130);
		}
	}
	
	private void initPane() {
		// Add Zoomable Component
		new Thread(new Runnable() {
			public void run() {

				// ProgressIndicator indicator = new
				// ProgressIndicator(InformationShape.this);

				if (information == null || "".equals(information)) {
					return;
				}
				
				URL url = null;
				try {
					try {
						if (information.endsWith(".pdf")) {
							url = InformationShape.class.getResource(information);
						}
						else if (information.endsWith(".html")) {
							try {
								url = new URL(information);
							}
							catch (Exception e) {
								url = InformationShape.class.getResource(information);
							}
						}
						else {
							url = new URL(information);
						}
					}
					catch (Exception e) {
						// do nothing
					}

					PNode cropNode;
					// PDF
					if (information.endsWith(".pdf")) {
						if (LOG.isDebugEnabled()) {
							LOG.debug("Display information as PDF.");
						}
						cropNode = new PDFPane(url.getFile());
					}
					// HTML
					else {
						if (LOG.isDebugEnabled()) {
							LOG.debug("Display information as HTML.");
						}

						JEditorPane editorPane = new JEditorPane();
						editorPane.setFont(internalFont.deriveFont(10f));

						FontMetrics fm = editorPane.getFontMetrics(editorPane.getFont());
						int editorWidth = 400;
						editorPane.setPreferredSize(new Dimension(editorWidth, FontUtils.getLineCount(information,
								editorWidth)
								* fm.getHeight()));

						cropNode = JComponentWrapper.create(editorPane);
						editorPane.setEditable(false);

						if (information.endsWith(".html")) {
							HTMLEditorKit editorKit = new HTMLEditorKit();
							editorPane.setEditorKit(editorKit);
							editorPane.setPage(url);
							editorPane.setPreferredSize(new Dimension(800, 2000));
						}
						else {
							editorPane.setText(information);
						}

						// Prepare HTML Kit
						// HTMLParser editorKit = new HTMLParser();
						// HTMLParserCallback callback = new
						// HTMLParserCallback();
						// getComponentEditorPane().setEditorKit(editorKit);
						// //Open connection
						// InputStreamReader reader = new
						// InputStreamReader(url.openStream());
						// //Start parse process
						// editorKit.getParser().parse(reader, callback, true);
						// Wait until parsing process has finished
						// try {
						// Thread.sleep(2000);
						// }
						// catch (InterruptedException e) {
						// if (LOG.isErrorEnabled()) {
						// LOG.error("Error in " +
						// InformationShape.class.getName() + ".", e);
						// }
						// }
					}

					cropScroll = new CropScroll(cropNode, new Dimension(1000, 700), 0.2);
					cropScroll.setOffset(getBoundsReference().getCenterX()
							- cropScroll.getBoundsReference().getCenterX(), 250);
					addChild(cropScroll);
					invalidateFullBounds();
					invalidateLayout();
					invalidatePaint();
				}
				catch (MalformedURLException e) {
					if (LOG.isErrorEnabled()) {
						LOG.error("Could not parse URL from input string: " + e.getMessage() + " in "
								+ RepositoryItem.class.getName() + ".\nInput was: " + information);
					}
				}
				catch (IOException e) {
					if (LOG.isErrorEnabled()) {
						LOG.error("Could not create HTMLPane in " + RepositoryItem.class.getName(), e);
					}
				}

				// indicator.done();
			}
		}).start();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((processableClass == null) ? 0 : processableClass.hashCode());
		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		RepositoryItem<?> other = (RepositoryItem<?>) obj;
		if (processableClass == null) {
			if (other.processableClass != null)
				return false;
		}
		else if (!processableClass.equals(other.processableClass))
			return false;
		return true;
	}

	/**
	 * <code>KnowledgeBaseEventHandler</code>.
	 * 
	 * <pre>
	 * Date: Jul 9, 2009
	 * Time: 4:15:27 PM
	 * </pre>
	 * 
	 * 
	 * @author Roman Rdle <a
	 *         href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle
	 * @uni-konstanz.de</a> Human-Computer Interaction Group University of
	 *                      Konstanz
	 * 
	 * @version $Id: RepositoryItem.java 772 2011-09-16 15:39:44Z raedle $
	 * @since 1.0.0
	 */
	class KnowledgeBaseEventHandler extends PBasicInputEventHandler {

		private VisualShape<?> instance;

		private PNode originator;
		private PNode parent;

		private PropertyChangeListener prop;

		private Stack<PNode> poppedNodes = new Stack<PNode>();
		private Stack<PAffineTransform> poppedTransforms = new Stack<PAffineTransform>();

		/* (non-Javadoc)
		 * @see edu.umd.cs.piccolo.event.PBasicInputEventHandler#mouseDragged(edu.umd.cs.piccolo.event.PInputEvent)
		 */
		@Override
		public void mouseDragged(final PInputEvent event) {
			super.mouseDragged(event);
			
			try {
				originator = getOriginator(event);
			}
			catch (SquidyException e) {
				return;
			}

			Processable processable = getInstance();

			instance = ShapeUtils.getActionShape(processable);

			parent = getParent();
			while (parent != null && !(parent instanceof ContainerShape<?, ?>)) {
				parent = parent.getParent();
			}

			if (parent != null && parent instanceof ContainerShape<?, ?>) {

				if (originator.getParent().getParent().getParent() instanceof NodeRepositoryShape<?>) {
					dragZoomedIntoKnowledgeBase(event);

					// Zoom out animation.
					if (parent != null && parent instanceof ZoomShape<?>) {
						((ZoomShape<?>) parent).animateToCenterView(event.getCamera());
					}
				}
				else {
					((ContainerShape) parent).addVisualShape(instance);

					PBounds bounds = originator.getBounds();
					double minX = bounds.getMinX();
					double minY = bounds.getMinY();

					instance.setVisible(true);
					originator.localToGlobal(bounds);
					instance.globalToLocal(bounds);

					double offsetX = (bounds.getMinX() - minX) * instance.getScale();
					double offsetY = (bounds.getMinY() - minY) * instance.getScale();

					instance.setOffset(offsetX, offsetY);
					
					if (instance instanceof RemoteUpdatable) {
						SessionFactory<? extends Session> factory = SessionFactoryProvider.getProvider();
						if (factory instanceof RemoteUpdatableSessionProvider)
							((RemoteUpdatableSessionProvider<? extends Session>) factory).updateRemote((RemoteUpdatable) instance);
					}
						
					Designer.getInstance().add(instance);
				}

				// Pick path -> traversed by event handler.
				PPickPath path = event.getPath();

				// Pop nodes and transforms until required type is reached.
				popNodesAndTransformUntilType(path, AdvancedKnowledgeBase.class);

				// Insert new node instance and its transform.
				path.pushNode(instance);
				path.pushTransform(instance.getTransform());

				if (originator.getParent().getParent().getParent() instanceof NodeRepositoryShape<?>) {
					// Push back popped nodes and popped transforms.
					pushBackNodesAndTransforms(path);
				}

				((ActionShape<?, Piping>) instance).setProcessable((Piping) processable);
				((ActionShape<?, Piping>) parent).getProcessable().addSubProcessable(processable);
			}

			instance.setDraggable(true);

			firePropertyChange(-1, ITEM_SELECTION, null, instance);
		}

		/**
		 * @param event
		 */
		private PNode getOriginator(PInputEvent event) throws SquidyException {
			PNode node = event.getPickedNode();
			while (node != null && !(node instanceof Instantiable<?>)) {
				node = node.getParent();
			}

			if (node == null) {
				throw new SquidyException("Required instatiable not found.");
			}

			return node;
		}

		

		/**
		 * @param path
		 * @param type
		 */
		private void popNodesAndTransformUntilType(PPickPath path, Class<?> type) {
			// Clear previously popped nodes and popped transforms.
			poppedNodes.clear();
			poppedTransforms.clear();

			boolean poppedTopNode = false;

			PStack stack = path.getNodeStackReference();
			PNode stackNode = (PNode) stack.peek();
			while (!type.isAssignableFrom(stackNode.getClass())) {

				// Pop node and its transform.
				PNode poppedNode = (PNode) stack.pop();
				PAffineTransform transform = stackNode.getTransformReference(false);
				path.popTransform(transform);

				if (poppedTopNode) {

					// Push node to storage stack.
					poppedNodes.push(poppedNode);

					// Push transform to storage stack.
					poppedTransforms.push(transform);
				}
				else {
					poppedTopNode = true;
				}

				// Process with next node.
				stackNode = (PNode) stack.peek();
			}
		}

		/**
		 * @param path
		 */
		private void pushBackNodesAndTransforms(PPickPath path) {
			for (PNode node : poppedNodes) {
				path.pushNode(node);
			}

			for (PAffineTransform transform : poppedTransforms) {
				path.pushTransform(transform);
			}
		}

		private PBasicInputEventHandler dragHandler;
		private PNode dragNode;

		/**
		 * @param event
		 */
		private void dragZoomedIntoKnowledgeBase(final PInputEvent event) {
			Point2D p = originator.getOffset();

			Point2D p1 = originator.getParent().getOffset();
			p.setLocation(p.getX() + p1.getX(), p.getY() + p1.getY());

			p1 = originator.getParent().getParent().getOffset();
			p.setLocation(p.getX() + p1.getX(), p.getY() + p1.getY());

			instance.setOffset(p);

			dragNode = createDragNode(originator);
			dragNode.removeFromParent();
			event.getCamera().addChild(dragNode);
			dragNode.setScale(0.02);
			dragNode.setOffset(event.getPositionRelativeTo(event.getCamera()));

			dragHandler = new PBasicInputEventHandler() {

				/*
				 * (non-Javadoc)
				 * 
				 * @see edu.umd.cs.piccolo.event.PBasicInputEventHandler#
				 * processEvent(edu.umd.cs.piccolo.event.PInputEvent, int)
				 */
				@Override
				public void processEvent(PInputEvent event, int type) {
					super.processEvent(event, type);

					switch (type) {
					case MouseEvent.MOUSE_DRAGGED:
						dragNode.setOffset(event.getPositionRelativeTo(event.getCamera()));
						break;
					case MouseEvent.MOUSE_RELEASED:
						if (event.getCamera().getChildrenReference().contains(dragNode)) {
							event.getCamera().removeChild(dragNode);
						}

						if (parent instanceof ContainerShape<?, ?>) {
							((ContainerShape<VisualShape<?>, ?>) parent).addVisualShape(instance);
							Point2D pos = event.getPositionRelativeTo(parent);
							instance.setOffset(pos);
						}

						event.getTopCamera().removeInputEventListener(dragHandler);

						break;
					}
				}
			};
			event.getTopCamera().addInputEventListener(dragHandler);
		}
	}

	private PNode createDragNode(PNode originator) {

		if (originator instanceof RepositoryItem<?>) {
			return new RepositoryItem(((RepositoryItem<?>) originator).getProcessableClass());
		}

		return new PNode();
	}
}