/*******************************************************************************
 * Copyright (c) 2010, 2012 SAP AG and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Tomasz Zarna <Tomasz.Zarna@pl.ibm.com> - Allow to save patches in Workspace
 *    Daniel Megert <daniel_megert@ch.ibm.com> - Create Patch... should remember previously chosen location
 *    Daniel Megert <daniel_megert@ch.ibm.com> - Create Patch wizard must validate initial input
 *******************************************************************************/
package org.eclipse.egit.ui.internal.history;

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

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.egit.core.op.CreatePatchOperation;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.UIIcons;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.Image;
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.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.model.BaseWorkbenchContentProvider;
import org.eclipse.ui.model.WorkbenchLabelProvider;
import org.eclipse.ui.views.navigator.ResourceComparator;

/**
 * A wizard page to choose the target location.
 * <p>
 * Extracted from {@link GitCreatePatchWizard}
 */
public class LocationPage extends WizardPage {

	private static final String PATH_KEY = "GitCreatePatchWizard.LocationPage.path"; //$NON-NLS-1$
	private static final String LOCATION_KEY = "GitCreatePatchWizard.LocationPage.location"; //$NON-NLS-1$
	private static final String LOCATION_VALUE_CLIPBOARD = "clipboard"; //$NON-NLS-1$
	private static final String LOCATION_VALUE_FILE_SYSTEM = "filesystem"; //$NON-NLS-1$
	private static final String LOCATION_VALUE_WORKSPACE = "workspace"; //$NON-NLS-1$


	private Button cpRadio;

	Button fsRadio;

	private Text fsPathText;

	private Button fsBrowseButton;

	private Button wsRadio;

	private Text wsPathText;

	private Button wsBrowseButton;

	private boolean wsBrowsed = false;

	private boolean pageValid;

	private IContainer wsSelectedContainer;

	private class LocationPageContentProvider extends BaseWorkbenchContentProvider {
		//Never show closed projects
		boolean showClosedProjects = false;

		public Object[] getChildren(Object element) {
			if (element instanceof IWorkspace) {
				// check if closed projects should be shown
				IProject[] allProjects = ((IWorkspace) element).getRoot().getProjects();
				if (showClosedProjects)
					return allProjects;

				ArrayList<IProject> accessibleProjects = new ArrayList<IProject>();
				for (IProject project : allProjects)
					if (project.isOpen())
						accessibleProjects.add(project);
				return accessibleProjects.toArray();
			}
			return super.getChildren(element);
		}
	}

	private class WorkspaceDialog extends TitleAreaDialog {

		protected TreeViewer wsTreeViewer;
		protected Text wsFilenameText;
		protected Image dlgTitleImage;

		private boolean modified = false;

		public WorkspaceDialog(Shell shell) {
			super(shell);
		}

		protected Control createContents(Composite parent) {
			Control control = super.createContents(parent);
			setTitle(UIText.GitCreatePatchWizard_WorkspacePatchDialogTitle);
			setMessage(UIText.GitCreatePatchWizard_WorkspacePatchDialogDescription);
			//create title image
			dlgTitleImage = UIIcons.WIZBAN_CREATE_PATCH.createImage();
			setTitleImage(dlgTitleImage);

			return control;
		}

		protected Control createDialogArea(Composite parent){
			Composite parentComposite = (Composite) super.createDialogArea(parent);

			// create a composite with standard margins and spacing
			Composite composite = new Composite(parentComposite, SWT.NONE);
			GridLayout layout = new GridLayout();
			layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
			layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN);
			layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING);
			layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING);
			composite.setLayout(layout);
			composite.setLayoutData(new GridData(GridData.FILL_BOTH));
			composite.setFont(parentComposite.getFont());

			getShell().setText(UIText.GitCreatePatchWizard_WorkspacePatchDialogSavePatch);

			wsTreeViewer = new TreeViewer(composite, SWT.BORDER);
			final GridData gd= new GridData(SWT.FILL, SWT.FILL, true, true);
			gd.widthHint= 550;
			gd.heightHint= 250;
			wsTreeViewer.getTree().setLayoutData(gd);

			wsTreeViewer.setContentProvider(new LocationPageContentProvider());
			wsTreeViewer.setComparator(new ResourceComparator(ResourceComparator.NAME));
			wsTreeViewer.setLabelProvider(new WorkbenchLabelProvider());
			wsTreeViewer.setInput(ResourcesPlugin.getWorkspace());

			//Open to whatever is selected in the workspace field
			IPath existingWorkspacePath = new Path(wsPathText.getText());
			//Ensure that this workspace path is valid
			IResource selectedResource = ResourcesPlugin.getWorkspace().getRoot().findMember(existingWorkspacePath);
			if (selectedResource != null) {
				wsTreeViewer.expandToLevel(selectedResource, 0);
				wsTreeViewer.setSelection(new StructuredSelection(selectedResource));
			}

			final Composite group = new Composite(composite, SWT.NONE);
			layout = new GridLayout(2, false);
			layout.marginWidth = 0;
			group.setLayout(layout);
			group.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));

			final Label label = new Label(group, SWT.NONE);
			label.setLayoutData(new GridData());
			label.setText(UIText.GitCreatePatchWizard_WorkspacePatchDialogFileName);

			wsFilenameText = new Text(group,SWT.BORDER);
			wsFilenameText.setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));

			setupListeners();

			return parent;
		}

		protected Button createButton(Composite parent, int id,
				String label, boolean defaultButton) {
			Button button = super.createButton(parent, id, label,
					defaultButton);
			if (id == IDialogConstants.OK_ID)
				button.setEnabled(false);
			return button;
		}

		private void validateDialog() {
			String fileName = wsFilenameText.getText();

			if (fileName.equals("")) //$NON-NLS-1$
				if (modified) {
					setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchDialogEnterFileName);
					getButton(IDialogConstants.OK_ID).setEnabled(false);
					return;
				} else {
					setErrorMessage(null);
					getButton(IDialogConstants.OK_ID).setEnabled(false);
					return;
				}

			// make sure that the filename is valid
			if (!(ResourcesPlugin.getWorkspace().validateName(fileName,
					IResource.FILE)).isOK() && modified) {
				setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchEnterValidFileName);
				getButton(IDialogConstants.OK_ID).setEnabled(false);
				return;
			}

			// Make sure that a container has been selected
			if (getSelectedContainer() == null) {
				setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchDialogEnterValidLocation);
				getButton(IDialogConstants.OK_ID).setEnabled(false);
				return;
			} else {
				IWorkspace workspace = ResourcesPlugin.getWorkspace();
				IPath fullPath = wsSelectedContainer.getFullPath().append(
						fileName);
				if (workspace.getRoot().getFolder(fullPath).exists()) {
					setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchFolderExists);
					getButton(IDialogConstants.OK_ID).setEnabled(false);
					return;
				}
			}

			setErrorMessage(null);
			getButton(IDialogConstants.OK_ID).setEnabled(true);
		}

		protected void okPressed() {
			IFile file = wsSelectedContainer.getFile(new Path(
					wsFilenameText.getText()));
			if (file != null)
				wsPathText.setText(file.getFullPath().toString());

			validatePage();
			super.okPressed();
		}

		private IContainer getSelectedContainer() {
			Object obj = ((IStructuredSelection)wsTreeViewer.getSelection()).getFirstElement();
			if (obj instanceof IContainer)
				wsSelectedContainer = (IContainer) obj;
			else if (obj instanceof IFile)
				wsSelectedContainer = ((IFile) obj).getParent();
			return wsSelectedContainer;
		}

		protected void cancelPressed() {
			validatePage();
			super.cancelPressed();
		}

		public boolean close() {
			if (dlgTitleImage != null)
				dlgTitleImage.dispose();
			return super.close();
		}

		void setupListeners(){
			wsTreeViewer.addSelectionChangedListener(
					new ISelectionChangedListener() {
						public void selectionChanged(SelectionChangedEvent event) {
							IStructuredSelection s = (IStructuredSelection)event.getSelection();
							Object obj=s.getFirstElement();
							if (obj instanceof IContainer)
								wsSelectedContainer = (IContainer) obj;
							else if (obj instanceof IFile){
								IFile tempFile = (IFile) obj;
								wsSelectedContainer = tempFile.getParent();
								wsFilenameText.setText(tempFile.getName());
							}
							validateDialog();
						}
					});

			wsTreeViewer.addDoubleClickListener(
					new IDoubleClickListener() {
						public void doubleClick(DoubleClickEvent event) {
							ISelection s= event.getSelection();
							if (s instanceof IStructuredSelection) {
								Object item = ((IStructuredSelection)s).getFirstElement();
								if (wsTreeViewer.getExpandedState(item))
									wsTreeViewer.collapseToLevel(item, 1);
								else
									wsTreeViewer.expandToLevel(item, 1);
							}
							validateDialog();
						}
					});

			wsFilenameText.addModifyListener(new ModifyListener() {
				public void modifyText(ModifyEvent e) {
					modified = true;
					validateDialog();
				}
			});
		}
	}

	/**
	 * @param pageName
	 * @param title
	 * @param titleImage
	 */
	protected LocationPage(String pageName, String title,
			ImageDescriptor titleImage) {
		super(pageName, title, titleImage);
	}

	public void createControl(Composite parent) {
		final Composite composite = new Composite(parent, SWT.NULL);
		GridLayout gridLayout = new GridLayout();
		gridLayout.numColumns = 3;
		composite.setLayout(gridLayout);
		composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
		initializeDialogUnits(composite);

		String selectedOption= getDialogSettings().get(LOCATION_KEY);
		if (selectedOption == null)
			selectedOption = LOCATION_VALUE_CLIPBOARD;

		// clipboard
		GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
		gd.horizontalSpan = 3;
		cpRadio = new Button(composite, SWT.RADIO);
		cpRadio.setText(UIText.GitCreatePatchWizard_Clipboard);
		cpRadio.setLayoutData(gd);
		cpRadio.setSelection(LOCATION_VALUE_CLIPBOARD.equals(selectedOption));

		// filesystem
		boolean isFileSystemSelected = LOCATION_VALUE_FILE_SYSTEM.equals(selectedOption);
		fsRadio = new Button(composite, SWT.RADIO);
		fsRadio.setText(UIText.GitCreatePatchWizard_File);
		fsRadio.setSelection(isFileSystemSelected);

		fsPathText = new Text(composite, SWT.BORDER);
		gd = new GridData(GridData.FILL_HORIZONTAL);
		fsPathText.setLayoutData(gd);
		fsPathText.setText(createFileName());
		fsPathText.setEnabled(isFileSystemSelected);

		fsBrowseButton = new Button(composite, SWT.PUSH);
		fsBrowseButton.setText(UIText.GitCreatePatchWizard_Browse);
		UIUtils.setButtonLayoutData(fsBrowseButton);
		fsBrowseButton.setEnabled(isFileSystemSelected);

		// workspace
		boolean isWorkspaceSelected = LOCATION_VALUE_WORKSPACE.equals(selectedOption);
		wsRadio = new Button(composite, SWT.RADIO);
		wsRadio.setText(UIText.GitCreatePatchWizard_Workspace);
		wsRadio.setSelection(isWorkspaceSelected);

		wsPathText = new Text(composite, SWT.BORDER);
		wsPathText.setLayoutData(gd);
		wsPathText.setEnabled(isWorkspaceSelected);

		wsBrowseButton = new Button(composite, SWT.PUSH);
		wsBrowseButton.setText(UIText.GitCreatePatchWizard_Browse);
		UIUtils.setButtonLayoutData(wsBrowseButton);
		wsBrowseButton.setEnabled(isWorkspaceSelected);

		cpRadio.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event event) {
				// disable other input controls
				if (((Button) event.widget).getSelection()) {
					fsPathText.setEnabled(false);
					fsBrowseButton.setEnabled(false);
					wsPathText.setEnabled(false);
					wsBrowseButton.setEnabled(false);
					getDialogSettings().put(LOCATION_KEY, LOCATION_VALUE_CLIPBOARD);
					validatePage();
				}
			}
		});

		fsRadio.addListener(SWT.Selection, new Listener() {

			public void handleEvent(Event event) {
				if (((Button) event.widget).getSelection()) {
					// enable filesystem input controls
					fsPathText.setEnabled(true);
					fsBrowseButton.setEnabled(true);
					wsPathText.setEnabled(false);
					wsBrowseButton.setEnabled(false);
					// set focus to filesystem input text control
					fsPathText.setFocus();
					getDialogSettings().put(LOCATION_KEY, LOCATION_VALUE_FILE_SYSTEM);
					validatePage();
				}
			}
		});

		fsPathText.addModifyListener(new ModifyListener() {

			public void modifyText(ModifyEvent e) {
				if (validatePage()) {
					IPath filePath= Path.fromOSString(fsPathText.getText()).removeLastSegments(1);
					getDialogSettings().put(PATH_KEY, filePath.toPortableString());
				}
			}
		});

		fsBrowseButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event event) {
				final FileDialog dialog = new FileDialog(getShell(),
						SWT.PRIMARY_MODAL | SWT.SAVE);
				if (pageValid) {
					final File file = new File(fsPathText.getText());
					dialog.setFilterPath(file.getParent());
					dialog.setFileName(file.getName());
				} else
					dialog.setFileName(""); //$NON-NLS-1$
				dialog.setText(""); //$NON-NLS-1$
				final String path = dialog.open();
				if (path != null)
					fsPathText.setText(new Path(path).toOSString());
				validatePage();
			}
		});

		wsRadio.addListener(SWT.Selection, new Listener() {

			public void handleEvent(Event event) {
				if (((Button) event.widget).getSelection()) {
					fsPathText.setEnabled(false);
					fsBrowseButton.setEnabled(false);
					// enable workspace input controls
					wsPathText.setEnabled(true);
					wsBrowseButton.setEnabled(true);
					// set focus to workspace input text control
					wsPathText.setFocus();
					getDialogSettings().put(LOCATION_KEY, LOCATION_VALUE_WORKSPACE);
					validatePage();
				}
			}
		});

		wsPathText.addModifyListener(new ModifyListener() {

			public void modifyText(ModifyEvent e) {
				if (validatePage()) {
					IPath filePath= Path.fromOSString(wsPathText.getText()).removeLastSegments(1);
					getDialogSettings().put(PATH_KEY, filePath.toPortableString());
				}
			}
		});

		wsBrowseButton.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event event) {
				final WorkspaceDialog dialog = new WorkspaceDialog(getShell());
				wsBrowsed = true;
				dialog.open();
				validatePage();
			}
		});

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

		validatePage();
	}

	private String createFileName() {
		String suggestedFileName = getCommit() != null ? CreatePatchOperation
				.suggestFileName(getCommit()) : getRepository().getWorkTree()
				.getName().concat(".patch"); //$NON-NLS-1$
		String path = getDialogSettings().get(PATH_KEY);
		if (path != null)
			return Path.fromPortableString(path).append(suggestedFileName).toOSString();

		return (new File(System.getProperty("user.dir", ""), suggestedFileName)).getPath(); //$NON-NLS-1$ //$NON-NLS-2$
	}

	private RevCommit getCommit() {
		return ((GitCreatePatchWizard)getWizard()).getCommit();
	}

	private Repository getRepository() {
		return ((GitCreatePatchWizard)getWizard()).getRepository();
	}

	/**
	 * Allow the user to finish if a valid file has been entered.
	 *
	 * @return page is valid
	 */
	protected boolean validatePage() {
		if (wsRadio.getSelection())
			pageValid = validateWorkspaceLocation();
		else if (fsRadio.getSelection())
			pageValid = validateFilesystemLocation();
		else if (cpRadio.getSelection())
			pageValid = true;

		/**
		 * Avoid draw flicker by clearing error message if all is valid.
		 */
		if (pageValid) {
			setMessage(null);
			setErrorMessage(null);
		}
		setPageComplete(pageValid);
		return pageValid;
	}

	/**
	 * The following conditions must hold for the file system location to be
	 * valid:
	 * - the path must be valid and non-empty
	 * - the path must be absolute
	 * - the specified file must be of type file
	 * - the parent must exist (new folders can be created via the browse button)
	 *
	 * @return if the location is valid
	 */
	private boolean validateFilesystemLocation() {
		final String pathString = fsPathText.getText().trim();
		if (pathString.length() == 0
				|| !new Path("").isValidPath(pathString)) { //$NON-NLS-1$
			setErrorMessage(UIText.GitCreatePatchWizard_FilesystemError);
			return false;
		}

		final File file = new File(pathString);
		if (!file.isAbsolute()) {
			setErrorMessage(UIText.GitCreatePatchWizard_FilesystemInvalidError);
			return false;
		}

		if (file.isDirectory()) {
			setErrorMessage(UIText.GitCreatePatchWizard_FilesystemDirectoryError);
			return false;
		}

		if (pathString.endsWith("/") || pathString.endsWith("\\")) { //$NON-NLS-1$//$NON-NLS-2$
			setErrorMessage(UIText.GitCreatePatchWizard_FilesystemDirectoryNotExistsError);
			return false;
		}

		final File parent = file.getParentFile();
		if (!(parent.exists() && parent.isDirectory())) {
			setErrorMessage(UIText.GitCreatePatchWizard_FilesystemDirectoryNotExistsError);
			return false;
		}
		return true;
	}

	/**
	 * The following conditions must hold for the workspace location to be valid:
	 * - a parent must be selected in the workspace tree view
	 * - the resource name must be valid
	 *
	 * @return if the location is valid
	 */
	private boolean validateWorkspaceLocation() {
		//make sure that the field actually has a filename in it - making
		//sure that the user has had a chance to browse the workspace first
		if (wsPathText.getText().equals("")){ //$NON-NLS-1$
			if (wsRadio.getSelection() && wsBrowsed)
				setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchEnterValidFileName);
			else
				setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchSelectByBrowsing);
			return false;
		}

		//Make sure that all the segments but the last one (i.e. project + all
		//folders) exist - file doesn't have to exist. It may have happened that
		//some folder refactoring has been done since this path was last saved.
		//
		// The path will always be in format project/{folders}*/file - this
		// is controlled by the workspace location dialog and by
		// validatePath method when path has been entered manually.

		IPath pathToWorkspaceFile = new Path(wsPathText.getText());
		IStatus status = ResourcesPlugin.getWorkspace().validatePath(wsPathText.getText(), IResource.FILE);
		if (status.isOK()) {
			//Trim file name from path
			IPath containerPath = pathToWorkspaceFile.removeLastSegments(1);
			IResource container = ResourcesPlugin.getWorkspace().getRoot().findMember(containerPath);
			if (container == null) {
				if (wsRadio.getSelection())
					setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchSelectByBrowsing);
				return false;
			} else if (!container.isAccessible()) {
				if (wsRadio.getSelection())
					setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchProjectClosed);
				return false;
			} else if (ResourcesPlugin.getWorkspace().getRoot().getFolder(
					pathToWorkspaceFile).exists()) {
				setErrorMessage(UIText.GitCreatePatchWizard_WorkspacePatchFolderExists);
				return false;
			}
		} else {
			setErrorMessage(status.getMessage());
			return false;
		}
		return true;
	}

	/**
	 * @return answers a full path to a file system file or
	 *         <code>null</code> if the user selected to save the patch in
	 *         the clipboard.
	 */
	public File getFile() {
		if (pageValid && fsRadio.getSelection())
			return new File(fsPathText.getText().trim());
		if (pageValid && wsRadio.getSelection()) {
			final String filename= wsPathText.getText().trim();
			IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
			final IFile file = root.getFile(new Path(filename));
			return file.getLocation().toFile();
		}
		return null;
	}
}