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.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
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.Event;
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(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent me) {
                String baseDirStr = txtBaseDir.getText();
                baseDir = baseDirStr.isEmpty() ? null : new File(baseDirStr);
                validate();
                updateInputs();
            }
        });
        txtResourcePattern.addModifyListener(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent 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(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent me) {
                updateOutputFileName();
                validate();
            }
        });
        Listener outputRadioListener = new Listener() {
            @Override
            public void handleEvent(Event 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(new Runnable() {
                    @Override
                    public void run() {
                        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;
        }
    }

}
