/*******************************************************************************
 * Copyright (c) 2021 Red Hat Inc.
 *
 * 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
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.sdk;

import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.cert.*;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.function.Function;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.artifact.processors.pgp.PGPPublicKeyStore;
import org.eclipse.equinox.internal.p2.engine.phases.CertificateChecker;
import org.eclipse.equinox.internal.p2.ui.dialogs.PGPPublicKeyViewDialog;
import org.eclipse.equinox.internal.p2.ui.viewers.CertificateLabelProvider;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
import org.eclipse.equinox.p2.repository.spi.PGPPublicKeyService;
import org.eclipse.jface.dialogs.*;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.preference.PreferencePage;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Policy;
import org.eclipse.jface.viewers.*;
import org.eclipse.jface.widgets.LabelFactory;
import org.eclipse.jface.widgets.WidgetFactory;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPreferencePage;
import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;

public class TrustPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {

	private static final String EXPORT_FILTER_PATH = "exportFilterPath"; //$NON-NLS-1$
	private static final String ADD_FILTER_PATH = "addFilterPath"; //$NON-NLS-1$

	private CertificateChecker certificateChecker;
	private Set<Certificate> trustedCertificates;
	private PGPPublicKeyStore trustedKeys;
	private Map<PGPPublicKey, Set<Bundle>> contributedTrustedKeys;
	private boolean dirty;
	private TableViewer viewer;

	public TrustPreferencePage() {
		super(ProvSDKMessages.TrustPreferencePage_title);
	}

	@Override
	public void init(IWorkbench workbench) {
		// nothing to do
	}

	@Override
	protected Control createContents(Composite parent) {
		IProvisioningAgent provisioningAgent = ProvSDKUIActivator.getDefault().getProvisioningAgent();
		certificateChecker = new CertificateChecker(provisioningAgent);
		certificateChecker
				.setProfile(provisioningAgent.getService(IProfileRegistry.class).getProfile(IProfileRegistry.SELF));
		trustedCertificates = new LinkedHashSet<>(certificateChecker.getPreferenceTrustedCertificates());
		contributedTrustedKeys = certificateChecker.getContributedTrustedKeys();
		trustedKeys = certificateChecker.getPreferenceTrustedKeys();

		Composite res = new Composite(parent, SWT.NONE);
		res.setLayout(new GridLayout(2, false));

		// Ensure that the message supports wrapping for a long text message.
		GridData data = new GridData(SWT.FILL, SWT.DEFAULT, true, false, 2, 1);
		data.widthHint = convertWidthInCharsToPixels(90);
		LabelFactory factory = WidgetFactory.label(SWT.WRAP).text(ProvSDKMessages.TrustPreferencePage_pgpIntro)
				.font(parent.getFont()).layoutData(data);
		factory.create(res);

		TableColumnLayout tableColumnLayout = new TableColumnLayout();
		Composite tableComposite = WidgetFactory.composite(SWT.NONE)
				.layoutData(new GridData(SWT.FILL, SWT.FILL, true, true)).layout(tableColumnLayout).create(res);
		Table keyTable = WidgetFactory.table(SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION)
				.headerVisible(true).linesVisible(true).font(parent.getFont()).create(tableComposite);
		viewer = new TableViewer(keyTable);
		keyTable.setHeaderVisible(true);
		viewer.setContentProvider(new ArrayContentProvider());

		// This column is packed later.
		TableViewerColumn typeColumn = createColumn(viewer, ProvSDKMessages.TrustPreferencePage_TypeColumn,
				key -> "PGP", cert -> "x509", tableColumnLayout, 1); //$NON-NLS-1$ //$NON-NLS-2$

		createColumn(viewer, ProvSDKMessages.TrustPreferencePage_FingerprintIdColumn,
				key -> PGPPublicKeyService.toHexFingerprint(key),
				cert -> cert.getSerialNumber().toString(), tableColumnLayout, 10);

		createColumn(viewer, ProvSDKMessages.TrustPreferencePage_NameColumn, key -> {
			List<String> userIds = new ArrayList<>();
			key.getUserIDs().forEachRemaining(userIds::add);
			return String.join(", ", userIds); //$NON-NLS-1$
		}, cert -> CertificateLabelProvider.getText(cert), tableColumnLayout, 15);

		createColumn(viewer, ProvSDKMessages.TrustPreferencePage_Contributor, key -> {
			{
				Set<String> contributors = new LinkedHashSet<>();
				if (trustedKeys.all().contains(key)) {
					contributors.add(ProvSDKMessages.TrustPreferencePage_PreferenceContributor);
				}
				Set<Bundle> bundles = contributedTrustedKeys.get(key);
				if (bundles != null) {
					Set<String> bundleContributors = new TreeSet<>(Policy.getComparator());
					bundles.stream().map(bundle -> getBundleName(bundle)).forEach(bundleContributors::add);
					contributors.addAll(bundleContributors);
				}
				return String.join(", ", contributors); //$NON-NLS-1$
			}
		}, cert -> ProvSDKMessages.TrustPreferencePage_PreferenceContributor, tableColumnLayout,
				contributedTrustedKeys.isEmpty() ? 8 : 15);

		createColumn(viewer, ProvSDKMessages.TrustPreferencePage_ValidityColumn, pgp -> {
			if (pgp.getCreationTime().after(Date.from(Instant.now()))) {
				return NLS.bind(ProvSDKMessages.TrustPreferencePage_DateNotYetValid, pgp.getCreationTime());
			}
			long validSeconds = pgp.getValidSeconds();
			if (validSeconds == 0) {
				return ProvSDKMessages.TrustPreferencePage_DateValid;
			}
			Instant expires = pgp.getCreationTime().toInstant().plus(validSeconds, ChronoUnit.SECONDS);
			return expires.isBefore(Instant.now())
					? NLS.bind(ProvSDKMessages.TrustPreferencePage_DateExpiredSince, expires)
					: NLS.bind(ProvSDKMessages.TrustPreferencePage_DataValidExpires, expires);
		}, x509 -> {
			try {
				x509.checkValidity();
				return ProvSDKMessages.TrustPreferencePage_DateValid;
			} catch (CertificateExpiredException expired) {
				return ProvSDKMessages.TrustPreferencePage_DateExpired;
			} catch (CertificateNotYetValidException notYetValid) {
				return ProvSDKMessages.TrustPreferencePage_DateNotYetvalid;
			}
		}, tableColumnLayout, 8);

		updateInput();

		Composite buttonComposite = createVerticalButtonBar(res);
		buttonComposite.setLayoutData(new GridData(SWT.DEFAULT, SWT.BEGINNING, false, false));

		Button exportButton = new Button(buttonComposite, SWT.PUSH);
		exportButton.setText(ProvSDKMessages.TrustPreferencePage_export);
		setVerticalButtonLayoutData(exportButton);
		exportButton.setEnabled(false);

		exportButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			Object element = viewer.getStructuredSelection().getFirstElement();
			FileDialog dialog = new FileDialog(getShell(), SWT.SAVE);
			dialog.setFilterPath(getFilterPath(EXPORT_FILTER_PATH));
			dialog.setText(ProvSDKMessages.TrustPreferencePage_fileExportTitle);
			dialog.setFilterExtensions(new String[] { "*.asc" }); //$NON-NLS-1$
			FileDialog destination = new FileDialog(exportButton.getShell(), SWT.SAVE);
			destination.setFilterPath(getFilterPath(EXPORT_FILTER_PATH));
			destination.setText(ProvSDKMessages.TrustPreferencePage_Export);
			if (element instanceof X509Certificate) {
				X509Certificate cert = (X509Certificate) element;
				destination.setFilterExtensions(new String[] { "*.der" }); //$NON-NLS-1$
				destination.setFileName(cert.getSerialNumber().toString() + ".der"); //$NON-NLS-1$
				String path = destination.open();
				setFilterPath(EXPORT_FILTER_PATH, destination.getFilterPath());
				if (path == null) {
					return;
				}
				File destinationFile = new File(path);
				try (FileOutputStream output = new FileOutputStream(destinationFile)) {
					output.write(cert.getEncoded());
				} catch (IOException | CertificateEncodingException ex) {
					ProvSDKUIActivator.getDefault().getLog()
							.log(new Status(IStatus.ERROR, ProvSDKUIActivator.PLUGIN_ID, ex.getMessage(), ex));
				}
			} else {
				PGPPublicKey key = (PGPPublicKey) element;
				destination.setFilterExtensions(new String[] { "*.asc" }); //$NON-NLS-1$
				destination.setFileName(PGPPublicKeyService.toHexFingerprint(key) + ".asc"); //$NON-NLS-1$
				String path = destination.open();
				setFilterPath(EXPORT_FILTER_PATH, destination.getFilterPath());
				if (path == null) {
					return;
				}
				File destinationFile = new File(path);
				try (OutputStream output = new ArmoredOutputStream(new FileOutputStream(destinationFile))) {
					key.encode(output);
				} catch (IOException ex) {
					ProvSDKUIActivator.getDefault().getLog()
							.log(new Status(IStatus.ERROR, ProvSDKUIActivator.PLUGIN_ID, ex.getMessage(), ex));
				}
			}
		}));

		Button addButton = new Button(buttonComposite, SWT.PUSH);
		addButton.setText(ProvSDKMessages.TrustPreferencePage_addPGPKeyButtonLabel);
		addButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			FileDialog dialog = new FileDialog(getShell(), SWT.OPEN);
			dialog.setFilterPath(getFilterPath(ADD_FILTER_PATH));
			dialog.setText(ProvSDKMessages.TrustPreferencePage_fileImportTitle);
			dialog.setFilterExtensions(new String[] { "*.asc;*.der" }); //$NON-NLS-1$
			String path = dialog.open();
			setFilterPath(ADD_FILTER_PATH, dialog.getFilterPath());
			if (path == null) {
				return;
			}

			if (path.endsWith(".der")) { //$NON-NLS-1$
				try {
					CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); //$NON-NLS-1$
					try (InputStream input = Files.newInputStream(Paths.get(path))) {
						Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(input);
						trustedCertificates.addAll(certificates);
						updateInput();
						viewer.setSelection(new StructuredSelection(certificates.toArray()), true);
					}
				} catch (IOException | CertificateException ex) {
					ProvSDKUIActivator.getDefault().getLog()
							.log(new Status(IStatus.ERROR, ProvSDKUIActivator.PLUGIN_ID, ex.getMessage(), ex));
				}
			} else {
				HashSet<Object> oldKeys = new HashSet<>(trustedKeys.all());
				trustedKeys.add(new File(path));

				HashSet<Object> newKeys = new HashSet<>(trustedKeys.all());
				newKeys.removeAll(oldKeys);
				updateInput();
				viewer.setSelection(new StructuredSelection(newKeys.toArray()), true);
			}
			dirty = true;
		}));
		setVerticalButtonLayoutData(addButton);

		Button removeButton = new Button(buttonComposite, SWT.PUSH);
		removeButton.setText(ProvSDKMessages.TrustPreferencePage_removePGPKeyButtonLabel);
		removeButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			for (Object key : getSelectedKeys()) {
				if (key instanceof PGPPublicKey) {
					trustedKeys.remove((PGPPublicKey) key);
				} else {
					trustedCertificates.remove(key);
				}
			}
			updateInput();
			dirty = true;
		}));
		removeButton.setEnabled(false);
		setVerticalButtonLayoutData(removeButton);

		Runnable details = () -> {
			Object element = viewer.getStructuredSelection().getFirstElement();
			if (element instanceof X509Certificate) {
				// create and open dialog for certificate chain
				CertificateLabelProvider.openDialog(getShell(), (X509Certificate) element);
			} else {
				new PGPPublicKeyViewDialog(getShell(), (PGPPublicKey) element,
						provisioningAgent.getService(PGPPublicKeyService.class)).open();
			}
		};

		Button detailsButton = new Button(buttonComposite, SWT.PUSH);
		detailsButton.setText(ProvSDKMessages.TrustPreferencePage_Details);
		detailsButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> details.run()));
		detailsButton.setEnabled(false);
		setVerticalButtonLayoutData(detailsButton);

		viewer.addPostSelectionChangedListener(e -> {
			List<Object> selectedKeys = getSelectedKeys();
			exportButton.setEnabled(selectedKeys.size() == 1);
			Collection<PGPPublicKey> keys = trustedKeys.all();
			removeButton.setEnabled(
					selectedKeys.stream().anyMatch(o -> keys.contains(o) || trustedCertificates.contains(o)));
			detailsButton.setEnabled(selectedKeys.size() == 1);
		});

		Button trustAllButton = WidgetFactory.button(SWT.CHECK).text(ProvSDKMessages.TrustPreferencePage_TrustAll)
				.font(JFaceResources.getDialogFont()).create(res);
		setButtonLayoutData(trustAllButton).verticalSpan = 2;
		trustAllButton.setSelection(certificateChecker.isTrustAlways());
		trustAllButton.addSelectionListener(widgetSelectedAdapter(e -> {
			if (trustAllButton.getSelection()) {
				// Prompt the user to ensure they really understand what they've chosen, the
				// risk, and where the preference is stored if they wish to change it in the
				// future. Also ensure that the default button is no so that they must
				// explicitly click the yes button, not just hit enter.
				MessageDialog messageDialog = new MessageDialog(getShell(),
						ProvSDKMessages.TrustPreferencePage_TrustAllConfirmationTitle, null,
						ProvSDKMessages.TrustPreferencePage_TrustAllConfirmationDescription, MessageDialog.QUESTION,
						new String[] { ProvSDKMessages.TrustPreferencePage_TrustAllYes,
								ProvSDKMessages.TrustPreferencePage_TrustAllNo },
						1) {
					@Override
					public Image getImage() {
						return getWarningImage();
					}
				};
				int result = messageDialog.open();
				if (result != Window.OK) {
					certificateChecker.setTrustAlways(false);
					// Restore the setting.
					trustAllButton.setSelection(false);
				} else {
					certificateChecker.setTrustAlways(true);
				}
			} else {
				certificateChecker.setTrustAlways(false);
			}
		}));

		viewer.addDoubleClickListener(e -> details.run());

		typeColumn.getColumn().pack();

		createMenu();

		return res;
	}

	private void createMenu() {
		Control control = viewer.getControl();
		Menu menu = new Menu(control);
		control.setMenu(menu);
		MenuItem item = new MenuItem(menu, SWT.PUSH);
		item.setText(ProvSDKMessages.TrustPreferencePage_CopyFingerprint);
		item.addSelectionListener(widgetSelectedAdapter(e -> {
			Object element = viewer.getStructuredSelection().getFirstElement();
			if (element instanceof PGPPublicKey) {
				Clipboard clipboard = new Clipboard(getShell().getDisplay());
				clipboard.setContents(new Object[] {
						PGPPublicKeyService.toHexFingerprint((PGPPublicKey) element) },
						new Transfer[] { TextTransfer.getInstance() });
				clipboard.dispose();
			}
		}));

		viewer.addSelectionChangedListener(
				e -> item.setEnabled(viewer.getStructuredSelection().getFirstElement() instanceof PGPPublicKey));
	}

	private TableViewerColumn createColumn(TableViewer tableViewer, String text, Function<PGPPublicKey, String> pgpMap,
			Function<X509Certificate, String> x509Map, TableColumnLayout tableColumnLayout, int columnWeight) {
		TableViewerColumn column = new TableViewerColumn(tableViewer, SWT.NONE);
		column.getColumn().setText(text);
		column.setLabelProvider(new PGPOrX509ColumnLabelProvider(pgpMap, x509Map));

		tableColumnLayout.setColumnData(column.getColumn(), new ColumnWeightData(columnWeight));
		return column;
	}

	private void updateInput() {
		Set<PGPPublicKey> input = new TreeSet<>((k1, k2) -> {
			return Arrays.compare(k1.getFingerprint(), k2.getFingerprint());
		});
		input = new LinkedHashSet<>();
		Collection<PGPPublicKey> all = trustedKeys.all();
		input = new TreeSet<>((k1, k2) -> {
			boolean contains1 = all.contains(k1);
			boolean contains2 = all.contains(k2);
			if (contains1 != contains2) {
				if (contains1) {
					return -1;
				}
				return 1;
			}
			return PGPPublicKeyService.toHexFingerprint(k1).compareTo(PGPPublicKeyService.toHexFingerprint(k2));
		});
		input.addAll(all);
		input.addAll(contributedTrustedKeys.keySet());

		LinkedHashSet<Object> allInput = new LinkedHashSet<>();
		allInput.addAll(trustedCertificates);
		allInput.addAll(input);
		viewer.setInput(allInput);
	}

	@SuppressWarnings("unchecked")
	private List<Object> getSelectedKeys() {
		return viewer.getStructuredSelection().toList();
	}

	private Composite createVerticalButtonBar(Composite parent) {
		// Create composite.
		Composite composite = new Composite(parent, SWT.NONE);
		initializeDialogUnits(composite);

		// create a layout with spacing and margins appropriate for the font
		// size.
		GridLayout layout = new GridLayout();
		layout.numColumns = 1;
		layout.marginWidth = 5;
		layout.marginHeight = 0;
		layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING);
		layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
		composite.setLayout(layout);

		return composite;
	}

	private GridData setVerticalButtonLayoutData(Button button) {
		GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
		int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH);
		Point minSize = button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
		data.widthHint = Math.max(widthHint, minSize.x);
		button.setLayoutData(data);
		return data;
	}

	private String getFilterPath(String key) {
		IDialogSettings dialogSettings = DialogSettings
				.getOrCreateSection(ProvSDKUIActivator.getDefault().getDialogSettings(), getClass().getName());
		String filterPath = dialogSettings.get(key);
		if (filterPath == null) {
			filterPath = System.getProperty("user.home"); //$NON-NLS-1$
		}
		return filterPath;
	}

	private void setFilterPath(String key, String filterPath) {
		if (filterPath != null) {
			IDialogSettings dialogSettings = DialogSettings
					.getOrCreateSection(ProvSDKUIActivator.getDefault().getDialogSettings(), getClass().getName());
			dialogSettings.put(key, filterPath);
		}
	}

	private String getBundleName(Bundle bundle) {
		String value = bundle.getHeaders().get(Constants.BUNDLE_NAME);
		return value == null ? bundle.getSymbolicName() : Platform.getResourceString(bundle, value);
	}

	@Override
	protected void performDefaults() {
		trustedCertificates = new LinkedHashSet<>(certificateChecker.getPreferenceTrustedCertificates());
		trustedKeys = certificateChecker.getPreferenceTrustedKeys();
		updateInput();
		super.performDefaults();
	}

	@Override
	public boolean performOk() {
		if (dirty) {
			IStatus persistTrustedCertificates = certificateChecker.persistTrustedCertificates(trustedCertificates);
			IStatus persistTrustedKeys = certificateChecker.persistTrustedKeys(trustedKeys);
			dirty = false;
			return persistTrustedKeys.isOK() && persistTrustedCertificates.isOK();
		}
		return true;
	}

	private static class PGPOrX509ColumnLabelProvider extends ColumnLabelProvider {
		private Function<PGPPublicKey, String> pgpMap;
		private Function<X509Certificate, String> x509map;

		public PGPOrX509ColumnLabelProvider(Function<PGPPublicKey, String> pgpMap,
				Function<X509Certificate, String> x509map) {
			this.pgpMap = pgpMap;
			this.x509map = x509map;
		}

		@Override
		public String getText(Object element) {
			if (element instanceof PGPPublicKey) {
				return pgpMap.apply((PGPPublicKey) element);
			}
			if (element instanceof X509Certificate) {
				return x509map.apply((X509Certificate) element);
			}
			return super.getText(element);
		}
	}

}
