/*******************************************************************************
 * Copyright (c) 2000, 2015 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
 *     Serge Beauchamp (Freescale Semiconductor) - [229633] Project Path Variable Support
 *******************************************************************************/

package org.eclipse.ui.internal.ide.dialogs;

import java.net.URI;
import java.net.URISyntaxException;

import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IPathVariableManager;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.equinox.bidi.StructuredTextTypeHandlerFactory;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.util.BidiUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DirectoryDialog;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.ide.dialogs.PathVariableSelectionDialog;
import org.eclipse.ui.internal.ide.IDEWorkbenchMessages;
import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin;
import org.eclipse.ui.internal.ide.filesystem.FileSystemConfiguration;
import org.eclipse.ui.internal.ide.filesystem.FileSystemSupportRegistry;

/**
 * Widget group for specifying a linked file or folder target.
 *
 * @since 2.1
 */
public class CreateLinkedResourceGroup {
	private Listener listener;

	private String linkTarget = ""; //$NON-NLS-1$

	private int type;

	private boolean createLink = false;

	// used to compute layout sizes
	private FontMetrics fontMetrics;

	// widgets
	private Composite groupComposite;

	private Text linkTargetField;

	private Button browseButton;

	private Button variablesButton;

	private Label resolvedPathLabelText;

	private Label resolvedPathLabelData;

	private final IStringValue updatableResourceName;

	/**
	 * Helper interface intended for updating a string value based on the
	 * currently selected link target.
	 *
	 * @since 3.2
	 */
	public static interface IStringValue {
		/**
		 * Sets the String value.
		 *
		 * @param string
		 *            a non-null String
		 */
		void setValue(String string);

		/**
		 * Gets the String value.
		 *
		 * @return the current value, or <code>null</code>
		 */
		String getValue();

		/**
		 * Gets the resource to which the resource will belong
		 *
		 * @return the IResource object, or <code>null</code>
		 */
		IResource getResource();
	}

	private String lastUpdatedValue;

	private FileSystemSelectionArea fileSystemSelectionArea;

	/**
	 * Creates a link target group
	 *
	 * @param type
	 *            specifies the type of resource to link to.
	 *            <code>IResource.FILE</code> or <code>IResource.FOLDER</code>
	 * @param listener
	 *            listener to notify when one of the widgets' value is changed.
	 * @param updatableResourceName
	 *            an updatable string value that will be updated to reflect the
	 *            link target's last segment, or <code>null</code>. Updating
	 *            will only happen if the current value of that string is null
	 *            or the empty string, or if it has not been changed since the
	 *            last time it was updated.
	 */
	public CreateLinkedResourceGroup(int type, Listener listener,
			IStringValue updatableResourceName) {
		this.type = type;
		this.listener = listener;
		this.updatableResourceName = updatableResourceName;
		if (updatableResourceName != null) {
			lastUpdatedValue = updatableResourceName.getValue();
		}
	}

	/**
	 * Creates the widgets
	 *
	 * @param parent
	 *            parent composite of the widget group
	 * @return the widget group
	 */
	public Composite createContents(Composite parent) {
		return createContents(parent, true);
	}

	/**
	 * Creates the widgets without the checkbox button
	 *
	 * @param parent
	 *            parent composite of the widget group
	 * @return the widget group
	 */
	public Composite createTextOnlyContents(Composite parent) {
		return createContents(parent, false);
	}

	/**
	 * Creates the widgets
	 *
	 * @param parent
	 *            parent composite of the widget group
	 * @return the widget group
	 */
	private Composite createContents(Composite parent, boolean createCheckbox) {
		Font font = parent.getFont();
		initializeDialogUnits(parent);
		// top level group
		groupComposite = new Composite(parent, SWT.NONE);
		GridLayout layout = new GridLayout();
		groupComposite.setLayout(layout);
		groupComposite.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_FILL
				| GridData.FILL_HORIZONTAL));
		groupComposite.setFont(font);

		if (createCheckbox) {
			final Button createLinkButton = new Button(groupComposite, SWT.CHECK);
			if (type == IResource.FILE) {
				createLinkButton
						.setText(IDEWorkbenchMessages.CreateLinkedResourceGroup_linkFileButton);
			} else {
				createLinkButton
						.setText(IDEWorkbenchMessages.CreateLinkedResourceGroup_linkFolderButton);
			}
			createLinkButton.setSelection(createLink);
			createLinkButton.setFont(font);
			SelectionListener selectionListener = new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent e) {
					setEnabled(createLinkButton.getSelection());
				}
			};
			createLinkButton.addSelectionListener(selectionListener);
		}

		createLinkLocationGroup(groupComposite, createLink);
		return groupComposite;
	}

	/**
	 * Set the group enabled state
	 * @param value true if enabled, false otherwise
	 */
	public void setEnabled(boolean value) {
		createLink = value;
		browseButton.setEnabled(createLink);
		variablesButton.setEnabled(createLink);
		// Set the required field color if the field is enabled
		linkTargetField.setEnabled(createLink);
		if (fileSystemSelectionArea != null)
			fileSystemSelectionArea.setEnabled(createLink);

		if (listener != null) {
			listener.handleEvent(new Event());
		}
	}

	/**
	 * Return the group enabled state
	 * @return if the group is enabled
	 */
	public boolean isEnabled() {
		return createLink;
	}

	/**
	 * Creates the link target location widgets.
	 *
	 * @param locationGroup
	 *            the parent composite
	 * @param enabled
	 *            sets the initial enabled state of the widgets
	 */
	private void createLinkLocationGroup(Composite locationGroup,
			boolean enabled) {
		Button button = new Button(locationGroup, SWT.CHECK);
		int indent = button.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;

		button.dispose();

		// linkTargetGroup is necessary to decouple layout from
		// resolvedPathGroup layout
		Composite linkTargetGroup = new Composite(locationGroup, SWT.NONE);
		GridLayout layout = new GridLayout();
		layout.numColumns = 4;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		linkTargetGroup.setLayout(layout);
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		data.horizontalIndent = indent;
		linkTargetGroup.setLayoutData(data);
		linkTargetGroup.setFont(locationGroup.getFont());

		// link target location entry field
		linkTargetField = new Text(linkTargetGroup, SWT.BORDER);
		data = new GridData(GridData.FILL_HORIZONTAL);
		data.widthHint = IDialogConstants.ENTRY_FIELD_WIDTH;
		data.horizontalSpan = 2;
		linkTargetField.setLayoutData(data);
		linkTargetField.setEnabled(enabled);
		linkTargetField.setFont(locationGroup.getFont());
		BidiUtils.applyBidiProcessing(linkTargetField, StructuredTextTypeHandlerFactory.FILE);
		linkTargetField.addModifyListener(e -> {
			linkTarget = linkTargetField.getText();
			if (isDefaultConfigurationSelected()) {
				linkTarget = getPathVariableManager().convertFromUserEditableFormat(linkTarget, true);
			}
			resolveVariable();
			if (updatableResourceName != null) {
				String value = updatableResourceName.getValue();
				if (value == null
						|| value.equals("") || value.equals(lastUpdatedValue)) { //$NON-NLS-1$
					IPath linkTargetPath = new Path(linkTarget);
					String lastSegment = linkTargetPath.lastSegment();
					if (lastSegment != null) {
						lastUpdatedValue = lastSegment;
						updatableResourceName.setValue(lastSegment);
					}
				}
			}
			if (listener != null) {
				listener.handleEvent(new Event());
			}
		});

		// browse button
		browseButton = new Button(linkTargetGroup, SWT.PUSH);
		browseButton
				.setText(IDEWorkbenchMessages.CreateLinkedResourceGroup_browseButton);
		browseButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent event) {
				handleLinkTargetBrowseButtonPressed();
			}
		});
		browseButton.setFont(locationGroup.getFont());
		browseButton.setEnabled(enabled);
		setButtonLayoutData(browseButton);

		// variables button
		variablesButton = new Button(linkTargetGroup, SWT.PUSH);
		variablesButton
				.setText(IDEWorkbenchMessages.CreateLinkedResourceGroup_variablesButton);
		variablesButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent event) {
				handleVariablesButtonPressed();
			}
		});
		variablesButton.setFont(locationGroup.getFont());
		variablesButton.setEnabled(enabled);
		setButtonLayoutData(variablesButton);

		createFileSystemSelection(linkTargetGroup, enabled);

		createResolvedPathGroup(locationGroup, indent);

		if (linkTarget != null) {
			linkTargetField.setText(linkTarget);
		}
	}

	/**
	 * Create the file system selection area.
	 *
	 * @param composite
	 * @param enabled
	 *            the initial enablement state.
	 */
	private void createFileSystemSelection(Composite composite, boolean enabled) {

		// Always use the default if that is all there is.
		if (FileSystemSupportRegistry.getInstance().hasOneFileSystem()) {
			return;
		}

		fileSystemSelectionArea = new FileSystemSelectionArea();

		Composite parent = new Composite(composite, SWT.NONE);
		parent.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false, 4, 1));
		GridLayout layout = new GridLayout(2, false);
		layout.marginWidth = 0;
		layout.marginHeight = 0;
		parent.setLayout(layout);
		parent.setFont(composite.getFont());

		fileSystemSelectionArea.createContents(parent);

		fileSystemSelectionArea.setEnabled(enabled);
	}

	/**
	 * Create the composite for the resolved path.
	 *
	 * @param locationGroup
	 * @param indent
	 */
	private void createResolvedPathGroup(Composite locationGroup, int indent) {
		GridLayout layout;
		GridData data;
		Composite resolvedPathGroup = new Composite(locationGroup, SWT.NONE);
		layout = new GridLayout();
		layout.numColumns = 2;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		resolvedPathGroup.setLayout(layout);
		data = new GridData(GridData.FILL_HORIZONTAL);
		data.horizontalIndent = indent;
		resolvedPathGroup.setLayoutData(data);
		resolvedPathGroup.setFont(locationGroup.getFont());

		resolvedPathLabelText = new Label(resolvedPathGroup, SWT.SINGLE);
		resolvedPathLabelText
				.setText(IDEWorkbenchMessages.CreateLinkedResourceGroup_resolvedPathLabel);
		resolvedPathLabelText.setVisible(false);
		resolvedPathLabelText.setFont(locationGroup.getFont());

		resolvedPathLabelData = new Label(resolvedPathGroup, SWT.SINGLE);
		data = new GridData(GridData.FILL_HORIZONTAL);
		resolvedPathLabelData.setLayoutData(data);
		resolvedPathLabelData.setVisible(false);
	}

	/**
	 * Returns a new status object with the given severity and message.
	 *
	 * @return a new status object with the given severity and message.
	 */
	private IStatus createStatus(int severity, String message) {
		return new Status(severity, IDEWorkbenchPlugin.getDefault().getBundle()
				.getSymbolicName(), severity, message, null);
	}

	/**
	 * Disposes the group's widgets.
	 */
	public void dispose() {
		if (groupComposite != null && groupComposite.isDisposed() == false) {
			groupComposite.dispose();
		}
	}

	/**
	 * Returns the link target location entered by the user.
	 *
	 * @return the link target location entered by the user. null if the user
	 *         chose not to create a link.
	 */
	public URI getLinkTargetURI() {
		if (!createLink)
			return null;
		// linkTarget can contain either:
		//  1) a URI, ex: 								foo://bar/file.txt
		//  2) A path, ex: 								c:\foo\bar\file.txt
		//  3) A path variable relative path, ex:		VAR\foo\bar\file.txt
		URI uri;
		try {
			IPath path = Path.fromOSString(linkTarget);
			if (path != null && path.toFile().exists())
				return URIUtil.toURI(path);

			uri = new URI(linkTarget);
			URI resolved = getPathVariableManager().resolveURI(uri);
			try {
				EFS.getStore(resolved);
			} catch (CoreException e) {
				return URIUtil.toURI(path);
			}
		}catch(URISyntaxException e) {
			uri = convertToURI(linkTarget);
		}
		return uri;
	}

	private URI convertToURI(String string) {
		FileSystemConfiguration configuration = getSelectedConfiguration();
		if (configuration == null) {
			return URIUtil.toURI(string);
		}
		// validate non-local file system location
		return configuration.getContributor().getURI(string);
	}

	/**
	 * Opens a file or directory browser depending on the link type.
	 */
	private void handleLinkTargetBrowseButtonPressed() {
		IFileStore store = null;
		String selection = null;
		FileSystemConfiguration config= getSelectedConfiguration();
		boolean isDefault = isDefaultConfigurationSelected();

		if (linkTarget.length() > 0) {
			store = IDEResourceInfoUtils.getFileStore(linkTarget);
			if (!store.fetchInfo().exists()) {
				store = null;
			}
		}
		if (type == IResource.FILE) {
			if (isDefault) {
				FileDialog dialog = new FileDialog(linkTargetField.getShell(), SWT.SHEET);
				dialog.setText(IDEWorkbenchMessages.CreateLinkedResourceGroup_targetSelectionTitle);
				if (store != null) {
					if (store.fetchInfo().isDirectory()) {
						dialog.setFilterPath(linkTarget);
					} else {
						dialog.setFileName(linkTarget);
					}
				}
				selection = dialog.open();
			} else {
				URI uri = config.getContributor().browseFileSystem(linkTarget,
						linkTargetField.getShell());
				if (uri != null)
					selection = uri.toString();
			}
		} else {
			String filterPath = null;
			if (store != null) {
				IFileStore path = store;
				if (!store.fetchInfo().isDirectory()) {
					path = store.getParent();
				}
				if (path != null) {
					filterPath = store.toString();
				}
			}

			if (isDefault) {
				DirectoryDialog dialog = new DirectoryDialog(linkTargetField
						.getShell(), SWT.SHEET);
				dialog
						.setMessage(IDEWorkbenchMessages.CreateLinkedResourceGroup_targetSelectionLabel);
				if (filterPath != null)
					dialog.setFilterPath(filterPath);
				selection = dialog.open();
			} else {
				String initialPath = IDEResourceInfoUtils.EMPTY_STRING;
				if (filterPath != null)
					initialPath = filterPath;
				URI uri = config.getContributor().browseFileSystem(initialPath,
						linkTargetField.getShell());
				if (uri != null)
					selection = uri.toString();
			}
		}
		if (selection != null) {
			linkTargetField.setText(selection);
		}
	}

	private boolean isDefaultConfigurationSelected() {
		FileSystemConfiguration config = getSelectedConfiguration();
		return config == null
				|| (FileSystemSupportRegistry.getInstance()
						.getDefaultConfiguration()).equals(config);
	}

	/**
	 * Return the selected configuration or <code>null</code> if there is not
	 * one selected.
	 *
	 * @return FileSystemConfiguration or <code>null</code>
	 */
	private FileSystemConfiguration getSelectedConfiguration() {
		if (fileSystemSelectionArea == null)
			return null;
		return fileSystemSelectionArea.getSelectedConfiguration();
	}

	/**
	 * Opens a path variable selection dialog
	 */
	private void handleVariablesButtonPressed() {
		int variableTypes = IResource.FOLDER;

		// allow selecting file and folder variables when creating a
		// linked file
		if (type == IResource.FILE) {
			variableTypes |= IResource.FILE;
		}

		PathVariableSelectionDialog dialog = new PathVariableSelectionDialog(
				linkTargetField.getShell(), variableTypes);
		if (updatableResourceName != null) {
		dialog.setResource(updatableResourceName.getResource());
		}
		if (dialog.open() == IDialogConstants.OK_ID) {
			String[] variableNames = (String[]) dialog.getResult();
			if (variableNames != null && variableNames.length == 1) {
				linkTargetField.setText(variableNames[0]);
			}
		}
	}

	/**
	 * Initializes the computation of horizontal and vertical dialog units based
	 * on the size of current font.
	 * <p>
	 * This method must be called before <code>setButtonLayoutData</code> is
	 * called.
	 * </p>
	 *
	 * @param control
	 *            a control from which to obtain the current font
	 */
	protected void initializeDialogUnits(Control control) {
		// Compute and store a font metric
		GC gc = new GC(control);
		gc.setFont(control.getFont());
		fontMetrics = gc.getFontMetrics();
		gc.dispose();
	}

	/**
	 * Tries to resolve the value entered in the link target field as a
	 * variable, if the value is a relative path. Displays the resolved value if
	 * the entered value is a variable.
	 */
	private void resolveVariable() {
		IPathVariableManager pathVariableManager = getPathVariableManager();
		boolean isURL = true;
		URI uri;
		try {
			uri = new URI(linkTarget);
		}catch(URISyntaxException e) {
			isURL = false;
			uri = convertToURI(linkTarget);
		}
		URI resolvedURI = pathVariableManager.resolveURI(uri);
		String resolvedString;
		if (isURL)
			resolvedString = resolvedURI.toString();
		else
			resolvedString = URIUtil.toPath(resolvedURI).toOSString();

		if (linkTarget.equals(resolvedString)) {
			resolvedPathLabelText.setVisible(false);
			resolvedPathLabelData.setVisible(false);
		} else {
			resolvedPathLabelText.setVisible(true);
			resolvedPathLabelData.setVisible(true);
		}
		resolvedPathLabelData.setText(resolvedString);
	}

	/**
	 * @return the current IPathVariableManager
	 */
	private IPathVariableManager getPathVariableManager() {
		if (updatableResourceName != null
				&& updatableResourceName.getResource() != null)
			return updatableResourceName.getResource().getPathVariableManager();
		return ResourcesPlugin.getWorkspace().getPathVariableManager();
	}

	/**
	 * Sets the <code>GridData</code> on the specified button to be one that
	 * is spaced for the current dialog page units. The method
	 * <code>initializeDialogUnits</code> must be called once before calling
	 * this method for the first time.
	 *
	 * @param button
	 *            the button to set the <code>GridData</code>
	 * @return the <code>GridData</code> set on the specified button
	 */
	private GridData setButtonLayoutData(Button button) {
		GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL);
		int widthHint = Dialog.convertHorizontalDLUsToPixels(fontMetrics,
				IDialogConstants.BUTTON_WIDTH);
		data.widthHint = Math.max(widthHint, button.computeSize(SWT.DEFAULT,
				SWT.DEFAULT, true).x);
		button.setLayoutData(data);
		return data;
	}

	/**
	 * Sets the value of the link target field
	 *
	 * @param target
	 *            the value of the link target field
	 */
	public void setLinkTarget(String target) {
		linkTarget = target;
		if (linkTargetField != null && linkTargetField.isDisposed() == false) {
			linkTargetField.setText(target);
		}
	}

	/**
	 * Validates the type of the given file against the link type specified in
	 * the constructor.
	 *
	 * @param linkTargetFile
	 *            file to validate
	 * @return IStatus indicating the validation result. IStatus.OK if the given
	 *         file is valid.
	 */
	private IStatus validateFileType(IFileInfo linkTargetFile) {
		if (type == IResource.FILE && linkTargetFile.isDirectory()) {
			return createStatus(
					IStatus.ERROR,
					IDEWorkbenchMessages.CreateLinkedResourceGroup_linkTargetNotFile);
		} else if (type == IResource.FOLDER && !linkTargetFile.isDirectory()) {
			return createStatus(
					IStatus.ERROR,
					IDEWorkbenchMessages.CreateLinkedResourceGroup_linkTargetNotFolder);
		}
		return Status.OK_STATUS;
	}

	/**
	 * Validates this page's controls.
	 *
	 * @param linkHandle
	 *            The target to check
	 *
	 * @return IStatus indicating the validation result. IStatus.OK if the
	 *         specified link target is valid given the linkHandle.
	 */
	public IStatus validateLinkLocation(IResource linkHandle) {
		if (linkTargetField == null || linkTargetField.isDisposed()
				|| !createLink) {
			return Status.OK_STATUS;
		}
		IWorkspace workspace = IDEWorkbenchPlugin.getPluginWorkspace();
		FileSystemConfiguration configuration = getSelectedConfiguration();
		if (configuration == null
				|| EFS.SCHEME_FILE.equals(configuration.getScheme())) {
			// Special handling for UNC paths. See bug 90825
			IPath location = new Path(linkTarget);
			if (location.isUNC()) {
				return createStatus(
						IStatus.WARNING,
						IDEWorkbenchMessages.CreateLinkedResourceGroup_unableToValidateLinkTarget);
			}
		}
		URI locationURI = getLinkTargetURI();
		if (locationURI == null) {
			return createStatus(
					IStatus.WARNING,
					IDEWorkbenchMessages.CreateLinkedResourceGroup_unableToValidateLinkTarget);
		}
		IStatus locationStatus = workspace.validateLinkLocationURI(linkHandle,
				locationURI);
		if (locationStatus.getSeverity() == IStatus.ERROR || linkTarget.trim().equals("")) { //$NON-NLS-1$
			return locationStatus;
		}

		URI resolved = linkHandle.getPathVariableManager().resolveURI(locationURI);
		if (resolved.getScheme() == null) {
			return createStatus(
					IStatus.WARNING,
					IDEWorkbenchMessages.CreateLinkedResourceGroup_linkTargetNonExistent);
		}
		IFileInfo linkTargetFile = IDEResourceInfoUtils.getFileInfo(resolved);
		if (linkTargetFile != null && linkTargetFile.exists()) {
			IStatus fileTypeStatus = validateFileType(linkTargetFile);
			if (!fileTypeStatus.isOK()) {
				return fileTypeStatus;
			}
		} else if (locationStatus.isOK()) {
			// locationStatus takes precedence over missing location warning.
			return createStatus(
					IStatus.WARNING,
					IDEWorkbenchMessages.CreateLinkedResourceGroup_linkTargetNonExistent);
		}
		return locationStatus;
	}
}
