package org.bndtools.core.ui.wizards.index;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.PatternSyntaxException;

import org.bndtools.core.ui.icons.Icons;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.fieldassist.ControlDecoration;
import org.eclipse.jface.fieldassist.FieldDecorationRegistry;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
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.DirectoryDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.dialogs.ContainerSelectionDialog;

import bndtools.Plugin;

public class IndexerWizardPage extends WizardPage {

	private File				baseDir;
	private String				resourcePattern	= "**.jar";								//$NON-NLS-1$
	private List<Path>			inputPaths		= Collections.emptyList();
	private String				outputFileName	= "index.xml.gz";						//$NON-NLS-1$

	private IndexFormatStyle	outputStyle		= IndexFormatStyle.PRETTY_UNCOMPRESSED;

	private Text				txtBaseDir;
	private Text				txtResourcePattern;
	private Text				txtOutputPrefix;
	private Button				btnOutputCompressed;
	private Button				btnOutputPretty;
	private Label				lblOutputName;

	private SearchFilesJob		updateInputFilesJob;
	private TableViewer			vwrInputs;
	private Label				lblInputCount;

	private final Image			imgFile			= Icons.desc("file")					//$NON-NLS-1$
		.createImage();
	private final Image			imgWarning		= Icons.desc("warning")					//$NON-NLS-1$
		.createImage();
	private final Image			imgError		= Icons.desc("error")					//$NON-NLS-1$
		.createImage();

	public IndexerWizardPage() {
		super("index"); //$NON-NLS-1$

		setTitle(Messages.IndexerWizardPage_title);
		setDescription(Messages.IndexerWizardPage_description);
	}

	@Override
	public void createControl(Composite parent) {
		Composite composite = new Composite(parent, SWT.NULL);
		setControl(composite);

		// Create controls
		new Label(composite, SWT.NONE).setText(Messages.IndexerWizardPage_baseDir);
		txtBaseDir = new Text(composite, SWT.BORDER);

		newSpacer(composite);
		Composite cmpButtonBar = new Composite(composite, SWT.NONE);
		Button btnBrowseWorkspace = new Button(cmpButtonBar, SWT.PUSH);
		btnBrowseWorkspace.setText(Messages.IndexerWizardPage_browse);
		Button btnBrowseExternal = new Button(cmpButtonBar, SWT.PUSH);
		btnBrowseExternal.setText(Messages.IndexerWizardPage_browseExternal);

		new Label(composite, SWT.NONE).setText(Messages.IndexerWizardPage_resourcePattern);
		txtResourcePattern = new Text(composite, SWT.BORDER);
		ControlDecoration decorResourcePattern = new ControlDecoration(txtResourcePattern, SWT.LEFT | SWT.TOP,
			composite);
		decorResourcePattern.setImage(FieldDecorationRegistry.getDefault()
			.getFieldDecoration(FieldDecorationRegistry.DEC_INFORMATION)
			.getImage());
		decorResourcePattern.setMarginWidth(3);
		decorResourcePattern.setDescriptionText(Messages.IndexerWizardPage_resourcePatternHelp);
		decorResourcePattern.setShowHover(true);
		decorResourcePattern.setShowOnlyOnFocus(false);

		Label lblInputs = new Label(composite, SWT.NONE);
		lblInputs.setText(Messages.IndexerWizardPage_inputs);
		Table tblInputs = new Table(composite, SWT.MULTI | SWT.BORDER);
		vwrInputs = new TableViewer(tblInputs);
		vwrInputs.setContentProvider(ArrayContentProvider.getInstance());
		vwrInputs.setLabelProvider(new StyledCellLabelProvider() {
			@Override
			public void update(ViewerCell cell) {
				Object elem = cell.getElement();
				if (elem instanceof Path) {
					cell.setText(elem.toString());
					cell.setImage(imgFile);
				} else if (elem instanceof IStatus) {
					IStatus status = (IStatus) elem;
					cell.setText(status.getMessage());
					if (status.getSeverity() == IStatus.ERROR)
						cell.setImage(imgError);
					else if (status.getSeverity() == IStatus.WARNING)
						cell.setImage(imgWarning);
				}
			}
		});
		newSpacer(composite);
		lblInputCount = new Label(composite, SWT.NONE);

		Label lblOutputs = new Label(composite, SWT.NONE);
		lblOutputs.setText(Messages.IndexerWizardPage_output);
		Group grpOutput = new Group(composite, SWT.NONE);

		new Label(grpOutput, SWT.NONE).setText(Messages.IndexerWizardPage_prefix);
		txtOutputPrefix = new Text(grpOutput, SWT.BORDER);

		newSpacer(grpOutput);
		btnOutputPretty = new Button(grpOutput, SWT.RADIO);
		btnOutputPretty.setText(Messages.IndexerWizardPage_prettyPrint);

		newSpacer(grpOutput);
		btnOutputCompressed = new Button(grpOutput, SWT.RADIO);
		btnOutputCompressed.setText(Messages.IndexerWizardPage_compressed);

		newSpacer(grpOutput);
		lblOutputName = new Label(grpOutput, SWT.NONE);

		// LAYOUT
		GridLayout gl;
		GridData gd;
		gl = new GridLayout(2, false);
		gl.horizontalSpacing = 10;
		composite.setLayout(gl);

		txtBaseDir.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		cmpButtonBar.setLayoutData(new GridData(SWT.RIGHT, SWT.FILL, false, false));
		gl = new GridLayout(2, false);
		gl.marginHeight = 0;
		gl.marginWidth = 0;
		cmpButtonBar.setLayout(gl);
		btnBrowseWorkspace.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
		btnBrowseExternal.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));

		txtResourcePattern.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));

		lblInputs.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
		gd = new GridData(SWT.FILL, SWT.FILL, true, false);
		gd.heightHint = 120;
		gd.widthHint = 100;
		tblInputs.setLayoutData(gd);
		lblInputCount.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false));

		lblOutputs.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
		grpOutput.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
		grpOutput.setLayout(new GridLayout(2, false));
		txtOutputPrefix.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		btnOutputCompressed.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		btnOutputPretty.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
		lblOutputName.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));

		// LOAD DATA
		if (baseDir != null)
			txtBaseDir.setText(baseDir.getAbsolutePath());
		if (resourcePattern != null)
			txtResourcePattern.setText(resourcePattern);
		txtOutputPrefix.setText("index"); //$NON-NLS-1$
		btnOutputCompressed.setSelection(outputStyle == IndexFormatStyle.COMPRESSED);
		btnOutputPretty.setSelection(outputStyle == IndexFormatStyle.PRETTY_UNCOMPRESSED);
		updateOutputFileName();
		updateInputs();

		// LISTENERS
		txtBaseDir.addModifyListener(me -> {
			String baseDirStr = txtBaseDir.getText();
			baseDir = baseDirStr.isEmpty() ? null : new File(baseDirStr);
			validate();
			updateInputs();
		});
		txtResourcePattern.addModifyListener(me -> {
			resourcePattern = txtResourcePattern.getText();
			updateInputs();
		});
		btnBrowseWorkspace.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				IWorkspaceRoot root = ResourcesPlugin.getWorkspace()
					.getRoot();
				ContainerSelectionDialog containerDlg = new ContainerSelectionDialog(getShell(), root, false,
					Messages.IndexerWizardPage_selectBaseDir);
				containerDlg.showClosedProjects(false);
				if (containerDlg.open() == Window.OK) {
					Object[] selection = containerDlg.getResult();
					if (selection != null && selection.length > 0) {
						IPath workspacePath = (IPath) selection[0];
						IResource resource = root.findMember(workspacePath);
						if (resource == null)
							MessageDialog.openError(getShell(), "Error",
								"Could not find resource for path " + workspacePath);
						else
							txtBaseDir.setText(resource.getLocation()
								.toString());
					}
				}
			}
		});
		btnBrowseExternal.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				DirectoryDialog dirDialog = new DirectoryDialog(getShell());
				dirDialog.setMessage(Messages.IndexerWizardPage_selectBaseDir);
				String selectedPath = dirDialog.open();
				if (selectedPath != null)
					txtBaseDir.setText(selectedPath);
			}
		});
		txtOutputPrefix.addModifyListener(me -> {
			updateOutputFileName();
			validate();
		});
		Listener outputRadioListener = ev -> {
			outputStyle = btnOutputCompressed.getSelection() ? IndexFormatStyle.COMPRESSED
				: IndexFormatStyle.PRETTY_UNCOMPRESSED;
			updateOutputFileName();
			validate();
		};
		btnOutputCompressed.addListener(SWT.Selection, outputRadioListener);
		btnOutputPretty.addListener(SWT.Selection, outputRadioListener);

		validate();
	}

	private Label newSpacer(Composite composite) {
		// Wrapping this in a method makes it easier to ignore the unused
		// warning
		return new Label(composite, SWT.NONE); // spacer
	}

	private void updateInputs() {
		if (updateInputFilesJob != null) {
			updateInputFilesJob.cancel();
		}
		inputPaths = Collections.emptyList();
		setPageComplete(false);
		vwrInputs.setInput(inputPaths);
		lblInputCount.setText(String.format("%d resources found", inputPaths.size()));
		lblInputCount.getParent()
			.layout(new Control[] {
				lblInputCount
			});
		final String resourcePattern = this.resourcePattern;
		final Display display = getShell().getDisplay();

		if (baseDir == null)
			return;
		updateInputFilesJob = new SearchFilesJob(baseDir, resourcePattern);
		updateInputFilesJob.addJobChangeListener(new JobChangeAdapter() {
			@Override
			public void done(IJobChangeEvent event) {
				final IStatus status = updateInputFilesJob.getSearchResult();
				display.syncExec(() -> {
					inputPaths = updateInputFilesJob.getPaths();
					if (inputPaths == null)
						inputPaths = Collections.emptyList();
					if (status.isOK())
						vwrInputs.setInput(updateInputFilesJob.getPaths());
					else
						// The error/warning status is displayed in the table
						// instead of the path list
						vwrInputs.setInput(Collections.singleton(status));
					lblInputCount.setText(String.format("%d resources found", inputPaths.size()));
					lblInputCount.getParent()
						.layout(new Control[] {
							lblInputCount
					});
					validate();
				});
			}
		});
		updateInputFilesJob.setSystem(true);
		updateInputFilesJob.schedule(500);
		vwrInputs.setInput(Collections
			.singleton(new Status(IStatus.INFO, Plugin.PLUGIN_ID, 0, Messages.IndexerWizardPage_checking, null)));
		lblInputCount.setText("...");
		lblInputCount.getParent()
			.layout(new Control[] {
				lblInputCount
			});
	}

	private void updateOutputFileName() {
		String prefix = txtOutputPrefix.getText();
		String suffix = outputStyle == IndexFormatStyle.COMPRESSED ? "xml.gz" : "xml"; //$NON-NLS-1$ //$NON-NLS-2$

		StringBuilder sb = new StringBuilder().append(prefix);
		if (sb.charAt(sb.length() - 1) != '.')
			sb.append('.');
		sb.append(suffix);
		outputFileName = sb.toString();
		lblOutputName.setText(String.format(Messages.IndexerWizardPage_outputFileMessage, outputFileName));
	}

	private void validate() {
		String warning = null;

		if (baseDir == null) {
			setErrorMessage(null);
			setPageComplete(false);
			return;
		}

		if (!baseDir.exists()) {
			setErrorMessage(Messages.IndexerWizardPage_error_noSuchDir + baseDir.getAbsolutePath());
			setPageComplete(false);
			return;
		}

		if (!baseDir.isDirectory()) {
			setErrorMessage(Messages.IndexerWizardPage_error_notDir + baseDir.getAbsolutePath());
			setPageComplete(false);
			return;
		}

		try {
			FileSystem fs = FileSystems.getDefault();
			fs.getPathMatcher("glob:" + resourcePattern); //$NON-NLS-1$
		} catch (Exception e) {
			setErrorMessage(e.getMessage());
			setPageComplete(false);
			return;
		}

		if (outputFileName.indexOf('/') > -1) {
			setErrorMessage(Messages.IndexerWizardPage_error_noSlashes);
			setPageComplete(false);
		}
		File outputFile = new File(baseDir, outputFileName);
		if (outputFile.exists()) {
			if (outputFile.isFile())
				warning = Messages.IndexerWizardPage_warn_fileOverwrite + outputFile.getAbsolutePath();
			else {
				setErrorMessage(Messages.IndexerWizardPage_error_fileExists + outputFile.getAbsolutePath());
				setPageComplete(false);
			}
		}

		setErrorMessage(null);
		setMessage(warning, WARNING);
		setPageComplete(inputPaths != null && !inputPaths.isEmpty());
	}

	public void setBaseDir(File baseDir) {
		this.baseDir = baseDir;
		if (isControlCreated() && !getControl().isDisposed())
			txtBaseDir.setText(baseDir.getAbsolutePath());
	}

	public File getBaseDir() {
		return baseDir;
	}

	public void setResourcePattern(String pattern) {
		this.resourcePattern = pattern;
		if (isControlCreated() && !getControl().isDisposed())
			txtResourcePattern.setText(resourcePattern);
	}

	public String getResourcePattern() {
		return resourcePattern;
	}

	public IndexFormatStyle getOutputStyle() {
		return outputStyle;
	}

	public File getOutputFile() {
		return new File(baseDir, outputFileName);
	}

	public List<Path> getInputPaths() {
		try {
			if (updateInputFilesJob != null)
				updateInputFilesJob.join();
			return inputPaths;
		} catch (InterruptedException e) {
			return Collections.emptyList();
		}
	}

	@Override
	public void dispose() {
		super.dispose();
		imgFile.dispose();
		imgError.dispose();
		imgWarning.dispose();
	}

	private static class SearchFilesJob extends Job {

		private final File		baseDir;
		private final String	resourcePattern;
		private IStatus			searchResult	= Status.OK_STATUS;
		private List<Path>		paths			= Collections.emptyList();

		private SearchFilesJob(File baseDir, String resourcePattern) {
			super(Messages.IndexerWizardPage_updateInputs);
			if (baseDir == null)
				throw new NullPointerException("baseDir cannot be null"); //$NON-NLS-1$
			this.baseDir = baseDir;
			this.resourcePattern = resourcePattern;
			setSystem(true);
			setUser(false);
		}

		@Override
		protected IStatus run(IProgressMonitor monitor) {
			try {
				final List<Path> result = new LinkedList<>();

				// Collect the files
				final Path basePath = baseDir.toPath();
				FileSystem fs = FileSystems.getDefault();
				final PathMatcher matcher = fs.getPathMatcher("glob:" + resourcePattern); //$NON-NLS-1$
				Files.walkFileTree(basePath, new SimpleFileVisitor<Path>() {
					@Override
					public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
						Path relative = basePath.relativize(path);
						if (matcher.matches(relative))
							result.add(relative);
						return FileVisitResult.CONTINUE;
					}
				});
				if (result.isEmpty())
					searchResult = new Status(IStatus.WARNING, Plugin.PLUGIN_ID, 0,
						Messages.IndexerWizardPage_warn_noMatchingFiles, null);
				else
					searchResult = Status.OK_STATUS;
				paths = result;
			} catch (PatternSyntaxException e) {
				searchResult = new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0,
					Messages.IndexerWizardPage_error_invalidPattern + e.getMessage(), e);
			} catch (Exception e) {
				searchResult = new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0,
					Messages.IndexerWizardPage_error_fileSearch, e);
			}

			return Status.OK_STATUS;
		}

		public IStatus getSearchResult() {
			return searchResult;
		}

		public List<Path> getPaths() {
			return paths;
		}
	}

}
