/*******************************************************************************
 * Copyright (c) 2001, 2008 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * Contributors:
 * IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.wst.validation.internal.ui;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IViewActionDelegate;
import org.eclipse.ui.IViewPart;
import org.eclipse.wst.validation.ValidationFramework;
import org.eclipse.wst.validation.internal.ConfigurationManager;
import org.eclipse.wst.validation.internal.DisabledResourceManager;
import org.eclipse.wst.validation.internal.ValType;
import org.eclipse.wst.validation.internal.ValidationRegistryReader;
import org.eclipse.wst.validation.internal.ValidationSelectionHandlerRegistryReader;
import org.eclipse.wst.validation.internal.plugin.ValidationPlugin;
import org.eclipse.wst.validation.internal.ui.plugin.ValidationUIPlugin;
import org.eclipse.wst.validation.ui.internal.ManualValidationRunner;

/**
 * This class implements the pop-up menu item "Run Validation" When the item is selected, this
 * action triggers a validation of the project, using all configured, enabled validators.
 */
public class ValidationMenuAction implements IViewActionDelegate {
	private ISelection 		_currentSelection;
	protected static final String SEP = "/"; //$NON-NLS-1$
	private Display 		_currentDisplay;
	private IResourceVisitor _folderVisitor;
	private IResourceVisitor _projectVisitor;
	private Map<IProject, Set<IResource>> _selectedResources;

	public ValidationMenuAction() {
		// cache the display before this action is forked. After the action is forked,
		// Display.getCurrent() returns null.
		_currentDisplay = Display.getCurrent(); 
		_selectedResources = new HashMap<IProject, Set<IResource>>();
	}

	private Display getDisplay() {
		return (_currentDisplay == null) ? Display.getCurrent() : _currentDisplay;
	}

	/**
	 * Return the wizard's shell.
	 */
	Shell getShell() {
		Display display = getDisplay();
		Shell shell = (display == null) ? null : display.getActiveShell();
		if (shell == null && display != null) {
			Shell[] shells = display.getShells();
			if (shells.length > 0)
				shell = shells[0];
		}
		return shell;
	}

	/**
	 * Return a map of the selected elements. Each key of the map is an IProject, and the value is a
	 * Set of the selected resources in that project. If a project is selected, and nothing else in
	 * the project is selected, a full validation (null value) will be done on the project. If a
	 * project is selected, and some files/folders in the project are also selected, only the
	 * files/folders will be validated. If a folder is selected, all of its contents are also
	 * validated.
	 * 
	 * @return null if there is no selection.
	 */
	private Map<IProject, Set<IResource>> loadSelected(ValidateAction action) {
		// GRK previously this did not do a clear, but I couldn't understand why that would be so I am forcing a clear
		// GRK In my testing, not doing a clear caused duplicate validations
		_selectedResources.clear();
		if ((_currentSelection == null) || _currentSelection.isEmpty() || 
			!(_currentSelection instanceof IStructuredSelection))return null;
		
		Object[] elements = ((IStructuredSelection) _currentSelection).toArray();
		for (Object element : elements) {
			if (element != null)addSelected(action, element);
		}
		return _selectedResources;
	}

	private void addSelected(ValidateAction action, Object selected) {
		if (selected instanceof IProject) {
			addVisitor((IProject) selected);
		} else if (selected instanceof IFile) {
			addSelected((IFile) selected);
		} else if (selected instanceof IFolder) {
			addVisitor((IFolder) selected);
		} else if (isValidType(getExtendedType(selected))) {
			addSelected(action,getExtendedType(selected));
		} else {
			// Not a valid input type. Must be IProject, IJavaProject, or IResource.
			// If this ValidationMenuAction is a delegate of ValidateAction, is
			// the input type recognized by the ValidateAction?
			boolean valid = false;
			if (action != null) {
				IResource[] resources = action.getResource(selected);
				if (resources != null) {
					valid = true;
					for (int i = 0; i < resources.length; i++) {
						addSelected(action, resources[i]);
					}
				}
			}
			if (!valid) {
				// Stop processing. This allows the "Run Validation" menu item
				// to gray out once an element that can not be validated is selected.
				_selectedResources.clear();
			}
		}
	}
	
	private Object getExtendedType(Object selected) {
		Object result = ValidationSelectionHandlerRegistryReader.getInstance().getExtendedType(selected);
		return result == null ? selected : result;
	}
	
	private boolean isValidType(Object object) {
		return object instanceof IProject || object instanceof IFile || object instanceof IFolder;
	}


	void addSelected(IResource selected) {
		IProject project = selected.getProject();
		boolean added = _selectedResources.containsKey(project);
		Set<IResource> changedRes = null;
		if (added) {
			// If the value is null, the entire project needs to be validated anyway.
			changedRes = _selectedResources.get(project);
			if (changedRes == null)return;
			
		} else {
			changedRes = new HashSet<IResource>();
		}
		if (changedRes.add(selected)) {
			_selectedResources.put(project, changedRes);
		}
	}

	private void addVisitor(IFolder selected) {
		// add the folder and its children
		try {
			selected.accept(getFolderVisitor());
		} catch (CoreException exc) {
			ValidationUIPlugin.getPlugin().handleException(exc);
			return;
		}
	}

	private IResourceVisitor getFolderVisitor() {
		if (_folderVisitor == null) {
			_folderVisitor = new IResourceVisitor() {
				public boolean visit(IResource res) {
					if (res instanceof IFile) {
						addSelected(res);
					} else if (res instanceof IFolder) {
						addSelected(res);
					}
					return true; // visit the resource's children
				}
			};
		}
		return _folderVisitor;
	}
	
	private void addVisitor(IProject selected) {
		// add the folder and its children
		if(!selected.isAccessible())return;
		try {
			selected.accept(getProjectVisitor());
		} catch (CoreException exc) {
			ValidationUIPlugin.getPlugin().handleException(exc);
			return;
		}
	}

	private IResourceVisitor getProjectVisitor() {
		if (_projectVisitor == null) {
			_projectVisitor = new IResourceVisitor() {
				public boolean visit(IResource res) {
					if (DisabledResourceManager.getDefault().isDisabled(res))return false;
					if (res instanceof IFile)addSelected(res);
					else if (res instanceof IFolder)addSelected(res);
					else if (res instanceof IProject)addSelected(res);
					
					return true;
				}
			};
		}
		return _projectVisitor;
	}

	/**
	 * The delegating action has been invoked. This method does the actual work.
	 * 
	 * @param action
	 *            Action proxy that handles the presentation portion of the plug-in action.
	 */
	public void run(IAction action) {
		ValidateAction vaction = null;
		if (action instanceof ValidateAction) {
			vaction = (ValidateAction) action;
		}
		final Map<IProject, Set<IResource>> projects = loadSelected(vaction);
		if ((projects == null) || (projects.size() == 0)) {
			return;
		}
		
		// If the files aren't saved do not run validation.
		if(!handleFilesToSave(projects))return;

		boolean confirm = org.eclipse.wst.validation.internal.ValManager.getDefault().getGlobalPreferences()
			.getConfirmDialog();
		ManualValidationRunner.validate(projects, ValType.Manual, confirm);
	}
	
	/**
	 * Selection in the desktop has changed. Plug-in provider can use it to change the availability
	 * of the action or to modify other presentation properties.
	 * 
	 * <p>
	 * Action delegate cannot be notified about selection changes before it is loaded. For that
	 * reason, control of action's enable state should also be performed through simple XML rules
	 * defined for the extension point. These rules allow enable state control before the delegate
	 * has been loaded.
	 * </p>
	 * 
	 * @param action
	 *            action proxy that handles presentation portion of the plug-in action
	 * @param selection
	 *            current selection in the desktop
	 */
	public void selectionChanged(IAction action, ISelection selection) {
		_currentSelection = selection;
		boolean enabled = quickCheck(selection);
		
		// Don't force the plug-in to be activated just to check this setting.
		if (enabled && ValidationPlugin.isActivated() && ValidationRegistryReader.isActivated()){
			enabled = hasManualValidators(selection);
		}
		action.setEnabled(enabled);
	}
	
	/**
	 * Do a quick check on the selection, so see if we know that we don't want to validate the selection.
	 * 
	 * @param selection
	 * @return false if we are sure that we don't want to validate it. Return true if we are still not sure.
	 */
	private boolean quickCheck(ISelection selection){
		if (selection == null || selection.isEmpty())return false;
		if (selection instanceof IStructuredSelection){
			IStructuredSelection ss = (IStructuredSelection)selection;
			Object sel = ss.getFirstElement();
			if (sel != null){
				if (sel instanceof IProject){
					IProject project = (IProject)sel;
					if (!project.isOpen())return false;
				}
			}
		}

		return true;
	}
	
	/**
	 * Answer true if any of the selected items have manual validators enabled.
	 * @param selection
	 */
	private boolean hasManualValidators(ISelection selection){
				
		if (selection instanceof IStructuredSelection){
			IStructuredSelection ss = (IStructuredSelection)selection;
			for (Iterator it = ss.iterator(); it.hasNext();){
				Object sel = it.next();
				if (sel instanceof IResource){
					IResource resource = (IResource)sel;
					if (ValidationFramework.getDefault().hasValidators(resource, true, false))return true;
				}
			}		
		}
		
		return false;
		
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.IViewActionDelegate#init(org.eclipse.ui.IViewPart)
	 */
	public void init(IViewPart view) { 
	}
	
	/**
	 * Handle any files that must be saved prior to running
	 * validation.
	 * 
	 * @param projects
	 * 			The list of projects that will be validated.
	 * @return
	 * 			True if all files have been saved, false otherwise.
	 */
	protected boolean handleFilesToSave(Map<IProject, Set<IResource>> projects)
	{
	  List fileList = getIFiles(projects);
      final IEditorPart[] dirtyEditors = SaveFilesHelper.getDirtyEditors(fileList);
      if(dirtyEditors == null || dirtyEditors.length == 0)return true;
      boolean saveAutomatically = false;
      try
      {
        saveAutomatically = ConfigurationManager.getManager().getGlobalConfiguration().getSaveAutomatically();
      }
      catch(InvocationTargetException e)
      {
    	// In this case simply default to false.
      }
      SaveFilesDialog sfDialog = null;
      if(!saveAutomatically)
      {
	    sfDialog = new SaveFilesDialog(ValidationUIPlugin.getPlugin().getWorkbench().getActiveWorkbenchWindow().getShell());
	    sfDialog.setInput(Arrays.asList(dirtyEditors));
      }
      
      if(saveAutomatically || sfDialog.open() == Window.OK){
    	  ProgressMonitorDialog ctx = new ProgressMonitorDialog(getShell());
          
          IRunnableWithProgress runnable = new IRunnableWithProgress(){
              public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException{
            	  try {
            		  monitor.beginTask(ValidationUIMessages.SaveFilesDialog_saving, dirtyEditors.length);
            		  int numDirtyEditors = dirtyEditors.length;
                      for(int i = 0; i < numDirtyEditors; i++){
                    	  dirtyEditors[i].doSave(new SubProgressMonitor(monitor, 1));
                      }
            	  } finally {
            		  monitor.done();
            	  }
             }
          };
          
          try {
                ctx.run(false, true, runnable);
                return true;
          } catch (InvocationTargetException e) {
                ValidationUIPlugin.getPlugin().handleException(e);
          } catch (InterruptedException e) {
        	  ValidationUIPlugin.getPlugin().handleException(e);
          }
      }
	  return false;
	}
	
	protected List<IFile> getIFiles(Map<IProject, Set<IResource>> projects) {
		List<IFile> fileList = new LinkedList<IFile>();
		for(IProject project : projects.keySet()) {
		  for(IResource resource : projects.get(project)) {
			if(resource instanceof IFile)fileList.add((IFile)resource);
		  }
		}
		return fileList;
	}
	
}
