/*******************************************************************************
 * Copyright (c) 2015, 2020 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.launch;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.observable.value.IValueChangeListener;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
import org.eclipse.jface.databinding.swt.typed.WidgetProperties;
import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
import org.eclipse.jface.databinding.viewers.ViewerSupport;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.linuxtools.docker.ui.Activator;
import org.eclipse.linuxtools.internal.docker.ui.SWTImagesFactory;
import org.eclipse.linuxtools.internal.docker.ui.wizards.ContainerPortDialog;
import org.eclipse.linuxtools.internal.docker.ui.wizards.ImageRunSelectionModel;
import org.eclipse.linuxtools.internal.docker.ui.wizards.ImageRunSelectionModel.ExposedPortModel;
import org.eclipse.linuxtools.internal.docker.ui.wizards.WizardMessages;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

public class RunImagePortsTab extends AbstractLaunchConfigurationTab
{

	private static final String TAB_NAME = "RunPortsTab.name"; //$NON-NLS-1$

	private static final int INDENT = 30;

	private static final int COLUMNS = 3;

	private final DataBindingContext dbc = new DataBindingContext();

	private final ImageRunSelectionModel model;

	public RunImagePortsTab(ImageRunSelectionModel model) {
		this.model = model;
	}

	@Override
	public void createControl(Composite parent) {
		final Composite container = new Composite(parent, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL).span(1, 1)
				.grab(true, false).applyTo(container);
		GridLayoutFactory.fillDefaults().numColumns(COLUMNS).margins(6, 6)
				.applyTo(container);
		if (model == null) {
			setErrorMessage(LaunchMessages.getString("NoConnectionError.msg"));
		} else {
			setErrorMessage(null);
			createPortSettingsSection(container);
		}
		setControl(container);
	}

	private void createPortSettingsSection(final Composite container) {
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(false, false).span(3, 1)
				.applyTo(new Label(container, SWT.NONE));
		final Button publishAllPortsButton = new Button(container, SWT.CHECK);
		publishAllPortsButton.setText(WizardMessages
				.getString("ImageRunSelectionPage.publishAllPorts")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.span(COLUMNS, 1).grab(true, false)
				.applyTo(publishAllPortsButton);
		dbc.bindValue(
				WidgetProperties.widgetSelection().observe(publishAllPortsButton),
				BeanProperties
						.value(ImageRunSelectionModel.class,
								ImageRunSelectionModel.PUBLISH_ALL_PORTS)
						.observe(model));
		// specify ports
		final Label portSettingsLabel = new Label(container, SWT.NONE);
		portSettingsLabel.setText(
				WizardMessages.getString("ImageRunSelectionPage.portSettings")); //$NON-NLS-1$
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER)
				.grab(true, false).span(COLUMNS, 1).indent(INDENT, 0)
				.applyTo(portSettingsLabel);
		final CheckboxTableViewer exposedPortsTableViewer = createPortSettingsTable(
				container);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL)
				.grab(true, true).span(COLUMNS - 1, 1).indent(INDENT, 0)
				.hint(200, 70).applyTo(exposedPortsTableViewer.getTable());
		// buttons
		final Composite buttonsContainers = new Composite(container, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP)
				.grab(false, false).applyTo(buttonsContainers);
		GridLayoutFactory.fillDefaults().numColumns(1).margins(0, 0)
				.spacing(SWT.DEFAULT, 0).applyTo(buttonsContainers);

		final Button addButton = new Button(buttonsContainers, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP)
				.grab(true, false).applyTo(addButton);
		addButton
				.setText(WizardMessages.getString("ImageRunSelectionPage.add")); //$NON-NLS-1$
		addButton.addSelectionListener(onAddPort(exposedPortsTableViewer));
		final Button editButton = new Button(buttonsContainers, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP)
				.grab(true, false).applyTo(editButton);
		editButton.setText(
				WizardMessages.getString("ImageRunSelectionPage.editButton")); //$NON-NLS-1$
		editButton.setEnabled(false);
		editButton.addSelectionListener(onEditPort(exposedPortsTableViewer));
		final Button removeButton = new Button(buttonsContainers, SWT.NONE);
		GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP)
				.grab(true, false).applyTo(removeButton);
		removeButton.setText(
				WizardMessages.getString("ImageRunSelectionPage.remove")); //$NON-NLS-1$
		removeButton
				.addSelectionListener(onRemovePorts(exposedPortsTableViewer));
		BeanProperties
				.value(ImageRunSelectionModel.class,
						ImageRunSelectionModel.PUBLISH_ALL_PORTS)
				.observe(model)
				.addValueChangeListener(onPublishAllPortsChange(
						exposedPortsTableViewer.getTable(), addButton,
						removeButton));
		ViewerSupport.bind(exposedPortsTableViewer, model.getExposedPorts(),
				BeanProperties.values(ExposedPortModel.class,
						ExposedPortModel.CONTAINER_PORT,
						ExposedPortModel.PORT_TYPE,
						ExposedPortModel.HOST_ADDRESS,
						ExposedPortModel.HOST_PORT));
		dbc.bindSet(
				ViewersObservables.observeCheckedElements(
						exposedPortsTableViewer, ExposedPortModel.class),
				BeanProperties.set(ImageRunSelectionModel.SELECTED_PORTS)
						.observe(model));

		// disable the edit and removeButton if the table is empty
		exposedPortsTableViewer.addSelectionChangedListener(
				onSelectionChanged(editButton, removeButton));
		exposedPortsTableViewer.addCheckStateListener(onCheckStateChanged());

		togglePortMappingControls(exposedPortsTableViewer.getTable(), addButton,
				removeButton);
	}

	private static void setControlsEnabled(final Control[] controls,
			final boolean enabled) {
		for (Control control : controls) {
			control.setEnabled(enabled);
		}
	}

	private ISelectionChangedListener onSelectionChanged(
			final Button... targetButtons) {
		return e -> {
			if (e.getSelection().isEmpty()) {
				setControlsEnabled(targetButtons, false);
			} else {
				setControlsEnabled(targetButtons, true);
			}
		};
	}

	private ICheckStateListener onCheckStateChanged() {
		return e -> {
			ImageRunSelectionModel.ExposedPortModel element = (ImageRunSelectionModel.ExposedPortModel) e
					.getElement();
			if (e.getChecked()) {
				model.getSelectedPorts().add(element);
				element.setSelected(true);
			} else {
				model.getSelectedPorts().remove(element);
				element.setSelected(false);
			}
			updateLaunchConfigurationDialog();
		};
	}

	private CheckboxTableViewer createPortSettingsTable(
			final Composite container) {
		final Table table = new Table(container, SWT.BORDER | SWT.FULL_SELECTION
				| SWT.V_SCROLL | SWT.H_SCROLL | SWT.CHECK);
		final CheckboxTableViewer tableViewer = new CheckboxTableViewer(table);
		table.setHeaderVisible(true);
		table.setLinesVisible(true);
		createTableViewerColumn(tableViewer,
				WizardMessages
						.getString("ImageRunSelectionPage.containerPortColumn"), //$NON-NLS-1$
				100);
		createTableViewerColumn(tableViewer,
				WizardMessages.getString("ImageRunSelectionPage.typeColumn"), //$NON-NLS-1$
				50);
		createTableViewerColumn(tableViewer,
				WizardMessages
						.getString("ImageRunSelectionPage.hostAddressColumn"), //$NON-NLS-1$
				100);
		createTableViewerColumn(tableViewer,
				WizardMessages
						.getString("ImageRunSelectionPage.hostPortColumn"), //$NON-NLS-1$
				100);
		tableViewer.setContentProvider(new ObservableListContentProvider<>());
		return tableViewer;
	}

	private TableViewerColumn createTableViewerColumn(
			final TableViewer tableViewer, final String title,
			final int width) {
		final TableViewerColumn viewerColumn = new TableViewerColumn(
				tableViewer, SWT.NONE);
		final TableColumn column = viewerColumn.getColumn();
		if (title != null) {
			column.setText(title);
		}
		column.setWidth(width);
		return viewerColumn;
	}

	private IValueChangeListener onPublishAllPortsChange(
			final Control... controls) {
		return event -> togglePortMappingControls(controls);
	}

	private SelectionListener onAddPort(
			final CheckboxTableViewer exposedPortsTableViewer) {
		return SelectionListener.widgetSelectedAdapter(e -> {
			final ContainerPortDialog dialog = new ContainerPortDialog(
					getShell());
			dialog.create();
			if (dialog.open() == IDialogConstants.OK_ID) {
				final ExposedPortModel port = dialog.getPort();
				model.addAvailablePort(port);
				model.getSelectedPorts().add(port);
				port.setSelected(true);
				exposedPortsTableViewer.setChecked(port, true);
				updateLaunchConfigurationDialog();
			}
		});
	}

	private SelectionListener onEditPort(
			final CheckboxTableViewer exposedPortsTableViewer) {
		return SelectionListener.widgetSelectedAdapter(e -> {
			final IStructuredSelection selection = exposedPortsTableViewer
					.getStructuredSelection();
			final ExposedPortModel selectedContainerPort = (ExposedPortModel) selection
					.getFirstElement();
			final ContainerPortDialog dialog = new ContainerPortDialog(
					getShell(), selectedContainerPort);
			dialog.create();
			if (dialog.open() == IDialogConstants.OK_ID) {
				final ExposedPortModel configuredPort = dialog.getPort();
				selectedContainerPort
						.setContainerPort(configuredPort.getContainerPort());
				selectedContainerPort
						.setHostAddress(configuredPort.getHostAddress());
				selectedContainerPort.setHostPort(configuredPort.getHostPort());
			}
			updateLaunchConfigurationDialog();
		});
	}

	private SelectionListener onRemovePorts(
			final TableViewer portsTableViewer) {
		return SelectionListener.widgetSelectedAdapter(e -> {
			final IStructuredSelection selection = portsTableViewer
					.getStructuredSelection();
			for (@SuppressWarnings("unchecked")
			Iterator<ExposedPortModel> iterator = selection.iterator(); iterator
					.hasNext();) {
				final ExposedPortModel port = iterator.next();
				model.removeAvailablePort(port);
				model.getSelectedPorts().remove(port);
			}
			updateLaunchConfigurationDialog();
		});
	}

	private void togglePortMappingControls(final Control... controls) {
		for (Control control : controls) {
			if (model.isPublishAllPorts()) {
				control.setEnabled(false);
			} else {
				control.setEnabled(true);
			}
			updateLaunchConfigurationDialog();
		}
	}

	@Override
	public Image getImage() {
		return SWTImagesFactory.get(SWTImagesFactory.IMG_CONTAINER_PORT);
	}

	@Override
	public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {

	}

	@Override
	public void initializeFrom(ILaunchConfiguration configuration) {
		if (model == null)
			return;
		try {
			// recycle the model
			model.removeExposedPorts();
			final boolean publishAllPorts = configuration.getAttribute(
					IRunDockerImageLaunchConfigurationConstants.PUBLISH_ALL_PORTS,
					true);
			model.setPublishAllPorts(publishAllPorts);
			final List<String> publishedPorts = configuration.getAttribute(
					IRunDockerImageLaunchConfigurationConstants.PUBLISHED_PORTS,
					new ArrayList<String>());
			final List<String> unusedPorts = configuration
					.getAttribute(IRunDockerImageLaunchConfigurationConstants.UNUSED_PORTS, new ArrayList<String>());
			final Set<ExposedPortModel> selectedPorts = new HashSet<>();
			for (String port : publishedPorts) {
				final ImageRunSelectionModel.ExposedPortModel exposedPort = ImageRunSelectionModel.ExposedPortModel
						.createPortModel(port);
				model.addExposedPort(exposedPort);
				if (exposedPort.getSelected()) {
					selectedPorts.add(exposedPort);
				}
			}
			for (String port : unusedPorts) {
				final ImageRunSelectionModel.ExposedPortModel exposedPort = ImageRunSelectionModel.ExposedPortModel
						.createPortModel(port);
				exposedPort.setSelected(false);
				model.addExposedPort(exposedPort);
			}

			// select ports
			model.setSelectedPorts(selectedPorts);

			// update the underlying launch config working copy on model
			// changes.
			model.addPropertyChangeListener(
					new LaunchConfigurationChangeListener());
		} catch (CoreException e) {
			Activator.logErrorMessage(
					LaunchMessages.getString(
							"RunDockerImageLaunchConfiguration.load.failure"), //$NON-NLS-1$
					e);
		}

	}

	@Override
	public void performApply(ILaunchConfigurationWorkingCopy configuration) {
		if (model == null)
			return;
		configuration.setAttribute(
				IRunDockerImageLaunchConfigurationConstants.PUBLISH_ALL_PORTS,
				model.isPublishAllPorts());
		List<String> portsList = new ArrayList<>();
		for (ImageRunSelectionModel.ExposedPortModel m : model.getExposedPorts()) {
			portsList.add(m.toString());
		}
		configuration.setAttribute(
				IRunDockerImageLaunchConfigurationConstants.PUBLISHED_PORTS,
				portsList);
	}

	@Override
	public String getName() {
		return LaunchMessages.getString(TAB_NAME);
	}

	private class LaunchConfigurationChangeListener
			implements PropertyChangeListener {

		@Override
		public void propertyChange(final PropertyChangeEvent evt) {
			updateLaunchConfigurationDialog();
		}
	}

}
