/*******************************************************************************
 * Copyright (c) 2001, 2009 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Jens Lukowski/Innoopract - initial renaming/restructuring
 *     
 *******************************************************************************/
package org.eclipse.wst.xml.ui.internal.contentoutline;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.viewers.StructuredViewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.wst.xml.ui.internal.XMLUIMessages;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * This job holds a queue of updates (affected nodes) for multiple structured
 * viewers. When a new request comes in, the current run is cancelled, the new
 * request is added to the queue, then the job is re-scheduled. Support for
 * multiple structured viewers is required because refresh updates are usually
 * triggered by model changes, and the model may be visible in more than one
 * viewer.
 * 
 * @author pavery
 */
class RefreshStructureJob extends Job {

	/** debug flag */
	static final boolean DEBUG;
	private static final long UPDATE_DELAY = 300;
	static {
		String value = Platform.getDebugOption("org.eclipse.wst.sse.ui/debug/refreshStructure"); //$NON-NLS-1$
		DEBUG = (value != null) && value.equalsIgnoreCase("true"); //$NON-NLS-1$
	}
	/** List of refresh requests (Nodes) */
	private final List fRefreshes;
	/** List of update requests (Nodes) */
	private final List fUpdates;
	/** List of update requests (Nodes) */
	private final List fUpdateProperties;
	/** the structured viewers */
	Set fRefreshViewers = new HashSet(3);
	Set fUpdateViewers = new HashSet(3);

	public RefreshStructureJob() {
		super(XMLUIMessages.refreshoutline_0);
		setPriority(Job.LONG);
		setSystem(true);
		fRefreshes = new ArrayList(5);
		fUpdates = new ArrayList(5);
		fUpdateProperties = new ArrayList(5);
	}

	private synchronized void addUpdateRequest(Node newNodeRequest, String[] updateProperties) {
		/*
		 * If we get to here, either from existing request list being zero
		 * length, or no exisitng requests "matched" new request, then add the
		 * new request.
		 */
		fUpdates.add(newNodeRequest);
		fUpdateProperties.add(updateProperties);
	}

	private synchronized void addUpdateViewer(StructuredViewer viewer) {
		fUpdateViewers.add(viewer);
	}

	private synchronized void addRefreshRequest(Node newNodeRequest) {
		/*
		 * note: the caller must NOT pass in null node request (which, since
		 * private method, we do not need to gaurd against here, as long as we
		 * gaurd against it in calling method.
		 */
		int size = fRefreshes.size();
		for (int i = 0; i < size; i++) {
			Node existingNodeRequest = (Node) fRefreshes.get(i);
			/*
			 * https://bugs.eclipse.org/bugs/show_bug.cgi?id=157427 If we
			 * already have a request which equals the new request, discard
			 * the new request
			 */
			if (existingNodeRequest.equals(newNodeRequest)) {
				return;
			}
			/*
			 * If we already have a request which contains the new request,
			 * discard the new request
			 */
			if (contains(existingNodeRequest, newNodeRequest)) {
				return;
			}
			/*
			 * If new request contains any existing requests, replace it with
			 * new request. ISSUE: technically, we should replace ALL
			 * contained, existing requests (such as if many siblings already
			 * que'd up when their common parent is then requested, but, I'm
			 * not sure if that occurs much, in practice, or if there's an
			 * algorithm to quickly find them all. Actually, I guess we could
			 * just go through the _rest_ of the list (i+1 to size) and remove
			 * any that are contained by new request ... in future :) .
			 */
			if (contains(newNodeRequest, existingNodeRequest)) {
				fRefreshes.set(i, newNodeRequest);
				return;
			}
		}
		/*
		 * If we get to here, either from existing request list being zero
		 * length, or no exisitng requests "matched" new request, then add the
		 * new request.
		 */
		fRefreshes.add(newNodeRequest);
	}

	private synchronized void addRefreshViewer(StructuredViewer viewer) {
		fRefreshViewers.add(viewer);
	}
	
	/* (non-Javadoc)
	 * @see org.eclipse.core.runtime.jobs.Job#canceling()
	 */
	protected void canceling() {
		fUpdates.clear();
		fUpdateViewers.clear();
		super.canceling();
	}

	/**
	 * Simple hierarchical containment relationship. Note, this method returns
	 * "false" if the two nodes are equal!
	 * 
	 * @param root
	 * @param possible
	 * @return if the root is parent of possible, return true, otherwise
	 *         return false
	 */
	private boolean contains(Node root, Node possible) {
		if (DEBUG) {
			System.out.println("=============================================================================================================="); //$NON-NLS-1$
			System.out.println("recursive call w/ root: " + root.getNodeName() + " and possible: " + possible); //$NON-NLS-1$ //$NON-NLS-2$
			System.out.println("--------------------------------------------------------------------------------------------------------------"); //$NON-NLS-1$
		}

		// the following checks are important
		// #document node will break the algorithm otherwise

		// can't contain the child if it's null
		if (root == null) {
			if (DEBUG) {
				System.out.println("returning false: root is null"); //$NON-NLS-1$
			}
			return false;
		}
		// nothing can be parent of Document node
		if (possible instanceof Document) {
			if (DEBUG) {
				System.out.println("returning false: possible is Document node"); //$NON-NLS-1$
			}
			return false;
		}
		// document contains everything
		if (root instanceof Document) {
			if (DEBUG) {
				System.out.println("returning true: root is Document node"); //$NON-NLS-1$
			}
			return true;
		}

		// check parentage
		Node current = possible;
		// loop parents
		while ((current != null) && (current.getNodeType() != Node.DOCUMENT_NODE)) {
			// found it
			if (root.equals(current)) {
				if (DEBUG) {
					System.out.println("   !!! found: " + possible.getNodeName() + " in subelement of: " + root.getNodeName()); //$NON-NLS-1$ //$NON-NLS-2$
				}
				return true;
			}
			current = current.getParentNode();
		}
		// never found it
		return false;
	}

	/**
	 * Refresh must be on UI thread because it's on a SWT widget.
	 * 
	 * @param node
	 */
	private void doRefresh(final Node node, final StructuredViewer[] viewers) {
		final Display display = PlatformUI.getWorkbench().getDisplay();
		display.asyncExec(new Runnable() {
			public void run() {
				if (DEBUG) {
					System.out.println("refresh on: [" + node.getNodeName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$
				}

				for (int i = 0; i < viewers.length; i++) {
					if (!viewers[i].getControl().isDisposed()) {
						if (node.getNodeType() == Node.DOCUMENT_NODE) {
							viewers[i].refresh(true);
						}
						else {
							viewers[i].refresh(node, true);
						}
					}
					else {
						if (DEBUG) {
							System.out.println("   !!! skipped refreshing disposed viewer: " + viewers[i]); //$NON-NLS-1$
						}
					}
				}
			}
		});
	}

	/**
	 * Update must be on UI thread because it's on a SWT widget.
	 * 
	 * @param node
	 */
	private void doUpdate(final StructuredViewer[] viewers, final Node node, final String[] properties) {
		final Display display = PlatformUI.getWorkbench().getDisplay();
		display.asyncExec(new Runnable() {
			public void run() {
				if (DEBUG) {
					System.out.println("refresh on: [" + node.getNodeName() + "]"); //$NON-NLS-1$ //$NON-NLS-2$
				}

				for (int i = 0; i < viewers.length; i++) {
					if (!viewers[i].getControl().isDisposed()) {
						viewers[i].update(node, properties);
					}
					else {
						if (DEBUG) {
							System.out.println("   !!! skipped refreshing disposed viewer: " + viewers[i]); //$NON-NLS-1$
						}
					}
				}
			}
		});
	}

	/**
	 * This method also synchronized because it accesses the fRefreshes queue
	 * and fRefreshViewers list
	 * 
	 * @return an array containing and array of the currently requested Nodes
	 *         to refresh and the viewers in which to refresh them
	 */
	private synchronized Object[] getRefreshRequests() {
		Node[] toRefresh = (Node[]) fRefreshes.toArray(new Node[fRefreshes.size()]);
		fRefreshes.clear();

		StructuredViewer[] viewers = (StructuredViewer[]) fRefreshViewers.toArray(new StructuredViewer[fRefreshViewers.size()]);
		fRefreshViewers.clear();

		return new Object[]{toRefresh, viewers};
	}

	/**
	 * This method also synchronized because it accesses the fUpdates queue
	 * and fUpdateViewers list
	 * 
	 * @return an array containing and array of the currently requested Nodes
	 *         to refresh and the viewers in which to refresh them
	 */
	private synchronized Object[] getUpdateRequests() {
		Node[] toUpdate = (Node[]) fUpdates.toArray(new Node[fUpdates.size()]);
		fUpdates.clear();

		StructuredViewer[] viewers = (StructuredViewer[]) fUpdateViewers.toArray(new StructuredViewer[fUpdateViewers.size()]);
		fUpdateViewers.clear();

		String[][] properties = (String[][]) fUpdateProperties.toArray(new String[fUpdateProperties.size()][]);
		fUpdateProperties.clear();

		return new Object[]{toUpdate, viewers, properties};
	}

	/**
	 * Invoke a refresh on the viewer on the given node.
	 * 
	 * @param node
	 */
	public void refresh(StructuredViewer viewer, Node node) {
		if (node == null) {
			return;
		}

		addRefreshViewer(viewer);
		addRefreshRequest(node);
		schedule(UPDATE_DELAY);
	}

	/**
	 * Invoke a refresh on the viewer on the given node.
	 * 
	 * @param node
	 */
	public void update(StructuredViewer viewer, Node node, String[] properties) {
		if (node == null) {
			return;
		}

		addUpdateViewer(viewer);
		addUpdateRequest(node, properties);
		schedule(UPDATE_DELAY);
	}

	protected IStatus run(IProgressMonitor monitor) {
		IStatus status = Status.OK_STATUS;
		try {
			performUpdates();
			
			performRefreshes(monitor);
		}
		finally {
			monitor.done();
		}
		return status;
	}

	private void performRefreshes(IProgressMonitor monitor) {
		// Retrieve BOTH viewers and Nodes on one block
		Object[] requests = getRefreshRequests();
		Node[] nodes = (Node[]) requests[0];
		StructuredViewer[] viewers = (StructuredViewer[]) requests[1];

		for (int i = 0; i < nodes.length; i++) {
			if (monitor.isCanceled()) {
				throw new OperationCanceledException();
			}
			doRefresh(nodes[i], viewers);
		}
	}

	private void performUpdates() {
		// Retrieve BOTH viewers and Nodes on one block
		Object[] requests = getUpdateRequests();
		Node[] nodes = (Node[]) requests[0];
		StructuredViewer[] viewers = (StructuredViewer[]) requests[1];
		String[][] properties = (String[][]) requests[2];

		for (int i = 0; i < nodes.length; i++) {
			doUpdate(viewers, nodes[i], properties[i]);
		}
	}

}
