/*******************************************************************************
 * Copyright (c) 2015, 2018 Red Hat Inc. and others.
 * 
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - Initial Contribution
 *******************************************************************************/

package org.eclipse.linuxtools.internal.docker.ui.wizards;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.databinding.Binding;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.IChangeListener;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.swt.ISWTObservableValue;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.fieldassist.ComboContentAdapter;
import org.eclipse.jface.fieldassist.ContentProposal;
import org.eclipse.jface.fieldassist.ContentProposalAdapter;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.fieldassist.IContentProposal;
import org.eclipse.jface.fieldassist.IContentProposalProvider;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.linuxtools.docker.core.IDockerConnection;
import org.eclipse.linuxtools.docker.core.IDockerContainer;
import org.eclipse.linuxtools.docker.core.IDockerContainerInfo;
import org.eclipse.linuxtools.internal.docker.ui.SWTImagesFactory;
import org.eclipse.linuxtools.internal.docker.ui.wizards.ImageRunResourceVolumesVariablesModel.MountType;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

/**
 * @author xcoulon
 *
 */
public class ContainerDataVolumeDialog extends Dialog {

	private final DataVolumeModel model;

	private final DataBindingContext dbc = new DataBindingContext();

	private final List<String> containerNames;

	private final IDockerConnection connection;

	public ContainerDataVolumeDialog(final Shell parentShell,
			final IDockerConnection connection,
			final DataVolumeModel selectedDataVolume) {
		super(parentShell);
		this.connection = connection;
		this.model = new DataVolumeModel(selectedDataVolume);
		this.containerNames = WizardUtils.getContainerNames(connection);
	}

	public ContainerDataVolumeDialog(final Shell parentShell,
			final IDockerConnection connection) {
		super(parentShell);
		this.connection = connection;
		this.model = new DataVolumeModel();
		this.containerNames = WizardUtils.getContainerNames(connection);
	}

	public DataVolumeModel getDataVolume() {
		return model;
	}

	@Override
	protected void configureShell(final Shell shell) {
		super.configureShell(shell);
		setShellStyle(getShellStyle() | SWT.RESIZE);
		shell.setText(
				WizardMessages.getString("ContainerDataVolumeDialog.title")); //$NON-NLS-1$
	}

	/**
	 * Disable the 'OK' button by default
	 */
	@Override
	protected Button createButton(Composite parent, int id, String label,
			boolean defaultButton) {
		final Button button = super.createButton(parent, id, label,
				defaultButton);
		if (id == IDialogConstants.OK_ID) {
			button.setEnabled(false);
		}
		return button;
	}

	@Override
	protected Point getInitialSize() {
		return new Point(450, super.getInitialSize().y);
	}

	@Override
	protected Control createDialogArea(final Composite parent) {
		final Composite container = new Composite(parent, SWT.NONE);
		final int COLUMNS = 3;
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).span(1, 1)
				.grab(true, true).applyTo(container);
		GridLayoutFactory.fillDefaults().margins(10, 10).numColumns(COLUMNS)
				.applyTo(container);

		// Container path
		final Label containerPathLabel = new Label(container, SWT.NONE);
		containerPathLabel.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.containerPathLabel")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(false, false).applyTo(containerPathLabel);
		final Text containerPathText = new Text(container, SWT.BORDER);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(true, false).applyTo(containerPathText);
		final IObservableValue<?> containerPathObservable = BeanProperties
				.value(DataVolumeModel.class, DataVolumeModel.CONTAINER_PATH)
				.observe(model);
		dbc.bindValue(
				WidgetProperties.text(SWT.Modify).observe(containerPathText),
				containerPathObservable);
		// mount type
		final Label explanationLabel = new Label(container, SWT.NONE);
		explanationLabel.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.explanationLabel")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.span(COLUMNS, 1).grab(true, false).applyTo(explanationLabel);
		final int INDENT = 20;
		// No mount
		final Button noMountButton = new Button(container, SWT.RADIO);
		noMountButton.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.noMountButton")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.indent(INDENT, 0).span(COLUMNS, 1).grab(true, false)
				.applyTo(noMountButton);
		bindButton(noMountButton, MountType.NONE);
		// File System mount
		final Button fileSystemMountButton = new Button(container, SWT.RADIO);
		fileSystemMountButton.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.fileSystemMountButton")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.indent(INDENT, 0).span(COLUMNS, 1).grab(true, false)
				.applyTo(fileSystemMountButton);
		final Label hostPathLabel = new Label(container, SWT.NONE);
		hostPathLabel.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.hostPathLabel")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.indent(2 * INDENT, SWT.DEFAULT).grab(false, false)
				.applyTo(hostPathLabel);
		final Text hostPathText = new Text(container, SWT.BORDER);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(true, false).applyTo(hostPathText);
		final IObservableValue<?> hostPathObservable = BeanProperties
				.value(DataVolumeModel.class, DataVolumeModel.HOST_PATH_MOUNT)
				.observe(model);
		dbc.bindValue(WidgetProperties.text(SWT.Modify).observe(hostPathText),
				hostPathObservable);
		// browse for directory
		final Button hostPathDirectoryButton = new Button(container, SWT.NONE);
		hostPathDirectoryButton.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.directoryButton")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(false, false).applyTo(hostPathDirectoryButton);
		hostPathDirectoryButton.addSelectionListener(onHostDirectoryPath());
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(false, false).applyTo(new Label(container, SWT.NONE));
		// optional read-only access
		final Button readOnlyButton = new Button(container, SWT.CHECK);
		readOnlyButton.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.readOnlyButton")); //$NON-NLS-1$
		readOnlyButton.setToolTipText(WizardMessages
				.getString("ContainerDataVolumeDialog.readOnlyButtonTooltip")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.span(COLUMNS - 2, 1).grab(true, false).applyTo(readOnlyButton);
		final ISWTObservableValue<?> readOnlyButtonObservable = WidgetProperties
				.widgetSelection().observe(readOnlyButton);
		dbc.bindValue(readOnlyButtonObservable,
				BeanProperties
						.value(DataVolumeModel.class,
								DataVolumeModel.READ_ONLY_VOLUME)
						.observe(model));
		// browse for file
		final Button hostPathFileButton = new Button(container, SWT.NONE);
		hostPathFileButton.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.fileButton")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(false, false).applyTo(hostPathFileButton);
		hostPathFileButton.addSelectionListener(onHostFilePath());
		bindButton(fileSystemMountButton, MountType.HOST_FILE_SYSTEM,
				hostPathText, hostPathDirectoryButton, hostPathFileButton,
				readOnlyButton);

		// Container mount
		final Button containerMountButton = new Button(container, SWT.RADIO);
		containerMountButton.setText(WizardMessages
				.getString("ContainerDataVolumeDialog.containerMountButton")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.indent(INDENT, 0).span(COLUMNS, 1).grab(true, false)
				.applyTo(containerMountButton);
		final Label containerSelectionLabel = new Label(container, SWT.NONE);
		containerSelectionLabel.setText(WizardMessages.getString(
				"ContainerDataVolumeDialog.containerSelectionLabel")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.indent(2 * INDENT, SWT.DEFAULT)
				.applyTo(containerSelectionLabel);
		final Combo containerSelectionCombo = new Combo(container, SWT.BORDER);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(true, false).span(1, 1).applyTo(containerSelectionCombo);
		new ControlDecoration(containerSelectionCombo, SWT.TOP | SWT.LEFT);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(false, false).applyTo(new Label(container, SWT.NONE));
		bindButton(containerMountButton, MountType.CONTAINER,
				containerSelectionCombo);
		final ComboViewer containerSelectionComboViewer = new ComboViewer(
				containerSelectionCombo);
		containerSelectionComboViewer
				.setContentProvider(new ArrayContentProvider());
		containerSelectionComboViewer.setInput(this.containerNames);
		final IObservableValue<?> selectedContainerObservable = BeanProperties
				.value(DataVolumeModel.class, DataVolumeModel.CONTAINER_MOUNT)
				.observe(model);
		dbc.bindValue(
				WidgetProperties.widgetSelection().observe(containerSelectionCombo),
				selectedContainerObservable);
		new ContentProposalAdapter(containerSelectionCombo,
				new ComboContentAdapter() {
					@Override
					public void insertControlContents(Control control,
							String text, int cursorPosition) {
						final Combo combo = (Combo) control;
						final Point selection = combo.getSelection();
						combo.setText(text);
						selection.x = text.length();
						selection.y = selection.x;
						combo.setSelection(selection);
					}
				}, getContainerNameContentProposalProvider(
						containerSelectionCombo),
				null, null);

		// error message
		final Composite errorContainer = new Composite(container, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
				.span(COLUMNS, 1).grab(true, true).applyTo(errorContainer);
		GridLayoutFactory.fillDefaults().margins(6, 6).numColumns(2)
				.applyTo(errorContainer);

		final Label errorMessageIcon = new Label(errorContainer, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.hint(20, SWT.DEFAULT)
				.applyTo(errorMessageIcon);
		final Label errorMessageLabel = new Label(errorContainer, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(true, false)
				.applyTo(errorMessageLabel);
		setupValidationSupport(errorMessageIcon, errorMessageLabel);
		return container;
	}

	private void setupValidationSupport(final Label errorMessageIcon,
			final Label errorMessageLabel) {
		for (Binding binding : dbc.getBindings()) {
			binding.getModel().addChangeListener(onDataVolumeSettingsChanged(errorMessageIcon, errorMessageLabel));
		}
	}

	/**
	 * Binds the given {@link MountType} to the given {@link Button} when it is
	 * selected, and set the enablement of the associated {@link Control} at the
	 * same time (ie: the {@link Control} are only enabled when the given
	 * {@link Button} is selected.
	 * 
	 * @param button
	 *            the {@link Button} to bind
	 * @param mountType
	 *            the {@link MountType} to bind to the {@link Button}
	 * @param controls
	 *            the {@link Control}s to enable or disable when the Button is
	 *            selected/unselected.
	 * @return
	 */
	private Binding bindButton(final Button button, final MountType mountType,
			final Control... controls) {
		return dbc.bindValue(WidgetProperties.widgetSelection().observe(button),
				BeanProperties.value(DataVolumeModel.class,
						DataVolumeModel.MOUNT_TYPE).observe(model),
				new UpdateValueStrategy() {
					@Override
					public Object convert(Object value) {
						if (value.equals(Boolean.TRUE)) {
							setEnabled(controls, true);
							return mountType;
						}
						setEnabled(controls, false);
						return null;
					}

					private void setEnabled(final Control[] controls,
							final boolean enabled) {
						for (Control control : controls) {
							control.setEnabled(enabled);
						}
					}
				}, new UpdateValueStrategy() {
					@Override
					public Object convert(final Object value) {
						if (mountType.equals(value)) {
							button.setEnabled(true);
						}
						return mountType.equals(value);
					}
				});
	}

	private SelectionListener onHostDirectoryPath() {
		return SelectionListener.widgetSelectedAdapter(e -> {
			final DirectoryDialog directoryDialog = new DirectoryDialog(
					getShell());
			final String selectedPath = directoryDialog.open();
			if (selectedPath != null) {
				model.setHostPathMount(selectedPath);
			}
		});
	}

	private SelectionListener onHostFilePath() {
		return SelectionListener.widgetSelectedAdapter(e -> {
			final FileDialog fileDialog = new FileDialog(getShell());
			final String selectedPath = fileDialog.open();
			if (selectedPath != null) {
				model.setHostPathMount(selectedPath);
			}
		});
	}

	/**
	 * Creates an {@link IContentProposalProvider} to propose
	 * {@link IDockerContainer} names based on the current text.
	 * 
	 * @param items
	 * @return
	 */
	private IContentProposalProvider getContainerNameContentProposalProvider(
			final Combo containerSelectionCombo) {
		return (contents, position) -> {
			final List<IContentProposal> proposals = new ArrayList<>();
			for (String containerName : containerSelectionCombo.getItems()) {
				if (containerName.contains(contents)) {
					proposals.add(new ContentProposal(containerName,
							containerName, containerName, position));
				}
			}
			return proposals.toArray(new IContentProposal[0]);
		};
	}

	private IChangeListener onDataVolumeSettingsChanged(
			final Label errorMessageIcon, final Label errorMessageLabel) {

		return event -> {
			// skip if dialog has been closed
			if (Display.getCurrent() == null || getShell().isDisposed()) {
				return;
			}
			final IStatus status = validateInput();
			Display.getCurrent().syncExec(() -> {
				if (status.isOK()) {
					errorMessageIcon.setVisible(false);
					errorMessageLabel.setVisible(false);
					setOkButtonEnabled(true);
				} else if (status.matches(IStatus.WARNING)) {
					errorMessageIcon.setVisible(true);
					errorMessageIcon.setImage(
							SWTImagesFactory.DESC_WARNING.createImage());
					errorMessageLabel.setVisible(true);
					errorMessageLabel.setText(status.getMessage());
					setOkButtonEnabled(true);
				} else if (status.matches(IStatus.ERROR)) {
					if (status.getMessage() != null
							&& !status.getMessage().isEmpty()) {
						errorMessageIcon.setVisible(true);
						errorMessageIcon.setImage(
								SWTImagesFactory.DESC_ERROR.createImage());
						errorMessageLabel.setVisible(true);
						errorMessageLabel.setText(status.getMessage());
					}
					setOkButtonEnabled(false);
				}
			});
		};
	}

	private IStatus validateInput() {
		final String containerPath = model.getContainerPath();
		final MountType mountType = model.getMountType();
		final String hostPath = model.getHostPathMount();
		if (containerPath == null || containerPath.isEmpty()) {
			return ValidationStatus.error(null);
		} else if (mountType == null) {
			return ValidationStatus.error(null);
		} else if (mountType == MountType.HOST_FILE_SYSTEM
				&& (hostPath == null || hostPath.isEmpty())) {
			return ValidationStatus.error(null);
		} else if (mountType == MountType.HOST_FILE_SYSTEM
				&& !new File(hostPath).exists()) {
			return ValidationStatus
					.warning("The specified path does not exist on the host."); //$NON-NLS-1$
		} else if (mountType == MountType.CONTAINER) {
			final IDockerContainer container = WizardUtils
					.getContainer(connection, model.getContainerMount());
			if (container == null) {
				// just make sure that the dialog cannot complete
				return ValidationStatus.error(null);
			}
			final IDockerContainerInfo selectedContainerInfo = container.info();
			if (selectedContainerInfo != null
					&& selectedContainerInfo.volumes() != null
					&& !selectedContainerInfo.volumes()
					.containsKey(model.getContainerPath())) {
				return ValidationStatus
						.warning(WizardMessages.getFormattedString(
								"ContainerDataVolumeDialog.volumeWarning", //$NON-NLS-1$
								model.getContainerPath()));
			}
		}
		return ValidationStatus.ok();
	}

	private void setOkButtonEnabled(final boolean enabled) {
		final Button okButton = getButton(IDialogConstants.OK_ID);
		// skip if 'OK' button does not exist yet.
		if (okButton != null) {
			okButton.setEnabled(enabled);
		}
	}

}
