/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation 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:
 *   Jesper Kamstrup Linnet (eclipse@kamstrup-linnet.dk) - initial API and implementation
 * 			(report 36180: Callers/Callees view)
 *   Michael Fraenkel (fraenkel@us.ibm.com) - patch
 *          (report 60714: Call Hierarchy: display search scope in view title)
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.callhierarchy;

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

import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.IDialogSettings;

import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkingSet;
import org.eclipse.ui.IWorkingSetManager;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionGroup;

import org.eclipse.jdt.core.search.IJavaSearchScope;

import org.eclipse.jdt.internal.corext.util.Messages;

import org.eclipse.jdt.ui.IContextMenuConstants;

class SearchScopeActionGroup extends ActionGroup {
	private static final String TAG_SEARCH_SCOPE_TYPE= "search_scope_type"; //$NON-NLS-1$
	private static final String TAG_SELECTED_WORKING_SET= "working_set"; //$NON-NLS-1$
	private static final String TAG_WORKING_SET_COUNT = "working_set_count"; //$NON-NLS-1$

	private static final String DIALOGSTORE_SCOPE_TYPE= "SearchScopeActionGroup.search_scope_type"; //$NON-NLS-1$
	private static final String DIALOGSTORE_SELECTED_WORKING_SET= "SearchScopeActionGroup.working_set";  //$NON-NLS-1$

	static final int SEARCH_SCOPE_TYPE_WORKSPACE= 1;
	static final int SEARCH_SCOPE_TYPE_PROJECT= 2;
	// static final int SEARCH_SCOPE_TYPE_HIERARCHY= 3;
	static final int SEARCH_SCOPE_TYPE_WORKING_SET= 4;

	private SearchScopeAction fSelectedAction = null;
	private String[] fSelectedWorkingSetNames = null;
	private CallHierarchyViewPart fView;
	private IDialogSettings fDialogSettings;
	private SearchScopeProjectAction fSearchScopeProjectAction;
	private SearchScopeWorkspaceAction fSearchScopeWorkspaceAction;
	private SelectWorkingSetAction fSelectWorkingSetAction;

	public SearchScopeActionGroup(CallHierarchyViewPart view, IDialogSettings dialogSettings) {
		this.fView= view;
		this.fDialogSettings= dialogSettings;
		createActions();
	}

	/**
	 * Returns the current search scope.
	 *
	 * @param includeMask the include mask
	 * @return the current search scope
	 * @since 3.7
	 */
	public IJavaSearchScope getSearchScope(int includeMask) {
		if (fSelectedAction != null) {
			return fSelectedAction.getSearchScope(includeMask);
		}

		return null;
	}

	@Override
	public void fillActionBars(IActionBars actionBars) {
		super.fillActionBars(actionBars);
		fillContextMenu(actionBars.getMenuManager());
	}

	protected void setActiveWorkingSets(IWorkingSet[] sets) {
		if (sets != null) {
			fSelectedWorkingSetNames = getWorkingSetNames(sets);
			fSelectedAction = new SearchScopeWorkingSetAction(this, sets, getScopeDescription(sets));
			fSelectedAction.run();
		} else {
			fSelectedWorkingSetNames = null;
			fSelectedAction = null;
		}
	}

	private String[] getWorkingSetNames(IWorkingSet[] sets) {
		String[] result= new String[sets.length];
		for (int i = 0; i < sets.length; i++) {
			result[i]= sets[i].getName();
		}
		return result;
	}

	protected IWorkingSet[] getActiveWorkingSets() {
		if (fSelectedWorkingSetNames != null) {
			return getWorkingSets(fSelectedWorkingSetNames);
		}

		return null;
	}

	private IWorkingSet[] getWorkingSets(String[] workingSetNames) {
		if (workingSetNames == null) {
			return null;
		}
		Set<IWorkingSet> workingSets= new HashSet<>(2);
		for (String workingSetName : workingSetNames) {
			IWorkingSet workingSet= getWorkingSetManager().getWorkingSet(workingSetName);
			if (workingSet != null) {
				workingSets.add(workingSet);
			}
		}

		return workingSets.toArray(new IWorkingSet[workingSets.size()]);
	}

	/**
	 * Sets the new search scope type.
	 *
	 * @param newSelection New action which should be the checked one (can be null iff <code>ignoreUnchecked == false</code>)
	 * @param ignoreUnchecked Ignores actions which are unchecked (necessary since both the old and the new action fires).
	 */
	protected void setSelected(SearchScopeAction newSelection, boolean ignoreUnchecked) {
		if (!ignoreUnchecked || newSelection.isChecked()) {
			if (newSelection instanceof SearchScopeWorkingSetAction) {
				fSelectedWorkingSetNames = getWorkingSetNames(((SearchScopeWorkingSetAction) newSelection).getWorkingSets());
			} else {
				fSelectedWorkingSetNames = null;
			}

			if (newSelection != null) {
				fSelectedAction= newSelection;
			} else {
				fSelectedAction= fSearchScopeWorkspaceAction;
			}

			fDialogSettings.put(DIALOGSTORE_SCOPE_TYPE, getSearchScopeType());
			fDialogSettings.put(DIALOGSTORE_SELECTED_WORKING_SET, fSelectedWorkingSetNames);
		}
	}

	protected CallHierarchyViewPart getView() {
		return fView;
	}

	protected IWorkingSetManager getWorkingSetManager() {
		IWorkingSetManager workingSetManager = PlatformUI.getWorkbench()
		.getWorkingSetManager();

		return workingSetManager;
	}

	protected void fillSearchActions(IMenuManager javaSearchMM) {
		for (Action action :  getActions()) {
			if (action.isEnabled()) {
				javaSearchMM.add(action);
			}
		}

		javaSearchMM.setVisible(!javaSearchMM.isEmpty());
	}

	@Override
	public void fillContextMenu(IMenuManager menu) {
		MenuManager javaSearchMM = new MenuManager(CallHierarchyMessages.SearchScopeActionGroup_searchScope,
				IContextMenuConstants.GROUP_SEARCH);
		javaSearchMM.setRemoveAllWhenShown(true);

		javaSearchMM.addMenuListener(this::fillSearchActions);

		fillSearchActions(javaSearchMM);
		menu.appendToGroup(IContextMenuConstants.GROUP_SEARCH, javaSearchMM);
	}

	private Action[] getActions() {
		List<Action> actions = new ArrayList<>(SearchUtil.LRU_WORKINGSET_LIST_SIZE + 4);
		addAction(actions, fSearchScopeWorkspaceAction);
		addAction(actions, fSearchScopeProjectAction);
		addAction(actions, fSelectWorkingSetAction);

		Iterator<IWorkingSet[]> iter= SearchUtil.getLRUWorkingSets().sortedIterator();
		while (iter.hasNext()) {
			IWorkingSet[] workingSets= iter.next();
			String description = SearchUtil.toString(workingSets);
			SearchScopeWorkingSetAction workingSetAction = new SearchScopeWorkingSetAction(this, workingSets, description);

			if (isSelectedWorkingSet(workingSets)) {
				workingSetAction.setChecked(true);
			}

			actions.add(workingSetAction);
		}

		Action[] result = actions.toArray(new Action[actions.size()]);

		ensureExactlyOneCheckedAction(result);
		return result;
	}

	private void ensureExactlyOneCheckedAction(Action[] result) {
		int checked = getCheckedActionCount(result);
		if (checked != 1) {
			if (checked > 1) {
				for (Action action : result) {
					action.setChecked(false);
				}
			}
			fSearchScopeWorkspaceAction.setChecked(true);
		}
	}

	private int getCheckedActionCount(Action[] result) {
		// Ensure that exactly one action is selected
		int checked= 0;
		for (Action action : result) {
			if (action.isChecked()) {
				checked++;
			}
		}
		return checked;
	}

	private void addAction(List<Action> actions, Action action) {
		if (action == fSelectedAction) {
			action.setChecked(true);
		} else {
			action.setChecked(false);
		}

		actions.add(action);
	}

	private void createActions() {
		fSearchScopeWorkspaceAction = new SearchScopeWorkspaceAction(this);
		fSelectWorkingSetAction = new SelectWorkingSetAction(this);
		fSearchScopeProjectAction = new SearchScopeProjectAction(this);

		int searchScopeType;
		try {
			searchScopeType= fDialogSettings.getInt(DIALOGSTORE_SCOPE_TYPE);
		} catch (NumberFormatException e) {
			searchScopeType= SEARCH_SCOPE_TYPE_WORKSPACE;
		}
		String[] workingSetNames= fDialogSettings.getArray(DIALOGSTORE_SELECTED_WORKING_SET);
		setSelected(getSearchScopeAction(searchScopeType, workingSetNames), false);
	}

	public void saveState(IMemento memento) {
		int type= getSearchScopeType();
		memento.putInteger(TAG_SEARCH_SCOPE_TYPE, type);
		if (type == SEARCH_SCOPE_TYPE_WORKING_SET) {
			memento.putInteger(TAG_WORKING_SET_COUNT, fSelectedWorkingSetNames.length);
			for (int i = 0; i < fSelectedWorkingSetNames.length; i++) {
				String workingSetName = fSelectedWorkingSetNames[i];
				memento.putString(TAG_SELECTED_WORKING_SET+i, workingSetName);
			}
		}
	}

	public void restoreState(IMemento memento) {
		String[] workingSetNames= null;
		Integer scopeType= memento.getInteger(TAG_SEARCH_SCOPE_TYPE);
		if (scopeType != null) {
			if (scopeType.intValue() == SEARCH_SCOPE_TYPE_WORKING_SET) {
				Integer workingSetCount= memento.getInteger(TAG_WORKING_SET_COUNT);
				if (workingSetCount != null) {
					workingSetNames = new String[workingSetCount.intValue()];
					for (int i = 0; i < workingSetCount.intValue(); i++) {
						workingSetNames[i]= memento.getString(TAG_SELECTED_WORKING_SET+i);
					}
				}
			}
			setSelected(getSearchScopeAction(scopeType, workingSetNames), false);
		}
	}

	private SearchScopeAction getSearchScopeAction(int searchScopeType, String[] workingSetNames) {
		switch (searchScopeType) {
			case SEARCH_SCOPE_TYPE_WORKSPACE:
				return fSearchScopeWorkspaceAction;
			case SEARCH_SCOPE_TYPE_PROJECT:
				return fSearchScopeProjectAction;
			case SEARCH_SCOPE_TYPE_WORKING_SET:
				IWorkingSet[] workingSets= getWorkingSets(workingSetNames);
				if (workingSets != null && workingSets.length > 0) {
					return new SearchScopeWorkingSetAction(this, workingSets, getScopeDescription(workingSets));
				}
				return null;
		}
		return null;
	}

	private int getSearchScopeType() {
		if (fSelectedAction != null) {
			return fSelectedAction.getSearchScopeType();
		}
		return 0;
	}

	private String getScopeDescription(IWorkingSet[] workingSets) {
		return Messages.format(CallHierarchyMessages.WorkingSetScope, new String[] {SearchUtil.toString(workingSets)});
	}

	/**
	 * Determines whether the specified working sets correspond to the currently selected working
	 * sets.
	 *
	 * @param workingSets the array of working sets
	 * @return <code>true</code> if the specified working sets correspond to the currently selected
	 *         working sets
	 */
	private boolean isSelectedWorkingSet(IWorkingSet[] workingSets) {
		if (fSelectedWorkingSetNames != null && fSelectedWorkingSetNames.length == workingSets.length) {
			Set<String> workingSetNames= new HashSet<>(workingSets.length);
			for (IWorkingSet workingSet : workingSets) {
				workingSetNames.add(workingSet.getName());
			}
			for (String selectedWorkingSetName : fSelectedWorkingSetNames) {
				if (!workingSetNames.contains(selectedWorkingSetName)) {
					return false;
				}
			}
			return true;
		}
		return false;
	}

	/**
	 * Fetches the full description of the scope with the appropriate include mask.
	 *
	 * @param includeMask the include mask
	 * @return the description of the scope with the appropriate include mask
	 * @since 3.7
	 */
	public String getFullDescription(int includeMask) {
		if (fSelectedAction != null)
			return fSelectedAction.getFullDescription(includeMask);
		return null;
	}
}

