/*******************************************************************************
 * Copyright (c) 2007, 2024 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:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.api.tools.ui.internal.wizards;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ViewerComparator;
import org.eclipse.jface.wizard.IWizardPage;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.ltk.core.refactoring.resource.DeleteResourceChange;
import org.eclipse.ltk.ui.refactoring.UserInputWizardPage;
import org.eclipse.pde.api.tools.internal.ApiDescriptionProcessor;
import org.eclipse.pde.api.tools.internal.IApiCoreConstants;
import org.eclipse.pde.api.tools.internal.provisional.ApiPlugin;
import org.eclipse.pde.api.tools.internal.util.Util;
import org.eclipse.pde.api.tools.ui.internal.ApiUIPlugin;
import org.eclipse.pde.api.tools.ui.internal.IApiToolsConstants;
import org.eclipse.pde.api.tools.ui.internal.IApiToolsHelpContextIds;
import org.eclipse.pde.api.tools.ui.internal.SWTFactory;
import org.eclipse.pde.internal.core.natures.PluginProject;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchSite;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.progress.WorkbenchJob;

/**
 * The main page for the {@link ApiToolingSetupWizard}
 *
 * @since 1.0.0
 */
public class ApiToolingSetupWizardPage extends UserInputWizardPage {

	/**
	 * Job for updating the filtering on the table viewer
	 */
	class UpdateJob extends WorkbenchJob {

		private String pattern = null;

		/**
		 * Constructor
		 */
		public UpdateJob() {
			super(WizardMessages.ApiToolingSetupWizardPage_filter_update_job);
			setSystem(true);
		}

		/**
		 * Sets the current text filter to use
		 */
		public synchronized void setFilter(String pattern) {
			this.pattern = pattern;
		}

		/**
		 * @see org.eclipse.ui.progress.UIJob#runInUIThread(org.eclipse.core.runtime.IProgressMonitor)
		 */
		@Override
		public IStatus runInUIThread(IProgressMonitor monitor) {
			if (tableviewer != null) {
				try {
					tableviewer.getTable().setRedraw(false);
					synchronized (this) {
						filter.setPattern(pattern + '*');
					}
					tableviewer.refresh(true);
					tableviewer.setCheckedElements(checkedset.toArray());
				} finally {
					tableviewer.getTable().setRedraw(true);
				}
			}
			return Status.OK_STATUS;
		}

	}

	private static final String SETTINGS_SECTION = "ApiToolingSetupWizardPage"; //$NON-NLS-1$
	private static final String SETTINGS_REMOVECXML = "remove_componentxml"; //$NON-NLS-1$

	CheckboxTableViewer tableviewer = null;
	HashSet<Object> checkedset = new HashSet<>();
	Button removecxml = null;
	UpdateJob updatejob = new UpdateJob();
	StringFilter filter = new StringFilter();
	private Text checkcount = null;

	/**
	 * Constructor
	 */
	protected ApiToolingSetupWizardPage() {
		super(WizardMessages.UpdateJavadocTagsWizardPage_4);
		setTitle(WizardMessages.UpdateJavadocTagsWizardPage_4);
		setMessage(WizardMessages.UpdateJavadocTagsWizardPage_7);
	}

	@Override
	public void createControl(Composite parent) {
		Composite comp = SWTFactory.createComposite(parent, 1, 1, GridData.FILL_BOTH);
		setControl(comp);
		PlatformUI.getWorkbench().getHelpSystem().setHelp(comp, IApiToolsHelpContextIds.API_TOOLING_SETUP_WIZARD_PAGE);
		SWTFactory.createWrapLabel(comp, WizardMessages.UpdateJavadocTagsWizardPage_6, 1, 100);
		SWTFactory.createVerticalSpacer(comp, 1);
		SWTFactory.createWrapLabel(comp, WizardMessages.ApiToolingSetupWizardPage_6, 1, 50);

		final Text text = SWTFactory.createText(comp, SWT.BORDER, 1);
		text.addModifyListener(e -> {
			updatejob.setFilter(text.getText().trim());
			updatejob.cancel();
			updatejob.schedule();
		});
		text.addKeyListener(KeyListener.keyPressedAdapter(e -> {
			if (e.keyCode == SWT.ARROW_DOWN) {
				if (tableviewer != null) {
					tableviewer.getTable().setFocus();
				}
			}
		}));

		SWTFactory.createWrapLabel(comp, WizardMessages.UpdateJavadocTagsWizardPage_8, 1, 50);

		Table table = new Table(comp, SWT.BORDER | SWT.FULL_SELECTION | SWT.CHECK | SWT.MULTI);
		GridData gd = new GridData(GridData.FILL_BOTH);
		gd.heightHint = 150;
		table.setLayoutData(gd);
		tableviewer = new CheckboxTableViewer(table);
		tableviewer.setLabelProvider(new WorkbenchLabelProvider());
		tableviewer.setContentProvider(ArrayContentProvider.getInstance());
		tableviewer.setInput(getInputProjects());
		tableviewer.setComparator(new ViewerComparator());
		tableviewer.addFilter(filter);
		tableviewer.addCheckStateListener(event -> {
			if (event.getChecked()) {
				checkedset.add(event.getElement());
			} else {
				checkedset.remove(event.getElement());
			}
			setPageComplete(pageValid());
		});
		Composite bcomp = SWTFactory.createComposite(comp, 3, 1, GridData.FILL_HORIZONTAL | GridData.END, 0, 0);
		Button button = SWTFactory.createPushButton(bcomp, WizardMessages.UpdateJavadocTagsWizardPage_10, null);
		button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			tableviewer.setAllChecked(true);
			checkedset.addAll(Arrays.asList(tableviewer.getCheckedElements()));
			setPageComplete(pageValid());
		}));
		button = SWTFactory.createPushButton(bcomp, WizardMessages.UpdateJavadocTagsWizardPage_11, null);
		button.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			tableviewer.setAllChecked(false);
			TableItem[] items = tableviewer.getTable().getItems();
			for (TableItem item : items) {
				checkedset.remove(item.getData());
			}
			setPageComplete(pageValid());
		}));

		checkcount = SWTFactory.createText(bcomp, SWT.FLAT | SWT.READ_ONLY, 1, GridData.HORIZONTAL_ALIGN_END | GridData.GRAB_HORIZONTAL);
		checkcount.setBackground(bcomp.getBackground());

		Object[] selected = getWorkbenchSelection();
		if (selected.length > 0) {
			tableviewer.setCheckedElements(selected);
			checkedset.addAll(Arrays.asList(selected));
		}
		setPageComplete(checkedset.size() > 0);

		SWTFactory.createVerticalSpacer(comp, 1);
		removecxml = SWTFactory.createCheckButton(comp, WizardMessages.ApiToolingSetupWizardPage_0, null, true, 1);

		IDialogSettings settings = ApiUIPlugin.getDefault().getDialogSettings().getSection(SETTINGS_SECTION);
		if (settings != null) {
			removecxml.setSelection(settings.getBoolean(SETTINGS_REMOVECXML));
		}
	}

	@Override
	public void setPageComplete(boolean complete) {
		super.setPageComplete(complete);
		updateCheckStatus(checkedset.size());
	}

	/**
	 * Updates the number of items that have been checked
	 */
	private void updateCheckStatus(int count) {
		if (checkcount == null) {
			return;
		}
		checkcount.setText(MessageFormat.format(WizardMessages.ApiToolingSetupWizardPage_n_items_checked, Integer.toString(count)));
	}

	/**
	 * @return the complete listing of projects in the workspace that could have
	 *         API Tools set-up on them
	 */
	private IProject[] getInputProjects() {
		IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
		return Arrays.stream(projects).filter(this::acceptProject).toArray(IProject[]::new);
	}

	private boolean acceptProject(IProject project) {
		if (project == null) {
			return false;
		}
		return PluginProject.isJavaProject(project) && PluginProject.isPluginProject(project)
				&& !Util.isApiProject(project) && !Util.isBinaryProject(project);
	}

	/**
	 * @return the current selection from the workbench as an array of objects
	 */
	protected Object[] getWorkbenchSelection() {
		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		if (window != null) {
			IWorkbenchPage page = window.getActivePage();
			if (page != null) {
				IWorkbenchPart part = page.getActivePart();
				if (part != null) {
					IWorkbenchSite site = part.getSite();
					if (site != null) {
						ISelectionProvider provider = site.getSelectionProvider();
						if (provider != null && provider.getSelection() instanceof IStructuredSelection selection) {
							return Arrays.stream(selection.toArray()).filter(IAdaptable.class::isInstance)
									.map(IAdaptable.class::cast).map(a -> a.getAdapter(IProject.class))
									.filter(this::acceptProject).toArray();
						}
					}
				}
			}
		}
		return new Object[0];
	}

	/**
	 * @return if the page is valid or not, this method also sets error messages
	 */
	protected boolean pageValid() {
		if (checkedset.size() < 1) {
			setErrorMessage(WizardMessages.UpdateJavadocTagsWizardPage_12);
			return false;
		}
		setErrorMessage(null);
		return true;
	}

	@Override
	public IWizardPage getNextPage() {
		// always have to collect changes again in the event the user goes back
		// and forth,
		// as a change cannot ever have more than one parent - EVER
		collectChanges();
		IWizardPage page = super.getNextPage();
		if (page != null) {
			page.setDescription(WizardMessages.ApiToolingSetupWizardPage_5);
		}
		return page;
	}

	/**
	 * Creates all of the text edit changes collected from the processor. The
	 * collected edits are arranged as multi-edits for the one file that they
	 * belong to
	 */
	void createTagChanges(CompositeChange projectchange, IJavaProject project, File cxml) {
		try {
			HashMap<IFile, Set<TextEdit>> map = new HashMap<>();
			ApiDescriptionProcessor.collectTagUpdates(project, cxml, map);
			IFile file = null;
			TextFileChange change = null;
			MultiTextEdit multiedit = null;
			Set<TextEdit> alledits = null;
			for (Entry<IFile, Set<TextEdit>> entry : map.entrySet()) {
				file = entry.getKey();
				change = new TextFileChange(MessageFormat.format(WizardMessages.JavadocTagRefactoring_2, file.getName()), file);
				multiedit = new MultiTextEdit();
				change.setEdit(multiedit);
				alledits = entry.getValue();
				if (alledits != null) {
					for (TextEdit edit : alledits) {
						multiedit.addChild(edit);
					}
				}
				projectchange.add(change);
			}
		} catch (CoreException e) {
			ApiUIPlugin.log(e);
		}
	}

	/**
	 * @return the mapping of text edits to the IFile they occur on
	 */
	private void collectChanges() {
		final ApiToolingSetupRefactoring refactoring = (ApiToolingSetupRefactoring) getRefactoring();
		IRunnableWithProgress op = monitor -> {
			Object[] projects = checkedset.toArray(new IProject[checkedset.size()]);
			IProject project = null;
			SubMonitor localmonitor = SubMonitor.convert(monitor);
			localmonitor.beginTask(IApiToolsConstants.EMPTY_STRING, projects.length);
			localmonitor.setTaskName(WizardMessages.ApiToolingSetupWizardPage_7);
			refactoring.resetRefactoring();
			boolean remove = removecxml.getSelection();
			CompositeChange pchange = null;
			for (Object project2 : projects) {
				project = (IProject) project2;
				pchange = new CompositeChange(project.getName());
				refactoring.addChange(pchange);
				pchange.add(new ProjectUpdateChange(project));
				localmonitor
						.subTask(MessageFormat.format(WizardMessages.ApiToolingSetupWizardPage_4, project.getName()));
				IResource cxml = project.findMember(IApiCoreConstants.COMPONENT_XML_NAME);
				if (cxml != null) {
					// collect the changes for doc
					createTagChanges(pchange, JavaCore.create(project), new File(cxml.getLocationURI()));
					if (remove) {
						pchange.add(new DeleteResourceChange(cxml.getFullPath(), true));
					}
				}
				localmonitor.split(1);
			}
		};
		try {
			getContainer().run(false, false, op);
		} catch (InvocationTargetException | InterruptedException e) {
			ApiUIPlugin.log(e);
		}
	}

	@Override
	protected boolean performFinish() {
		collectChanges();
		return super.performFinish();
	}

	/**
	 * Called by the {@link ApiToolingSetupWizard} when finishing the wizard
	 *
	 * @return true if the page finished normally, false otherwise
	 */
	public boolean finish() {
		IDialogSettings settings = ApiUIPlugin.getDefault().getDialogSettings().addNewSection(SETTINGS_SECTION);
		settings.put(SETTINGS_REMOVECXML, removecxml.getSelection());
		notifyNoDefaultProfile();
		return true;
	}

	/**
	 * Notifies the user that they have no default API profile
	 */
	private void notifyNoDefaultProfile() {
		if (ApiPlugin.getDefault().getApiBaselineManager().getDefaultApiBaseline() == null) {
			UIJob job = UIJob.create("No default API profile detected", monitor -> { //$NON-NLS-1$
				String msg = WizardMessages.ApiToolingSetupWizardPage_2 + WizardMessages.ApiToolingSetupWizardPage_3;
				if (MessageDialog.openQuestion(getShell(), WizardMessages.ApiToolingSetupWizardPage_1, msg)) {
					SWTFactory.showPreferencePage(getShell(), IApiToolsConstants.ID_BASELINES_PREF_PAGE, null);
				}
				return Status.OK_STATUS;
			});
			job.setSystem(true);
			job.schedule();
		}
	}
}
