/*******************************************************************************
 * Copyright (c) 2004, 2010 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.css.ui.internal.contentoutline;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.views.properties.PropertySheetPage;
import org.eclipse.wst.css.core.internal.document.CSSStructuredDocumentRegionContainer;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSDocument;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSMediaRule;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSModel;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSNode;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSPrimitiveValue;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSStyleDeclItem;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSStyleDeclaration;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSStyleRule;
import org.eclipse.wst.css.core.internal.provisional.document.ICSSStyleSheet;
import org.eclipse.wst.css.ui.internal.image.CSSImageHelper;
import org.eclipse.wst.css.ui.internal.image.CSSImageType;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapter;
import org.eclipse.wst.sse.core.internal.provisional.INodeAdapterFactory;
import org.eclipse.wst.sse.core.internal.provisional.INodeNotifier;
import org.eclipse.wst.sse.ui.internal.contentoutline.IJFaceNodeAdapter;
import org.eclipse.wst.sse.ui.internal.contentoutline.IJFaceNodeAdapterFactory;
import org.w3c.dom.css.CSSImportRule;
import org.w3c.dom.css.CSSRule;
import org.w3c.dom.stylesheets.MediaList;

/**
 * Adapts the CSS DOM node to a JFace viewer.
 */
class CSSNodeAdapter implements IJFaceNodeAdapter, INodeAdapter, Runnable {
	/**
	 * debug .option
	 */
	private static final boolean DEBUG = getDebugValue();

	private static boolean getDebugValue() {
		String value = Platform.getDebugOption("org.eclipse.wst.sse.ui/debug/outline"); //$NON-NLS-1$
		boolean result = (value != null) && value.equalsIgnoreCase("true"); //$NON-NLS-1$
		return result;
	}

	class NotifyContext {
		NotifyContext(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
			this.notifier = notifier;
			this.eventType = eventType;
			this.changedFeature = changedFeature;
			this.oldValue = oldValue;
			this.newValue = newValue;
			this.pos = pos;
		}

		void fire() {
			internalNotifyChanged(notifier, eventType, changedFeature, oldValue, newValue, pos);
		}

		INodeNotifier notifier;
		int eventType;
		Object changedFeature;
		Object oldValue;
		Object newValue;
		int pos;
	}

	class StyleViewUpdater implements Runnable {
		public void run() {
			if (lastUpdater == this) {
				internalActionPerformed();
				lastUpdater = null;
			}
		}
	}

	protected INodeAdapterFactory adapterFactory;
	private Vector notifyQueue;
	StyleViewUpdater lastUpdater;
	protected int delayMSecs = 500;
	RefreshStructureJob fRefreshJob = null;
	final static Class ADAPTER_KEY = IJFaceNodeAdapter.class;

	public CSSNodeAdapter(INodeAdapterFactory adapterFactory) {
		super();
		this.adapterFactory = adapterFactory;
	}

	private synchronized RefreshStructureJob getRefreshJob() {
		if (fRefreshJob == null) {
			fRefreshJob = new RefreshStructureJob();
		}
		return fRefreshJob;
	}

	/**
	 * Insert the method's description here.
	 */
	protected void internalActionPerformed() {
		if (notifyQueue == null) {
			return;
		}
		boolean refresh_all = false;
		boolean refresh_rule = false;
		int pos_all = 0;
		List targets = new ArrayList();
		for (int i = 0; i < notifyQueue.size(); i++) {
			NotifyContext context = (NotifyContext) notifyQueue.get(i);
			if (context.notifier instanceof ICSSStyleSheet) {
				refresh_all = true;
				pos_all = i;
			}
			if (context.notifier instanceof ICSSStyleDeclaration) {
				refresh_rule = true;
				targets.add(context);
				// pos_rule = i;
			}
			// ((NotifyContext) notifyQueue.get(i)).fire();
		}
		if (refresh_all) {
			((NotifyContext) notifyQueue.get(pos_all)).fire();
		}
		else if (refresh_rule) {
			Iterator i = targets.iterator();
			while (i.hasNext()) {
				((NotifyContext) i.next()).fire();
			}
			// else if (refresh_rule) internalRefreshAll();
		}
		else {
			for (int i = 0; i < notifyQueue.size(); i++) {
				((NotifyContext) notifyQueue.get(i)).fire();
			}
		}
		notifyQueue.clear();
	}

	/**
	 * Called by the object being adapter (the notifier) when something has
	 * changed.
	 */
	public void internalNotifyChanged(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		Iterator iterator = ((IJFaceNodeAdapterFactory) adapterFactory).getListeners().iterator();
		while (iterator.hasNext()) {
			Object listener = iterator.next();
			if (listener instanceof StructuredViewer) {
				notifyChangedForStructuredViewer((StructuredViewer) listener, notifier, eventType, changedFeature, oldValue, newValue, pos);
			}
			else if (listener instanceof PropertySheetPage) {
				notifyChangedForPropertySheetPage((PropertySheetPage) listener, notifier, eventType, changedFeature, oldValue, newValue, pos);
			}
		}
	}

	private void notifyChangedForPropertySheetPage(PropertySheetPage page, INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		if (page.getControl() == null || page.getControl().isDisposed()) {
			return;
		}
		if (eventType == INodeNotifier.CHANGE || eventType == INodeNotifier.ADD || eventType == INodeNotifier.REMOVE) {
			page.refresh();
		}
	}

	private void notifyChangedForStructuredViewer(StructuredViewer viewer, INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		if (viewer.getControl() == null || viewer.getControl().isDisposed()) {
			return;
		}
		if (eventType == INodeNotifier.CHANGE) {
			if (notifier instanceof ICSSStyleSheet) {
				ICSSNode temp = (changedFeature != null) ? (ICSSNode) changedFeature : (ICSSNode) newValue;
				if (temp instanceof ICSSStyleRule) {
					viewer.refresh();
				}
				else {
					for (;;) {
						if (temp instanceof ICSSStyleRule) {
							break;
						}
						temp = temp.getParentNode();
						if (temp == null) {
							break;
						}
					}
					if (temp == null || temp instanceof ICSSStyleSheet) {
						viewer.refresh();
					}
					else {
						viewer.refresh(temp);
					}
				}
			}
			else {
				ICSSNode temp = (ICSSNode) notifier;
				if (temp != null) {
					temp = temp.getParentNode();
				}
				if (temp == null || temp instanceof ICSSStyleSheet) {
					viewer.refresh();
				}
				else {
					viewer.refresh(temp);
				}
			}
		}
		if (eventType == INodeNotifier.ADD) {
			if (notifier instanceof ICSSStyleSheet) {
				ICSSNode temp = (changedFeature != null) ? (ICSSNode) changedFeature : (ICSSNode) newValue;
				if (temp instanceof ICSSStyleRule) {
					viewer.refresh();
				}
				else {
					for (;;) {
						if (temp instanceof ICSSStyleRule) {
							break;
						}
						temp = temp.getParentNode();
						if (temp == null) {
							break;
						}
					}
				}
				if (temp == null || (temp instanceof ICSSStyleSheet)) {
					viewer.refresh();
				}
				else {
					viewer.refresh(temp);
				}
			}
			else {
				if (newValue != null && (newValue instanceof ICSSStyleDeclItem)) {
					viewer.refresh(((ICSSNode) newValue).getParentNode());
				}
				else {
					ICSSNode temp = (ICSSNode) notifier;
					if (temp != null) {
						temp = temp.getParentNode();
					}
					if (temp == null || (temp instanceof ICSSStyleSheet)) {
						viewer.refresh();
					}
					else {
						viewer.refresh(temp);
					}
				}
			}
		}
		else if (eventType == INodeNotifier.REMOVE) {
			if (notifier instanceof ICSSStyleSheet) {
				ICSSNode temp = (changedFeature != null) ? (ICSSNode) changedFeature : (ICSSNode) newValue;
				if (temp instanceof ICSSStyleRule) {
					viewer.refresh();
				}
				else {
					for (;;) {
						if (temp instanceof ICSSStyleRule) {
							break;
						}
						temp = temp.getParentNode();
						if (temp == null) {
							break;
						}
					}
					if (temp == null || (temp instanceof ICSSStyleSheet)) {
						viewer.refresh();
					}
					else {
						viewer.refresh(temp);
					}
				}
			}
			else {
				// viewer.refresh(notifier);
				ICSSNode temp = (ICSSNode) notifier;
				if (temp != null) {
					temp = temp.getParentNode();
				}
				if (temp == null || (temp instanceof ICSSStyleSheet)) {
					viewer.refresh();
				}
				else {
					viewer.refresh(temp);
				}
			}
		}
		// }
	}

	/**
	 * 
	 */
	public void internalRefreshAll() {
		Collection listeners = ((JFaceNodeAdapterFactoryCSS) adapterFactory).getListeners();
		Iterator iterator = listeners.iterator();
		while (iterator.hasNext()) {
			Object listener = iterator.next();
			if (listener instanceof StructuredViewer) {
				StructuredViewer viewer = (StructuredViewer) listener;
				if (viewer.getControl() != null && !viewer.getControl().isDisposed()) {
					viewer.refresh();
				}
			}
			else if (listener instanceof PropertySheetPage) {
				PropertySheetPage page = (PropertySheetPage) listener;
				if (page.getControl() != null && !page.getControl().isDisposed()) {
					page.refresh();
				}
			}
		}
	}

	/**
	 * Allowing the INodeAdapter to compare itself against the type allows it
	 * to return true in more than one case.
	 */
	public boolean isAdapterForType(Object type) {
		return type.equals(ADAPTER_KEY);
	}

	/**
	 * Called by the object being adapter (the notifier) when something has
	 * changed.
	 */
	public void notifyChanged(INodeNotifier notifier, int eventType, Object changedFeature, Object oldValue, Object newValue, int pos) {
		if (notifyQueue == null)
			notifyQueue = new Vector();
//		notifyQueue.add(new NotifyContext(notifier, eventType, changedFeature, oldValue, newValue, pos));
		// TODO-future: there's probably a better way than relying on async
		// exec
		if (notifier instanceof ICSSNode) {
			Collection listeners = ((JFaceNodeAdapterFactoryCSS) adapterFactory).getListeners();
			Iterator iterator = listeners.iterator();

			while (iterator.hasNext()) {
				Object listener = iterator.next();
				// https://bugs.eclipse.org/bugs/show_bug.cgi?id=90637
				// if (notifier instanceof Node && (listener instanceof
				// StructuredViewer) && (eventType ==
				// INodeNotifier.STRUCTURE_CHANGED || (eventType ==
				// INodeNotifier.CHANGE && changedFeature == null))) {
				if ((listener instanceof StructuredViewer) && ((eventType == INodeNotifier.STRUCTURE_CHANGED) || (eventType == INodeNotifier.CONTENT_CHANGED) || (eventType == INodeNotifier.CHANGE || (eventType == INodeNotifier.ADD) || (eventType == INodeNotifier.REMOVE)))) {
					if (DEBUG) {
						System.out.println("JFaceNodeAdapter notified on event type > " + eventType); //$NON-NLS-1$
					}

					// refresh on structural and "unknown" changes
					StructuredViewer structuredViewer = (StructuredViewer) listener;
					// https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=5230
					if (structuredViewer.getControl() != null) {
						getRefreshJob().refresh(structuredViewer, (ICSSNode) notifier);
					}
				}
			}
		}
	}

	Display getDisplay() {
		return PlatformUI.getWorkbench().getDisplay();
	}

	/**
	 * this method is intended only for timerExec()
	 */
	public void run() {
		lastUpdater = new StyleViewUpdater();
		getDisplay().asyncExec(lastUpdater);
	}

	/**
	 * Returns an enumeration containing all child nodes of the given element,
	 * which represents a node in a tree. The difference to
	 * <code>IStructuredContentProvider.getElements(Object)</code> is as
	 * follows: <code>getElements</code> is called to obtain the tree
	 * viewer's root elements. Method <code>getChildren</code> is used to
	 * obtain the children of a given node in the tree, which can can be a
	 * root node, too.
	 */
	public Object[] getChildren(Object object) {
		if (object instanceof ICSSNode) {
			ICSSNode node = (ICSSNode) object;

			short nodeType = node.getNodeType();
			if (nodeType == ICSSNode.STYLERULE_NODE || nodeType == ICSSNode.PAGERULE_NODE || nodeType == ICSSNode.FONTFACERULE_NODE) {
				for (node = node.getFirstChild(); node != null && !(node instanceof ICSSStyleDeclaration); node.getNextSibling()) {
					// nop
				}
			}
			List children = new ArrayList();
			ICSSNode child = (node != null) ? node.getFirstChild() : null;
			while (child != null) {
				if (!(child instanceof ICSSPrimitiveValue) && !(child instanceof MediaList)) {
					children.add(child);
				}
				/*
				 * Required to correctly connect the refreshing behavior to
				 * the tree
				 */
				if (child instanceof INodeNotifier) {
					((INodeNotifier) child).getAdapterFor(IJFaceNodeAdapter.class);
				}
				child = child.getNextSibling();
			}
			return children.toArray();
		}
		return new Object[0];
	}

	/**
	 * Returns an enumeration with the elements belonging to the passed
	 * element. These elements can be presented as rows in a table, items in a
	 * list etc.
	 */
	public Object[] getElements(Object object) {
		// The root is usually an instance of an XMLStructuredModel in
		// which case we want to extract the document.

		if (object instanceof ICSSModel) {
			ArrayList v = new ArrayList();
			// internalGetElements(object, v);
			addElements(object, v);
			Object[] elements = v.toArray();

			for (int i = 0; i < elements.length; i++) {
				/*
				 * Required to correctly connect the refreshing behavior to
				 * the tree
				 */
				if (elements[i] instanceof INodeNotifier) {
					((INodeNotifier) elements[i]).getAdapterFor(IJFaceNodeAdapter.class);
				}
			}

			return elements;
		}
		return new Object[0];

	}

	/**
	 * Returns the image for the label of the given element, for use in the
	 * given viewer.
	 * 
	 * @param viewer
	 *            The viewer that displays the element.
	 * @param element
	 *            The element for which to provide the label image. Element
	 *            can be <code>null</code> indicating no input object is set
	 *            to the viewer.
	 */
	public Image getLabelImage(Object element) {
		if (element instanceof ICSSNode) {
			CSSImageHelper helper = CSSImageHelper.getInstance();
			return helper.getImage(CSSImageType.getImageType((ICSSNode) element));
			// Image image = getCSSNodeImage(element);
			// return image;
			// return getAdapter(element).getLabelImage((ICSSNode) element);
		}
		return null;
	}

	/**
	 * Returns the text for the label of the given element, for use in the
	 * given viewer.
	 * 
	 * @param viewer
	 *            The viewer that displays the element.
	 * @param element
	 *            The element for which to provide the label text. Element can
	 *            be <code>null</code> indicating no input object is set to
	 *            the viewer.
	 */
	public String getLabelText(Object element) {
		// This was returning null, on occasion ... probably should not be,
		// but
		// took the quick and easy way out for now. (dmw 3/8/01)

		String result = "";//$NON-NLS-1$
		String mediaText;
		if (element instanceof ICSSNode) {
			switch (((ICSSNode) element).getNodeType()) {
				case ICSSNode.STYLERULE_NODE :
					result = ((ICSSStyleRule) element).getSelectors().getString();
					break;
				case ICSSNode.FONTFACERULE_NODE :
					result = "@font-face";//$NON-NLS-1$
					break;
				case ICSSNode.IMPORTRULE_NODE :
					result = ((CSSImportRule) element).getHref();
					mediaText = getMediaText((CSSImportRule) element);
					if (mediaText != null && 0 < mediaText.length()) {
						result += " (" + mediaText + ")"; //$NON-NLS-1$ //$NON-NLS-2$
					}
					break;
				case ICSSNode.PAGERULE_NODE :
					result = "@page";//$NON-NLS-1$
					break;
				case ICSSNode.STYLEDECLARATION_NODE :
					result = "properties";//$NON-NLS-1$
					break;
				case ICSSNode.STYLEDECLITEM_NODE :
					result = ((ICSSStyleDeclItem) element).getPropertyName();
					break;
				case ICSSNode.PRIMITIVEVALUE_NODE :
					result = ((ICSSPrimitiveValue) element).getStringValue();
					break;
				case ICSSNode.MEDIARULE_NODE :
					result = "@media";//$NON-NLS-1$
					mediaText = getMediaText((ICSSMediaRule) element);
					if (mediaText != null && 0 < mediaText.length()) {
						result += " (" + mediaText + ")"; //$NON-NLS-1$ //$NON-NLS-2$
					}
					break;
				case ICSSNode.CHARSETRULE_NODE :
					result = "@charset";//$NON-NLS-1$
					break;
				case ICSSNode.MEDIALIST_NODE :
					result = ((MediaList) element).getMediaText();
					break;
				default :
					break;
			}
		}

		// if (element instanceof ICSSNode) {
		// ICSSNode node = ((ICSSNode)element);
		// result = getAdapter(element).getLabelText((ICSSNode) element);
		// }
		return result;
	}

	/**
	 * Returns the parent for the given element. This method can return
	 * <code>null</code> indicating that the parent can't be computed. In
	 * this case the tree viewer can't expand a given node correctly if
	 * requested.
	 */
	public Object getParent(Object object) {
		if (object instanceof ICSSNode) {
			ICSSNode node = ((ICSSNode) object).getParentNode();
			if (node != null && node.getNodeType() == ICSSNode.STYLEDECLARATION_NODE) {
				/*
				 * Required to also correctly connect style declaration to the
				 * refreshing behavior in the tree
				 */
				if (node instanceof INodeNotifier) {
					((INodeNotifier) node).getAdapterFor(IJFaceNodeAdapter.class);
				}
				node = node.getParentNode();
			}
			return node;
		}
		return null;
	}

	/**
	 * Returns <code>true</code> if the given element has children.
	 * Otherwise <code>false</code> is returned.
	 */
	public boolean hasChildren(Object object) {
		// return getAdapter(object).hasChildren((ICSSNode) object);
		if (object instanceof ICSSNode) {
			/*
			 * Required to correctly connect the refreshing behavior to the
			 * tree
			 */
			if (object instanceof INodeNotifier) {
				((INodeNotifier) object).getAdapterFor(IJFaceNodeAdapter.class);
			}

			if (object instanceof ICSSStyleDeclItem)
				return false;
			else {
				if(((ICSSNode) object).hasChildNodes()) {
					ICSSNode child = ((ICSSNode) object).getFirstChild();
					
					while(child != null) {
						if(child instanceof CSSStructuredDocumentRegionContainer) {
							String childText = ((CSSStructuredDocumentRegionContainer) child).getCssText();
							if(childText != null && childText.length() > 0)
								return true;
						}
						child = child.getNextSibling();
					}
				}
			}
		}
		return false;
	}

	private void addElements(Object element, ArrayList v) {

		ICSSNode node;

		if (element instanceof ICSSModel) {
			ICSSModel model = (ICSSModel) element;
			ICSSDocument doc = model.getDocument();
			node = doc.getFirstChild();
		}
		else if (element instanceof ICSSNode) {
			node = ((ICSSNode) element).getFirstChild();
		}
		else
			return;

		while (node != null) {
			if (node instanceof CSSRule) {
				v.add(node);
			}

			node = node.getNextSibling();
		}

	}

	private String getMediaText(CSSRule rule) {
		String result = ""; //$NON-NLS-1$
		ICSSNode child = (rule != null) ? ((ICSSNode) rule).getFirstChild() : null;
		while (child != null) {
			if (child.getNodeType() == ICSSNode.MEDIALIST_NODE) {
				result = ((MediaList) child).getMediaText();
				break;
			}
			child = child.getNextSibling();
		}
		return result;
	}
}
