/*******************************************************************************
 * Copyright (c) 2005, 2006 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.ui.internal.projection;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapter;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
import org.w3c.dom.Node;

/**
 * Updates projection annotation model with projection annotations for this
 * adapter node's children
 */
public class ProjectionModelNodeAdapterXML implements INodeAdapter {
	private final static boolean debugProjectionPerf = "true".equalsIgnoreCase(Platform.getDebugOption("org.eclipse.wst.xml.ui/projectionperf")); //$NON-NLS-1$ //$NON-NLS-2$

	private class TagProjectionAnnotation extends ProjectionAnnotation {
		private boolean fIsVisible = false; /* workaround for BUG85874 */
		private Node fNode;

		public TagProjectionAnnotation(Node node, boolean isCollapsed) {
			super(isCollapsed);
			fNode = node;
		}

		public Node getNode() {
			return fNode;
		}

		public void setNode(Node node) {
			fNode = node;
		}

		/**
		 * Does not paint hidden annotations. Annotations are hidden when they
		 * only span one line.
		 * 
		 * @see ProjectionAnnotation#paint(org.eclipse.swt.graphics.GC,
		 *      org.eclipse.swt.widgets.Canvas,
		 *      org.eclipse.swt.graphics.Rectangle)
		 */
		public void paint(GC gc, Canvas canvas, Rectangle rectangle) {
			/* workaround for BUG85874 */
			/*
			 * only need to check annotations that are expanded because hidden
			 * annotations should never have been given the chance to
			 * collapse.
			 */
			if (!isCollapsed()) {
				// working with rectangle, so line height
				FontMetrics metrics = gc.getFontMetrics();
				if (metrics != null) {
					// do not draw annotations that only span one line and
					// mark them as not visible
					if ((rectangle.height / metrics.getHeight()) <= 1) {
						fIsVisible = false;
						return;
					}
				}
			}
			fIsVisible = true;
			super.paint(gc, canvas, rectangle);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see org.eclipse.jface.text.source.projection.ProjectionAnnotation#markCollapsed()
		 */
		public void markCollapsed() {
			/* workaround for BUG85874 */
			// do not mark collapsed if annotation is not visible
			if (fIsVisible) {
				super.markCollapsed();
			}
		}
	}

	ProjectionModelNodeAdapterFactoryXML fAdapterFactory;
	private Map fTagAnnotations = new HashMap();

	public ProjectionModelNodeAdapterXML(ProjectionModelNodeAdapterFactoryXML factory) {
		fAdapterFactory = factory;
	}

	/**
	 * Create a projection position from the given node. Able to get
	 * projection position if node isNodeProjectable.
	 * 
	 * @param node
	 * @return null if no projection position possible, a Position otherwise
	 */
	private Position createProjectionPosition(Node node) {
		Position pos = null;
		if (isNodeProjectable(node) && (node instanceof IndexedRegion)) {
			// IDocument document =
			// fAdapterFactory.getProjectionViewer().getDocument();
			// if (document != null) {
			IndexedRegion inode = (IndexedRegion) node;
			int start = inode.getStartOffset();
			int end = inode.getEndOffset();
			if ((start >= 0) && (start < end)) {
				// region-based
				// extra line when collapsed, but no region increase when add
				// newline
				pos = new Position(start, end - start);
				// try {
				// // line-based
				// // extra line when collapsed, but no region increase when
				// add newline
				// IRegion startLineRegion =
				// document.getLineInformationOfOffset(start);
				// IRegion endLineRegion =
				// document.getLineInformationOfOffset(end);
				// int startOffset = startLineRegion.getOffset();
				// int endOffset = endLineRegion.getOffset() +
				// endLineRegion.getLength();
				// if (endOffset > startOffset) {
				// pos = new Position(startOffset, endOffset - startOffset);
				// }

				// // line-based
				// // no extra line when collapsed, but region increase when
				// add newline
				// int startLine = document.getLineOfOffset(start);
				// int endLine = document.getLineOfOffset(end);
				// if (endLine + 1 < document.getNumberOfLines()) {
				// int offset = document.getLineOffset(startLine);
				// int endOffset = document.getLineOffset(endLine + 1);
				// pos = new Position(offset, endOffset - offset);
				// }
				// }
				// catch (BadLocationException x) {
				// Logger.log(Logger.WARNING_DEBUG, null, x);
				// }
			}
			// }
		}
		return pos;
	}

	/**
	 * Find TagProjectionAnnotation for node in the current list of projection
	 * annotations for this adapter
	 * 
	 * @param node
	 * @return TagProjectionAnnotation
	 */
	private TagProjectionAnnotation getExistingAnnotation(Node node) {
		TagProjectionAnnotation anno = null;

		if ((node != null) && (!fTagAnnotations.isEmpty())) {
			Iterator it = fTagAnnotations.keySet().iterator();
			while (it.hasNext() && (anno == null)) {
				TagProjectionAnnotation a = (TagProjectionAnnotation) it.next();
				Node n = a.getNode();
				if (node.equals(n)) {
					anno = a;
				}
			}
		}
		return anno;
	}

	public boolean isAdapterForType(Object type) {
		return type == ProjectionModelNodeAdapterXML.class;
	}

	/**
	 * Returns true if node is a node type able to fold
	 * 
	 * @param node
	 * @return boolean true if node is projectable, false otherwise
	 */
	private boolean isNodeProjectable(Node node) {
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			return true;
		}
		return false;
	}

	public void notifyChanged(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		// check if folding is even enabled, if not, just ignore notifyChanged
		// events
		if (!fAdapterFactory.isActive()) {
			return;
		}

		if ((eventType == INodeNotifier.STRUCTURE_CHANGED) && (notifier instanceof Node)) {
			updateAdapter((Node) notifier);
		}
	}

	/**
	 * Update the projection annotation of all the nodes that are children of
	 * node
	 * 
	 * @param node
	 */
	void updateAdapter(Node node) {
		updateAdapter(node, null);
	}

	/**
	 * Update the projection annotation of all the nodes that are children of
	 * node and adds all projection annotations to viewer (for newly added
	 * viewers)
	 * 
	 * @param node
	 * @param viewer
	 */
	void updateAdapter(Node node, ProjectionViewer viewer) {
		long start = System.currentTimeMillis();

		Map additions = new HashMap();
		Map projectionAnnotations = new HashMap();

		// go through immediate child nodes and figure out projection
		// model annotations
		if (node != null) {
			Node childNode = node.getFirstChild();
			while (childNode != null) {
				Position newPos = createProjectionPosition(childNode);
				if (newPos != null) {
					TagProjectionAnnotation newAnnotation = new TagProjectionAnnotation(childNode, false);
					TagProjectionAnnotation existing = getExistingAnnotation(childNode);
					if (existing == null) {
						// add to map containing all annotations for this
						// adapter
						projectionAnnotations.put(newAnnotation, newPos);
						// add to map containing annotations to add
						additions.put(newAnnotation, newPos);
					}
					else {
						// add to map containing all annotations for this
						// adapter
						projectionAnnotations.put(existing, newPos);
						// remove from map containing annotations to delete
						fTagAnnotations.remove(existing);
					}
				}
				childNode = childNode.getNextSibling();
			}

			// in the end, want to delete anything leftover in old list, add
			// everything in additions, and update everything in
			// projectionAnnotations
			ProjectionAnnotation[] oldList = null;
			if (!fTagAnnotations.isEmpty()) {
				oldList = (ProjectionAnnotation[]) fTagAnnotations.keySet().toArray(new ProjectionAnnotation[0]);
			}
			ProjectionAnnotation[] modifyList = null;
			if (!projectionAnnotations.isEmpty()) {
				modifyList = (ProjectionAnnotation[]) projectionAnnotations.keySet().toArray(new ProjectionAnnotation[0]);
			}

			// specifically add all annotations to viewer
			if ((viewer != null) && !projectionAnnotations.isEmpty()) {
				fAdapterFactory.queueAnnotationModelChanges(node, null, projectionAnnotations, null, viewer);
			}

			// only update when there is something to update
			if (((oldList != null) && (oldList.length > 0)) || (!additions.isEmpty()) || ((modifyList != null) && (modifyList.length > 0))) {
				fAdapterFactory.queueAnnotationModelChanges(node, oldList, additions, modifyList);
			}
		}

		// save new list of annotations
		fTagAnnotations = projectionAnnotations;

		if (debugProjectionPerf) {
			long end = System.currentTimeMillis();
			String nodeName = node != null ? node.getNodeName() : "null"; //$NON-NLS-1$
			System.out.println("ProjectionModelNodeAdapterXML.updateAdapter (" + nodeName + "):" + (end - start)); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}
}
