/*******************************************************************************
 *  Copyright (c) 2008, 2018 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
 *     Rapicorp, Inc. - add support for information dialog
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.ui;

import java.net.URL;
import java.security.cert.Certificate;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.equinox.internal.p2.ui.dialogs.TrustCertificateDialog;
import org.eclipse.equinox.internal.p2.ui.dialogs.UserValidationDialog;
import org.eclipse.equinox.internal.p2.ui.viewers.CertificateLabelProvider;
import org.eclipse.equinox.p2.core.UIServices;
import org.eclipse.equinox.p2.ui.LoadMetadataRepositoryJob;
import org.eclipse.jface.dialogs.*;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.PlatformUI;

/**
 * The default GUI-based implementation of {@link UIServices}.
 * The service declaration is made in the serviceui_component.xml file.

 */
public class ValidationDialogServiceUI extends UIServices {

	static final class MessageDialogWithLink extends MessageDialog {
		private final String linkText;

		MessageDialogWithLink(Shell parentShell, String dialogTitle, Image dialogTitleImage, String dialogMessage, int dialogImageType, String[] dialogButtonLabels, int defaultIndex, String linkText) {
			super(parentShell, dialogTitle, dialogTitleImage, dialogMessage, dialogImageType, dialogButtonLabels, defaultIndex);
			this.linkText = linkText;
		}

		@Override
		protected Control createCustomArea(Composite parent) {
			if (linkText == null)
				return super.createCustomArea(parent);

			Link link = new Link(parent, SWT.NONE);
			link.setText(linkText);
			link.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
				try {
					URL url = new URL(e.text);
					PlatformUI.getWorkbench().getBrowserSupport().getExternalBrowser().openURL(url);
				} catch (Exception x) {
					ProvUIActivator.getDefault().getLog().log(//
							new Status(IStatus.ERROR, ProvUIActivator.PLUGIN_ID, x.getMessage(), x));
				}
			}));
			return link;
		}
	}

	/**
	 * Subclassed to add a cancel button to the error dialog.
	 */
	static class OkCancelErrorDialog extends ErrorDialog {

		public OkCancelErrorDialog(Shell parentShell, String dialogTitle, String message, IStatus status, int displayMask) {
			super(parentShell, dialogTitle, message, status, displayMask);
		}

		@Override
		protected void createButtonsForButtonBar(Composite parent) {
			// create OK and Details buttons
			createButton(parent, IDialogConstants.OK_ID, ProvUIMessages.ServiceUI_InstallAnywayAction_Label, true);
			createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, true);
			createDetailsButton(parent);
		}
	}

	@Override
	public AuthenticationInfo getUsernamePassword(final String location) {

		final AuthenticationInfo[] result = new AuthenticationInfo[1];
		if (!suppressAuthentication() && !isHeadless()) {
			PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
				Shell shell = ProvUI.getDefaultParentShell();
				String message = NLS.bind(ProvUIMessages.ServiceUI_LoginDetails, location);
				UserValidationDialog dialog = new UserValidationDialog(shell, ProvUIMessages.ServiceUI_LoginRequired, null, message);
				int dialogCode = dialog.open();
				if (dialogCode == Window.OK) {
					result[0] = dialog.getResult();
				} else if (dialogCode == Window.CANCEL) {
					result[0] = AUTHENTICATION_PROMPT_CANCELED;
				}
			});
		}
		return result[0];
	}

	private boolean suppressAuthentication() {
		Job job = Job.getJobManager().currentJob();
		if (job != null) {
			return job.getProperty(LoadMetadataRepositoryJob.SUPPRESS_AUTHENTICATION_JOB_MARKER) != null;
		}
		return false;
	}

	@Override
	public TrustInfo getTrustInfo(Certificate[][] untrustedChains, final String[] unsignedDetail) {
		boolean trustUnsigned = true;
		boolean persistTrust = false;
		Certificate[] trusted = new Certificate[0];
		// Some day we may summarize all of this in one UI, or perhaps we'll have a preference to honor regarding
		// unsigned content.  For now we prompt separately first as to whether unsigned detail should be trusted
		if (!isHeadless() && unsignedDetail != null && unsignedDetail.length > 0) {
			final boolean[] result = new boolean[] {false};
			PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable() {
				@Override
				public void run() {
					Shell shell = ProvUI.getDefaultParentShell();
					OkCancelErrorDialog dialog = new OkCancelErrorDialog(shell, ProvUIMessages.ServiceUI_warning_title, null, createStatus(), IStatus.WARNING);
					result[0] = dialog.open() == IDialogConstants.OK_ID;
				}

				private IStatus createStatus() {
					MultiStatus parent = new MultiStatus(ProvUIActivator.PLUGIN_ID, 0, ProvUIMessages.ServiceUI_unsigned_message, null);
					for (String element : unsignedDetail) {
						parent.add(new Status(IStatus.WARNING, ProvUIActivator.PLUGIN_ID, element));
					}
					return parent;
				}
			});
			trustUnsigned = result[0];
		}
		// For now, there is no need to show certificates if there was unsigned content and we don't trust it.
		if (!trustUnsigned)
			return new TrustInfo(trusted, persistTrust, trustUnsigned);

		// We've established trust for unsigned content, now examine the untrusted chains
		if (!isHeadless() && untrustedChains != null && untrustedChains.length > 0) {

			final Object[] result = new Object[1];
			final TreeNode[] input = createTreeNodes(untrustedChains);

			PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
				Shell shell = ProvUI.getDefaultParentShell();
				ILabelProvider labelProvider = new CertificateLabelProvider();
				TreeNodeContentProvider contentProvider = new TreeNodeContentProvider();
				TrustCertificateDialog trustCertificateDialog = new TrustCertificateDialog(shell, input, labelProvider, contentProvider);
				trustCertificateDialog.open();
				Certificate[] values = new Certificate[trustCertificateDialog.getResult() == null ? 0 : trustCertificateDialog.getResult().length];
				for (int i = 0; i < values.length; i++) {
					values[i] = (Certificate) ((TreeNode) trustCertificateDialog.getResult()[i]).getValue();
				}
				result[0] = values;
			});
			persistTrust = true;
			trusted = (Certificate[]) result[0];
		}
		return new TrustInfo(trusted, persistTrust, trustUnsigned);
	}

	private TreeNode[] createTreeNodes(Certificate[][] certificates) {
		TreeNode[] children = new TreeNode[certificates.length];
		for (int i = 0; i < certificates.length; i++) {
			TreeNode head = new TreeNode(certificates[i][0]);
			TreeNode parent = head;
			children[i] = head;
			for (int j = 0; j < certificates[i].length; j++) {
				TreeNode node = new TreeNode(certificates[i][j]);
				node.setParent(parent);
				parent.setChildren(new TreeNode[] {node});
				parent = node;
			}
		}
		return children;
	}

	@Override
	public AuthenticationInfo getUsernamePassword(final String location, final AuthenticationInfo previousInfo) {
		final AuthenticationInfo[] result = new AuthenticationInfo[1];
		if (!suppressAuthentication() && !isHeadless()) {
			PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
				Shell shell = ProvUI.getDefaultParentShell();
				String message = null;
				if (previousInfo.saveResult())
					message = NLS.bind(ProvUIMessages.ProvUIMessages_SavedNotAccepted_EnterFor_0, location);
				else
					message = NLS.bind(ProvUIMessages.ProvUIMessages_NotAccepted_EnterFor_0, location);

				UserValidationDialog dialog = new UserValidationDialog(previousInfo, shell, ProvUIMessages.ServiceUI_LoginRequired, null, message);
				int dialogCode = dialog.open();
				if (dialogCode == Window.OK) {
					result[0] = dialog.getResult();
				} else if (dialogCode == Window.CANCEL) {
					result[0] = AUTHENTICATION_PROMPT_CANCELED;
				}
			});
		}
		return result[0];
	}

	@Override
	public void showInformationMessage(final String title, final String text, final String linkText) {
		if (isHeadless()) {
			super.showInformationMessage(title, text, linkText);
			return;
		}
		PlatformUI.getWorkbench().getDisplay().syncExec(() -> {
			MessageDialog dialog = new MessageDialogWithLink(ProvUI.getDefaultParentShell(), title, null, text, MessageDialog.INFORMATION, new String[] {IDialogConstants.OK_LABEL}, 0, linkText);
			dialog.open();
		});
	}

	private boolean isHeadless() {
		// If there is no UI available and we are still the IServiceUI,
		// assume that the operation should proceed.  See
		// https://bugs.eclipse.org/bugs/show_bug.cgi?id=291049
		return !PlatformUI.isWorkbenchRunning();
	}

}
