/*******************************************************************************
 * Copyright (c) 2000, 2011 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
 *     Brock Janiczak <brockj@tpg.com.au> - Bug 185708 Provide link to open SSH/SSH2/proxy preferences from Connection wizard
 *     Brock Janiczak <brockj@tpg.com.au> - Bug 107025 [Wizards] expose the 'paste cvs connection' easter egg
 *     Remy Chi Jian Suen <remy.suen@gmail.com> - Bug 274284 - 'Add CVS Repository' dialog is clipped at the bottom
 *******************************************************************************/
package org.eclipse.team.internal.ccvs.ui.wizards;

import java.util.*;
import java.util.List;

import org.eclipse.core.runtime.*;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.fieldassist.*;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;
import org.eclipse.team.internal.ccvs.core.*;
import org.eclipse.team.internal.ccvs.core.connection.CVSRepositoryLocation;
import org.eclipse.team.internal.ccvs.core.util.KnownRepositories;
import org.eclipse.team.internal.ccvs.ui.*;
import org.eclipse.team.internal.ui.SWTUtils;
import org.eclipse.ui.PlatformUI;

/**
 * Wizard page for entering information about a CVS repository location.
 */
public class ConfigurationWizardMainPage extends CVSWizardPage {
	private static final String ANONYMOUS_USER = "anonymous"; //$NON-NLS-1$

	private boolean showValidate;
	private boolean validate;

	// Widgets

	// Connection Method
	private Combo connectionMethodCombo;
	// User
	private Combo userCombo;
	// Password
	private Text passwordText;
	// Port
	private Text portText;
	private Button useDefaultPort;
	private Button useCustomPort;
	// Host
	private Combo hostCombo;
	// Repository Path
	private Combo repositoryPathCombo;
	// Validation
	private Button validateButton;
	// Caching password
	private Button allowCachingButton;
	private boolean allowCaching = false;

	private static final int COMBO_HISTORY_LENGTH = 5;

	private Properties properties = null;

	// The previously created repository.
	// It is recorded when asked for and
	// nulled when the page is changed.
	private ICVSRepositoryLocation location;

	// The previously created repository.
	// It is recorded when fields are changed
	private ICVSRepositoryLocation oldLocation;

	// Dialog store id constants
	private static final String STORE_USERNAME_ID =
		"ConfigurationWizardMainPage.STORE_USERNAME_ID";//$NON-NLS-1$
	private static final String STORE_HOSTNAME_ID =
		"ConfigurationWizardMainPage.STORE_HOSTNAME_ID";//$NON-NLS-1$
	private static final String STORE_PATH_ID =
		"ConfigurationWizardMainPage.STORE_PATH_ID";//$NON-NLS-1$
	private static final String STORE_DONT_VALIDATE_ID =
		"ConfigurationWizardMainPage.STORE_DONT_VALIDATE_ID";//$NON-NLS-1$

	// In case the page was launched from a different wizard
	private IDialogSettings settings;

	/**
	 * ConfigurationWizardMainPage constructor.
	 * 
	 * @param pageName  the name of the page
	 * @param title  the title of the page
	 * @param titleImage  the image for the page
	 */
	public ConfigurationWizardMainPage(String pageName, String title, ImageDescriptor titleImage) {
		super(pageName, title, titleImage);
	}
	/**
	 * Adds an entry to a history, while taking care of duplicate history items
	 * and excessively long histories.  The assumption is made that all histories
	 * should be of length <code>ConfigurationWizardMainPage.COMBO_HISTORY_LENGTH</code>.
	 *
	 * @param history the current history
	 * @param newEntry the entry to add to the history
	 * @return the history with the new entry appended
	 */
	private String[] addToHistory(String[] history, String newEntry) {
		ArrayList<String> l = new ArrayList<>(Arrays.asList(history));
		addToHistory(l, newEntry);
		String[] r = new String[l.size()];
		l.toArray(r);
		return r;
	}
	@Override
	protected IDialogSettings getDialogSettings() {
		return settings;
	}
	protected void setDialogSettings(IDialogSettings settings) {
		this.settings = settings;
	}
	/**
	 * Adds an entry to a history, while taking care of duplicate history items
	 * and excessively long histories.  The assumption is made that all histories
	 * should be of length <code>ConfigurationWizardMainPage.COMBO_HISTORY_LENGTH</code>.
	 *
	 * @param history the current history
	 * @param newEntry the entry to add to the history
	 */
	private void addToHistory(List<String> history, String newEntry) {
		history.remove(newEntry);
		history.add(0,newEntry);

		// since only one new item was added, we can be over the limit
		// by at most one item
		if (history.size() > COMBO_HISTORY_LENGTH)
			history.remove(COMBO_HISTORY_LENGTH);
	}
	/**
	 * Creates the UI part of the page.
	 * 
	 * @param parent  the parent of the created widgets
	 */
	@Override
	public void createControl(Composite parent) {
		Composite composite = createComposite(parent, 2, false);
		// set F1 help
		PlatformUI.getWorkbench().getHelpSystem().setHelp(composite, IHelpContextIds.SHARING_NEW_REPOSITORY_PAGE);

		Listener listener = event -> {
			if (location != null) {
				oldLocation = location;
				location = null;
			}
			if (event.widget == hostCombo) {
				String hostText = hostCombo.getText();
				if (hostText.length() > 0 && hostText.charAt(0) == ':') {
					try {
						CVSRepositoryLocation newLocation = CVSRepositoryLocation.fromString(hostText);
						connectionMethodCombo.setText(newLocation.getMethod().getName());
						repositoryPathCombo.setText(newLocation.getRootDirectory());
						int port = newLocation.getPort();
						if (port == ICVSRepositoryLocation.USE_DEFAULT_PORT) {
							useDefaultPort.setSelection(true);
							useCustomPort.setSelection(false);
						} else {
							useCustomPort.setSelection(true);
							useDefaultPort.setSelection(false);
							portText.setText(String.valueOf(port));
						}

						userCombo.setText(newLocation.getUsername());
						// passwordText.setText(newLocation.xxx);
						hostCombo.setText(newLocation.getHost());
					} catch (CVSException e) {
						CVSUIPlugin.log(e);
					}
				}
			}
			updateWidgetEnablements();
		};

		Group g = createGroup(composite, CVSUIMessages.ConfigurationWizardMainPage_Location_1);

		// Host name
		createLabel(g, CVSUIMessages.ConfigurationWizardMainPage_host);
		hostCombo = createEditableCombo(g);
		ControlDecoration decoration = new ControlDecoration(hostCombo, SWT.TOP | SWT.LEFT);
		FieldDecoration infoDecoration = FieldDecorationRegistry.getDefault().getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION);
		decoration.setImage(infoDecoration.getImage());
		decoration.setDescriptionText(CVSUIMessages.ConfigurationWizardMainPage_8);
		decoration.setShowOnlyOnFocus(true);

		((GridLayout)g.getLayout()).horizontalSpacing = decoration.getMarginWidth() + infoDecoration.getImage().getBounds().width;

		hostCombo.addListener(SWT.Selection, listener);
		hostCombo.addListener(SWT.Modify, listener);

		// Repository Path
		createLabel(g, CVSUIMessages.ConfigurationWizardMainPage_repositoryPath);
		repositoryPathCombo = createEditableCombo(g);
		repositoryPathCombo.addListener(SWT.Selection, listener);
		repositoryPathCombo.addListener(SWT.Modify, listener);

		g = createGroup(composite, CVSUIMessages.ConfigurationWizardMainPage_Authentication_2);

		// User name
		createLabel(g, CVSUIMessages.ConfigurationWizardMainPage_userName);
		userCombo = createEditableCombo(g);
		userCombo.addListener(SWT.Selection, listener);
		userCombo.addListener(SWT.Modify, listener);

		// Password
		createLabel(g, CVSUIMessages.ConfigurationWizardMainPage_password);
		passwordText = createPasswordField(g);
		passwordText.addListener(SWT.Modify, listener);

		g = createGroup(composite, CVSUIMessages.ConfigurationWizardMainPage_Connection_3);

		// Connection type
		createLabel(g, CVSUIMessages.ConfigurationWizardMainPage_connection);
		connectionMethodCombo = createCombo(g);
		connectionMethodCombo.addListener(SWT.Selection, listener);

		// Port number
		// create a composite to ensure the radio buttons come in the correct order
		Composite portGroup = new Composite(g, SWT.NONE);
		GridData data = new GridData();
		data.horizontalSpan = 2;
		portGroup.setLayoutData(data);
		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		portGroup.setLayout(layout);
		useDefaultPort = createRadioButton(portGroup, CVSUIMessages.ConfigurationWizardMainPage_useDefaultPort, 2);
		useCustomPort = createRadioButton(portGroup, CVSUIMessages.ConfigurationWizardMainPage_usePort, 1);
		useCustomPort.addListener(SWT.Selection, listener);
		portText = createTextField(portGroup);
		portText.addListener(SWT.Modify, listener);

		// create a composite to ensure the validate button is in its own tab group
		if (showValidate) {
			Composite validateButtonTabGroup = new Composite(composite, SWT.NONE);
			data = new GridData();
			data.horizontalSpan = 2;
			validateButtonTabGroup.setLayoutData(data);
			validateButtonTabGroup.setLayout(new FillLayout());

			validateButton = new Button(validateButtonTabGroup, SWT.CHECK);
			validateButton.setText(CVSUIMessages.ConfigurationWizardAutoconnectPage_validate);
			validateButton.addListener(SWT.Selection, e -> validate = validateButton.getSelection());
		}

		allowCachingButton = new Button(composite, SWT.CHECK);
		allowCachingButton.setText(CVSUIMessages.UserValidationDialog_6);
		data = new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL);
		data.horizontalSpan = 2;
		allowCachingButton.setLayoutData(data);
		allowCachingButton.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				allowCaching = allowCachingButton.getSelection();
			}
		});

		Link link = SWTUtils.createPreferenceLink(getShell(), composite, CVSUIMessages.ConfigurationWizardMainPage_9, CVSUIMessages.ConfigurationWizardMainPage_10);
		data = new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL);
		data.horizontalSpan = 2;
		link.setLayoutData(data);

		SWTUtils.createPreferenceLink(getShell(), composite,
				"org.eclipse.team.cvs.ui.ExtMethodPreferencePage", //$NON-NLS-1$
				new String[] { "org.eclipse.team.cvs.ui.cvs", //$NON-NLS-1$
			"org.eclipse.team.cvs.ui.ExtMethodPreferencePage", //$NON-NLS-1$
			"org.eclipse.jsch.ui.SSHPreferences", //$NON-NLS-1$
		"org.eclipse.ui.net.NetPreferences" }, //$NON-NLS-1$
		CVSUIMessages.ConfigurationWizardMainPage_7);

		initializeValues();
		updateWidgetEnablements();
		hostCombo.setFocus();

		setControl(composite);
		Dialog.applyDialogFont(parent);
	}

	/**
	 * Utility method to create an editable combo box
	 * 
	 * @param parent  the parent of the combo box
	 * @return the created combo
	 */
	protected Combo createEditableCombo(Composite parent) {
		Combo combo = new Combo(parent, SWT.NULL);
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		data.widthHint = IDialogConstants.ENTRY_FIELD_WIDTH;
		combo.setLayoutData(data);
		return combo;
	}

	protected Group createGroup(Composite parent, String text) {
		Group group = new Group(parent, SWT.NULL);
		group.setText(text);
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		data.horizontalSpan = 2;
		//data.widthHint = GROUP_WIDTH;

		group.setLayoutData(data);
		GridLayout layout = new GridLayout();
		layout.numColumns = 2;
		group.setLayout(layout);
		return group;
	}

	/*
	 * Create a Properties node that contains everything needed to create a repository location
	 */
	private Properties createProperties() {
		Properties result = new Properties();
		result.setProperty("connection", connectionMethodCombo.getText()); //$NON-NLS-1$
		result.setProperty("user", userCombo.getText()); //$NON-NLS-1$
		result.setProperty("password", passwordText.getText()); //$NON-NLS-1$
		result.setProperty("host", hostCombo.getText()); //$NON-NLS-1$
		if (useCustomPort.getSelection()) {
			result.setProperty("port", portText.getText()); //$NON-NLS-1$
		}
		result.setProperty("root", repositoryPathCombo.getText()); //$NON-NLS-1$
		return result;
	}

	/**
	 * Create a new location with the information entered on the page.
	 * The location will exists and can be set for connecting but is not
	 * registered for persistence. This method must be called from the UI
	 * thread.
	 * @return a location or <code>null</code>
	 * @throws CVSException
	 */
	public ICVSRepositoryLocation getLocation() throws CVSException {
		if (location == null) {
			if (!isPageComplete()) return null;
			location = CVSRepositoryLocation.fromProperties(createProperties());
			if (location.equals(oldLocation)) {
				location = oldLocation;
			}
			location.setAllowCaching(allowCaching);
			oldLocation = null;
			saveWidgetValues();
		}
		return location;
	}

	/**
	 * Initializes states of the controls.
	 */
	private void initializeValues() {
		// Set remembered values
		IDialogSettings settings = getDialogSettings();
		if (settings != null) {
			String[] hostNames = settings.getArray(STORE_HOSTNAME_ID);
			if (hostNames != null) {
				for (int i = 0; i < hostNames.length; i++) {
					hostCombo.add(hostNames[i]);
				}
			}
			String[] paths = settings.getArray(STORE_PATH_ID);
			if (paths != null) {
				for (int i = 0; i < paths.length; i++) {
					repositoryPathCombo.add(paths[i]);
				}
			}
			String[] userNames = settings.getArray(STORE_USERNAME_ID);
			if (userNames != null) {
				for (int i = 0; i < userNames.length; i++) {
					userCombo.add(userNames[i]);
				}
			}
			userCombo.add(ANONYMOUS_USER);
			if (showValidate) {
				validate = !settings.getBoolean(STORE_DONT_VALIDATE_ID);
				validateButton.setSelection(validate);
			}
		}

		// Initialize other values and widget states
		IConnectionMethod[] methods = CVSRepositoryLocation.getPluggedInConnectionMethods();
		for (int i = 0; i < methods.length; i++) {
			connectionMethodCombo.add(methods[i].getName());
		}

		// pserver is a default connection method
		int defaultIndex = connectionMethodCombo.indexOf("pserver") != -1 ? connectionMethodCombo.indexOf("pserver") : 0;  //$NON-NLS-1$ //$NON-NLS-2$

		connectionMethodCombo.select(defaultIndex);
		useDefaultPort.setSelection(true);

		if(properties != null) {
			String method = properties.getProperty("connection"); //$NON-NLS-1$
			if (method == null) {
				connectionMethodCombo.select(defaultIndex);
			} else {
				connectionMethodCombo.select(connectionMethodCombo.indexOf(method));
			}

			String user = properties.getProperty("user"); //$NON-NLS-1$
			if (user != null) {
				userCombo.setText(user);
			}

			String password = properties.getProperty("password"); //$NON-NLS-1$
			if (password != null) {
				passwordText.setText(password);
			}

			String host = properties.getProperty("host"); //$NON-NLS-1$
			if (host != null) {
				hostCombo.setText(host);
			}

			String port = properties.getProperty("port"); //$NON-NLS-1$
			if (port != null) {
				useCustomPort.setSelection(true);
				portText.setText(port);
			}

			String repositoryPath = properties.getProperty("root"); //$NON-NLS-1$
			if (repositoryPath != null) {
				repositoryPathCombo.setText(repositoryPath);
			}
		}
	}
	/**
	 * Saves the widget values
	 */
	private void saveWidgetValues() {
		// Update history
		IDialogSettings settings = getDialogSettings();
		if (settings != null) {
			String userName = userCombo.getText();
			if (!userName.equals(ANONYMOUS_USER)) {
				String[] userNames = settings.getArray(STORE_USERNAME_ID);
				if (userNames == null) userNames = new String[0];
				userNames = addToHistory(userNames, userName);
				settings.put(STORE_USERNAME_ID, userNames);
			}
			String[] hostNames = settings.getArray(STORE_HOSTNAME_ID);
			if (hostNames == null) hostNames = new String[0];
			hostNames = addToHistory(hostNames, hostCombo.getText());
			settings.put(STORE_HOSTNAME_ID, hostNames);

			String[] paths = settings.getArray(STORE_PATH_ID);
			if (paths == null) paths = new String[0];
			paths = addToHistory(paths, repositoryPathCombo.getText());
			settings.put(STORE_PATH_ID, paths);

			if (showValidate) {
				settings.put(STORE_DONT_VALIDATE_ID, !validate);
			}
		}
	}

	public void setShowValidate(boolean showValidate) {
		this.showValidate = showValidate;
	}

	/**
	 * Sets the properties for the repository connection
	 * 
	 * @param properties  the properties or null
	 */
	public void setProperties(Properties properties) {
		this.properties = properties;
	}

	/**
	 * Updates widget enablements and sets error message if appropriate.
	 */
	protected void updateWidgetEnablements() {
		if (useDefaultPort.getSelection()) {
			portText.setEnabled(false);
		} else {
			portText.setEnabled(true);
		}

		validateFields();
	}
	/**
	 * Validates the contents of the editable fields and set page completion
	 * and error messages appropriately.
	 */
	protected void validateFields() {
		String user = userCombo.getText();
		IStatus status = validateUserName(user);
		if (!isStatusOK(status)) {
			return;
		}

		String host = hostCombo.getText();
		status = validateHost(host);
		if (!isStatusOK(status)) {
			return;
		}

		if (portText.isEnabled()) {
			String port = portText.getText();
			status = validatePort(port);
			if (!isStatusOK(status)) {
				return;
			}
		}

		String pathString = repositoryPathCombo.getText();
		status = validatePath(pathString);
		if (!isStatusOK(status)) {
			return;
		}

		try {
			CVSRepositoryLocation l = CVSRepositoryLocation.fromProperties(createProperties());
			if (!l.equals(oldLocation) && KnownRepositories.getInstance().isKnownRepository(l.getLocation())) {
				setErrorMessage(CVSUIMessages.ConfigurationWizardMainPage_0);
				setPageComplete(false);
				return;
			}
		} catch (CVSException e) {
			CVSUIPlugin.log(e);
			// Let it pass. Creation should fail
		}

		// Everything passed so we're good to go
		setErrorMessage(null);
		setPageComplete(true);
	}

	private boolean isStatusOK(IStatus status) {
		if (!status.isOK()) {
			if (status.getCode() == REQUIRED_FIELD) {
				// Don't set the message for an empty field
				setErrorMessage(null);
			} else {
				setErrorMessage(status.getMessage());
			}
			setPageComplete(false);
			return false;
		}
		return true;
	}

	public boolean getValidate() {
		return validate;
	}
	@Override
	public void setVisible(boolean visible) {
		super.setVisible(visible);
		if (visible) {
			hostCombo.setFocus();
		}
	}

	public static final int REQUIRED_FIELD = 1;
	public static final int INVALID_FIELD_CONTENTS = 2;
	public static final IStatus validateUserName(String user) {
		if (user.length() == 0) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, REQUIRED_FIELD, CVSUIMessages.ConfigurationWizardMainPage_1, null);
		}
		// removed the @ sign check since, so the UI can allow full kerberos names
		if (user.indexOf(':') != -1) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
					CVSUIMessages.ConfigurationWizardMainPage_invalidUserName, null);
		}
		if (user.startsWith(" ") || user.endsWith(" ")) { //$NON-NLS-1$ //$NON-NLS-2$
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
					CVSUIMessages.ConfigurationWizardMainPage_6, null);
		}
		return Status.OK_STATUS;
	}
	public static final IStatus validateHost(String host) {
		if (host.length() == 0) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, REQUIRED_FIELD, CVSUIMessages.ConfigurationWizardMainPage_2, null);
		}
		if (host.indexOf(':') != -1) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
					CVSUIMessages.ConfigurationWizardMainPage_invalidHostName, null);
		}
		if (host.startsWith(" ") || host.endsWith(" ")) { //$NON-NLS-1$ //$NON-NLS-2$
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
					CVSUIMessages.ConfigurationWizardMainPage_5, null);
		}
		return Status.OK_STATUS;
	}
	public static final IStatus validatePort(String port) {
		if (port.length() == 0) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, REQUIRED_FIELD, CVSUIMessages.ConfigurationWizardMainPage_3, null);
		}
		try {
			Integer.parseInt(port);
		} catch (NumberFormatException e) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
					CVSUIMessages.ConfigurationWizardMainPage_invalidPort, null);
		}
		return Status.OK_STATUS;
	}
	public static final IStatus validatePath(String pathString) {
		if (pathString.length() == 0) {
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, REQUIRED_FIELD, CVSUIMessages.ConfigurationWizardMainPage_4, null);
		}
		IPath path = new Path(null, pathString);
		String[] segments = path.segments();
		for (int i = 0; i < segments.length; i++) {
			String string = segments[i];
			if (string.charAt(0) == ' ' || string.charAt(string.length() -1) == ' ') {
				return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
						CVSUIMessages.ConfigurationWizardMainPage_invalidPathWithSpaces, null);
			}
		}
		// look for // and inform the user that we support use of C:\cvs\root instead of /c//cvs/root
		if (pathString.indexOf("//") != -1) { //$NON-NLS-1$
			if (pathString.indexOf("//") == 2) { //$NON-NLS-1$
				// The user is probably trying to specify a CVSNT path
				return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
						CVSUIMessages.ConfigurationWizardMainPage_useNTFormat, null);
			} else {
				return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
						CVSUIMessages.ConfigurationWizardMainPage_invalidPathWithSlashes, null);
			}
		}
		if (pathString.endsWith("/")) { //$NON-NLS-1$
			return new Status(IStatus.ERROR, CVSUIPlugin.ID, INVALID_FIELD_CONTENTS,
					CVSUIMessages.ConfigurationWizardMainPage_invalidPathWithTrailingSlash, null);
		}
		return Status.OK_STATUS;
	}
}
