/*******************************************************************************
 * Copyright (c) 2006, 2015 Wind River Systems and others.
 *
 * 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:
 *     Wind River Systems - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.examples.dsf.filebrowser;

import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.eclipse.cdt.dsf.concurrent.DataRequestMonitor;
import org.eclipse.cdt.dsf.concurrent.IDsfStatusConstants;
import org.eclipse.cdt.dsf.concurrent.RequestMonitor;
import org.eclipse.cdt.dsf.internal.ui.DsfUIPlugin;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMContext;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMNode;
import org.eclipse.cdt.dsf.ui.viewmodel.IVMProvider;
import org.eclipse.cdt.dsf.ui.viewmodel.VMDelta;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenCountUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IElementLabelProvider;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IHasChildrenUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ILabelUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IModelDelta;
import org.eclipse.debug.internal.ui.viewers.model.provisional.IViewerUpdate;
import org.eclipse.debug.internal.ui.viewers.model.provisional.ModelDelta;

/**
 * File view model node which returns file elements that are found in the directory
 * specified by the parent element.  The child nodes of this node are fixed to
 * reference this element, and therefore this node will recursively populate
 * the contents of the tree reflecting the underlying filesystem directories.
 * <br>
 * Note: this node does NOT sub-class the {@link org.eclipse.cdt.dsf.ui.viewmodel.AbstractVMNode}
 */
@SuppressWarnings("restriction")
class FileVMNode implements IElementLabelProvider, IVMNode {
	/**
	 * Reference to the viewer model provider.  It's mainly used to access the
	 * viewer model adapter and its executor.
	 */
	private final FileBrowserVMProvider fProvider;

	public FileVMNode(FileBrowserVMProvider provider) {
		fProvider = provider;
	}

	@Override
	public String toString() {
		return "FileVMNode";
	}

	@Override
	public void dispose() {
		// All resources garbage collected.
	}

	public void setChildNodes(IVMNode[] childNodes) {
		throw new UnsupportedOperationException("This node does not support children."); //$NON-NLS-1$
	}

	/**
	 * List of child nodes containing only a reference to this.
	 */
	private final IVMNode[] fChildNodes = { this };

	public IVMNode[] getChildNodes() {
		return fChildNodes;
	}

	@Override
	public void update(final IHasChildrenUpdate[] updates) {
		new Job("") { //$NON-NLS-1$
			{
				setSystem(true);
				setPriority(INTERACTIVE);
			}

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				for (IHasChildrenUpdate update : updates) {
					/*
					 * Do not retrieve directory contents just to mark the plus
					 * sign in the tree.  If it's a directory, just assume that
					 * it has children.
					 */
					FileVMContext vmc = (FileVMContext) update.getElement();
					update.setHasChilren(vmc.getFile().isDirectory());
					update.done();
				}

				return Status.OK_STATUS;
			}
		}.schedule();
	}

	@Override
	public void update(final IChildrenCountUpdate[] updates) {
		new Job("") { //$NON-NLS-1$
			{
				setSystem(true);
				setPriority(INTERACTIVE);
			}

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				for (IChildrenCountUpdate update : updates) {
					update.setChildCount(getFiles(update).length);
					update.done();
				}
				return Status.OK_STATUS;
			}
		}.schedule();
	}

	@Override
	public void update(final IChildrenUpdate[] updates) {
		new Job("") { //$NON-NLS-1$
			{
				setSystem(true);
				setPriority(INTERACTIVE);
			}

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				for (IChildrenUpdate update : updates) {
					File[] files = getFiles(update);
					int offset = update.getOffset() != -1 ? update.getOffset() : 0;
					int length = update.getLength() != -1 ? update.getLength() : files.length;
					for (int i = offset; (i < files.length) && (i < (offset + length)); i++) {
						update.setChild(new FileVMContext(FileVMNode.this, files[i]), i);
					}
					update.done();
				}
				return Status.OK_STATUS;
			}
		}.schedule();
	}

	@Override
	public void update(final ILabelUpdate[] updates) {
		new Job("") { //$NON-NLS-1$
			{
				setSystem(true);
				setPriority(INTERACTIVE);
			}

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				for (ILabelUpdate update : updates) {
					update.setLabel(getLabel((FileVMContext) update.getElement()), 0);
					update.done();
				}

				return Status.OK_STATUS;
			}
		}.schedule();
	}

	private static final File[] EMPTY_FILE_LIST = new File[0];

	/**
	 * Retrieves the list of files for this node.  The list of files is based
	 * on the parent element in the tree, which must be of type FileVMC.
	 *
	 * @param update Update object containing the path (and the parent element)
	 * in the tree viewer.
	 * @return List of files contained in the directory specified in the
	 * update object.  An empty list if the parent element is not a directory.
	 * @throws ClassCastException If the parent element contained in the update
	 * is NOT of type FileVMC.
	 */
	private File[] getFiles(IViewerUpdate update) {
		FileVMContext vmc = (FileVMContext) update.getElement();
		File[] files = vmc.getFile().listFiles();
		return files != null ? files : EMPTY_FILE_LIST;
	}

	/**
	 * Returs the text label to show in the tree for given element.
	 */
	private String getLabel(FileVMContext vmc) {
		return vmc.getFile().getName();
	}

	@Override
	public void getContextsForEvent(VMDelta parentDelta, Object event, DataRequestMonitor<IVMContext[]> rm) {
		rm.setStatus(new Status(IStatus.ERROR, DsfUIPlugin.PLUGIN_ID, IDsfStatusConstants.NOT_SUPPORTED, "", null)); //$NON-NLS-1$
		rm.done();
	}

	@Override
	public int getDeltaFlags(Object e) {
		/*
		 * @see buildDelta()
		 */
		int retVal = IModelDelta.NO_CHANGE;
		if (e instanceof String) {
			retVal |= IModelDelta.SELECT | IModelDelta.EXPAND;
		}

		return retVal;
	}

	@Override
	public void buildDelta(final Object event, final VMDelta parentDelta, final int nodeOffset,
			final RequestMonitor requestMonitor) {
		/*
		 * The FileLayoutNode is recursive, with itself as the only child.  In this
		 * method the delta is calculated for a full path VMContext elements, and the
		 * implementation of this method is not recursive.
		 */
		if (event instanceof String) {
			new Job("") { //$NON-NLS-1$
				{
					setSystem(true);
					setPriority(INTERACTIVE);
				}

				@Override
				protected IStatus run(IProgressMonitor monitor) {
					/*
					 * Requirements for a selection event to be issued is that the file exist, and
					 * that the parentDelta contain a FileVMC of a parent directory as its element.
					 *
					 * The test for first the former requirement could be performed inside getDeltaFlags()
					 * but getDeltaFlags() is synchronous, so it is better to perform this test here using
					 * a background thread (job).
					 *
					 *  The latter is requirement is needed because this node does not have the algorithm
					 *  calculate the complete list of root nodes.  That algorithm is implemented inside the
					 *  {@link FileSystemRootsLayoutNode#updateElements} method.
					 */

					final File eventFile = new File((String) event);
					File parentFile = null;
					if (parentDelta.getElement() instanceof FileVMContext) {
						parentFile = ((FileVMContext) parentDelta.getElement()).getFile();
					}

					// The file has to exist in order for us to be able to select
					// it in the tree.
					if (eventFile.exists() && parentFile != null) {
						// Create a list containing all files in path
						List<File> filePath = new LinkedList<>();
						for (File file = eventFile; file != null
								&& !file.equals(parentFile); file = file.getParentFile()) {
							filePath.add(0, file);
						}

						if (filePath.size() != 0) {
							// Build the delta for all files in path.
							ModelDelta delta = parentDelta;
							File[] allFilesInDirectory = parentFile.listFiles();
							for (File pathSegment : filePath) {
								// All files in path should be directories, and should therefore
								// have a valid list of elements.
								assert allFilesInDirectory != null;

								File[] pathSegmentDirectoryFiles = pathSegment.listFiles();
								delta = delta.addNode(new FileVMContext(FileVMNode.this, pathSegment),
										nodeOffset + Arrays.asList(allFilesInDirectory).indexOf(pathSegment),
										IModelDelta.NO_CHANGE,
										pathSegmentDirectoryFiles != null ? pathSegmentDirectoryFiles.length : 0);
								allFilesInDirectory = pathSegmentDirectoryFiles;
							}

							// The last file in path gets the EXPAND | SELECT flags.
							delta.setFlags(delta.getFlags() | IModelDelta.SELECT | IModelDelta.EXPAND);
						}
					}

					// Invoke the request monitor.

					requestMonitor.done();

					return Status.OK_STATUS;
				}
			}.schedule();
		} else {
			requestMonitor.done();
		}
	}

	/**
	 * Override the behavior which checks for delta flags of all the child nodes,
	 * because we would get stuck in a recursive loop.  Instead call only the child
	 * nodes which are not us.
	 */
	protected Map<IVMNode, Integer> getChildNodesWithDeltas(Object e) {
		Map<IVMNode, Integer> nodes = new HashMap<>();
		for (final IVMNode childNode : getChildNodes()) {
			int delta = childNode.getDeltaFlags(e);
			if (delta != IModelDelta.NO_CHANGE) {
				nodes.put(childNode, delta);
			}
		}
		return nodes;
	}

	@Override
	public IVMProvider getVMProvider() {
		return fProvider;
	}

}
