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

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.bndtools.templating.Category;
import org.bndtools.templating.Template;
import org.bndtools.templating.TemplateLoader;
import org.bndtools.utils.jface.ProgressRunner;
import org.eclipse.core.runtime.ILog;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.JFaceColors;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.wizard.IWizardPage;
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.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.FormText;
import org.eclipse.ui.forms.widgets.Hyperlink;
import org.eclipse.ui.forms.widgets.ScrolledFormText;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentConstants;
import org.osgi.util.promise.Promise;

import aQute.bnd.osgi.Processor;
import aQute.lib.exceptions.Exceptions;
import aQute.lib.io.IO;
import aQute.libg.tuple.Pair;
import bndtools.Plugin;

public class TemplateSelectionWizardPage extends WizardPage {

    public static final String PROP_TEMPLATE = "template";

    private static final String NO_HELP_CONTENT = "<form>No help content available</form>";

    private final ILog log = Plugin.getDefault()
        .getLog();

    private final PropertyChangeSupport propSupport = new PropertyChangeSupport(this);
    private final String templateType;
    private final Template emptyTemplate;

    private final Map<Template, Image> loadedImages = new IdentityHashMap<>();

    private Tree tree;
    private TreeViewer viewer;
    private RepoTemplateContentProvider contentProvider;
    private final LatestTemplateFilter latestFilter = new LatestTemplateFilter();
    private Button btnLatestOnly;
    private ScrolledFormText txtDescription;
    private Image defaultTemplateImage;

    private Template selected = null;

    private boolean shown = false;

    public TemplateSelectionWizardPage(String pageName, String templateType, Template emptyTemplate) {
        super(pageName);
        this.templateType = templateType;
        this.emptyTemplate = emptyTemplate;
    }

    @Override
    public void setVisible(boolean visible) {
        super.setVisible(visible);
        if (visible && !shown) {
            shown = true;
            loadTemplates();
        }
    }

    /*
     * Don't allow wizard to complete before this page is shown
     */
    @Override
    public boolean isPageComplete() {
        return shown && selected != null && super.isPageComplete();
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propSupport.addPropertyChangeListener(listener);
    }

    public void removePropertyChangeListener(PropertyChangeListener listener) {
        propSupport.removePropertyChangeListener(listener);
    }

    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        propSupport.addPropertyChangeListener(propertyName, listener);
    }

    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        propSupport.removePropertyChangeListener(propertyName, listener);
    }

    @Override
    public void createControl(Composite parent) {
        setImageDescriptor(Plugin.imageDescriptorFromPlugin("icons/bndtools-wizban.png")); //$NON-NLS-1$

        GridData gd;

        Composite composite = new Composite(parent, SWT.NULL);
        setControl(composite);

        composite.setLayout(new GridLayout(1, false));

        Control headerControl = createHeaderControl(composite);
        if (headerControl != null)
            headerControl.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));

        new Label(composite, SWT.NONE).setText("Select Template:");

        tree = new Tree(composite, SWT.BORDER | SWT.FULL_SELECTION);
        gd = new GridData(SWT.FILL, SWT.FILL, true, true);
        gd.heightHint = 150;
        tree.setLayoutData(gd);

        defaultTemplateImage = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "icons/template.gif")
            .createImage(parent.getDisplay());

        viewer = new TreeViewer(tree);
        contentProvider = new RepoTemplateContentProvider(false);
        viewer.setContentProvider(contentProvider);
        viewer.setLabelProvider(new RepoTemplateLabelProvider(loadedImages, defaultTemplateImage));
        viewer.addFilter(latestFilter);
        setTemplates(emptyTemplate != null ? Collections.singletonList(emptyTemplate) : Collections.<Template> emptyList());

        btnLatestOnly = new Button(composite, SWT.CHECK);
        btnLatestOnly.setText("Show latest versions only");
        btnLatestOnly.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false));
        btnLatestOnly.setSelection(true);

        new Label(composite, SWT.NONE).setText("Description:");

        Composite cmpDescription = new Composite(composite, SWT.BORDER);
        cmpDescription.setBackground(tree.getBackground());

        txtDescription = new ScrolledFormText(cmpDescription, SWT.V_SCROLL | SWT.H_SCROLL, false);
        FormText formText = new FormText(txtDescription, SWT.NO_FOCUS);
        txtDescription.setFormText(formText);
        txtDescription.setBackground(tree.getBackground());
        formText.setBackground(tree.getBackground());
        formText.setForeground(tree.getForeground());
        formText.setFont("fixed", JFaceResources.getTextFont());
        formText.setFont("italic", JFaceResources.getFontRegistry()
            .getItalic(""));

        GridData gd_cmpDescription = new GridData(SWT.FILL, SWT.FILL, true, true);
        gd_cmpDescription.heightHint = 25;
        cmpDescription.setLayoutData(gd_cmpDescription);

        GridLayout layout_cmpDescription = new GridLayout(1, false);
        cmpDescription.setLayout(layout_cmpDescription);

        GridData gd_txtDescription = new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1);
        gd_txtDescription.heightHint = 25;
        txtDescription.setLayoutData(gd_txtDescription);

        Hyperlink linkRetina = new Hyperlink(composite, SWT.NONE);
        linkRetina.setText("Why is this text blurred?");
        linkRetina.setUnderlined(true);
        linkRetina.setForeground(JFaceColors.getHyperlinkText(getShell().getDisplay()));
        linkRetina.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, false));

        viewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                Object element = ((IStructuredSelection) viewer.getSelection()).getFirstElement();
                setTemplate(element instanceof Template ? (Template) element : null);
                getContainer().updateButtons();
            }
        });
        viewer.addOpenListener(new IOpenListener() {
            @Override
            public void open(OpenEvent event) {
                Object element = ((IStructuredSelection) viewer.getSelection()).getFirstElement();
                setTemplate(element instanceof Template ? (Template) element : null);
                getContainer().updateButtons();
                IWizardPage nextPage = getNextPage();
                if (nextPage != null && selected != null)
                    getContainer().showPage(nextPage);
            }
        });
        btnLatestOnly.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                boolean latestOnly = btnLatestOnly.getSelection();
                if (latestOnly)
                    viewer.addFilter(latestFilter);
                else
                    viewer.removeFilter(latestFilter);
            }
        });
        linkRetina.addHyperlinkListener(new HyperlinkAdapter() {
            @Override
            public void linkActivated(HyperlinkEvent ev) {
                try {
                    IWorkbenchBrowserSupport browser = PlatformUI.getWorkbench()
                        .getBrowserSupport();
                    browser.getExternalBrowser()
                        .openURL(new URL("https://github.com/bndtools/bnd/wiki/Blurry-Form-Text-on-High-Resolution-Displays"));
                } catch (Exception e) {
                    log.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Browser open error", e));
                }
            }
        });
        txtDescription.getFormText()
            .addHyperlinkListener(new HyperlinkAdapter() {
                @Override
                public void linkActivated(HyperlinkEvent ev) {
                    try {
                        PlatformUI.getWorkbench()
                            .getBrowserSupport()
                            .getExternalBrowser()
                            .openURL(new URL((String) ev.getHref()));
                    } catch (Exception ex) {
                        log.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Browser open error", ex));
                    }
                }
            });
    }

    /**
     * Can be overridden to provide a control that will be placed at the top of the page.
     */
    protected Control createHeaderControl(@SuppressWarnings("unused") Composite parent) {
        return null;
    }

    private class LoadTemplatesJob implements IRunnableWithProgress {

        private final Shell shell;
        private final String originalMessage;

        private final BundleContext context = FrameworkUtil.getBundle(LoadTemplatesJob.class)
            .getBundleContext();

        public LoadTemplatesJob(Shell shell, String originalMessage) {
            this.shell = shell;
            this.originalMessage = originalMessage;
        }

        @Override
        public void run(IProgressMonitor progress) throws InvocationTargetException {
            SubMonitor monitor = SubMonitor.convert(progress);
            try {
                final Set<Template> templates = new LinkedHashSet<>();

                // Fire all the template loaders and get their promises
                List<ServiceReference<TemplateLoader>> templateLoaderSvcRefs = new ArrayList<>(context.getServiceReferences(TemplateLoader.class, null));
                monitor.beginTask("Loading templates...", templateLoaderSvcRefs.size());
                Collections.sort(templateLoaderSvcRefs);
                List<Pair<String, Promise<? extends Collection<Template>>>> promises = new LinkedList<>();
                for (ServiceReference<TemplateLoader> templateLoaderSvcRef : templateLoaderSvcRefs) {
                    String label = (String) templateLoaderSvcRef.getProperty(Constants.SERVICE_DESCRIPTION);
                    if (label == null)
                        label = (String) templateLoaderSvcRef.getProperty(ComponentConstants.COMPONENT_NAME);
                    if (label == null)
                        label = String.format("Template Loader service ID " + templateLoaderSvcRef.getProperty(Constants.SERVICE_ID));

                    TemplateLoader templateLoader = context.getService(templateLoaderSvcRef);

                    Promise<? extends Collection<Template>> promise = templateLoader.findTemplates(templateType, new Processor());
                    promise.onResolve(() -> context.ungetService(templateLoaderSvcRef));
                    promises.add(new Pair<String, Promise<? extends Collection<Template>>>(label, promise));
                }

                // Force the promises in sequence
                for (Pair<String, Promise<? extends Collection<Template>>> namedPromise : promises) {
                    String name = namedPromise.getFirst();
                    SubMonitor childMonitor = monitor.newChild(1, SubMonitor.SUPPRESS_NONE);
                    childMonitor.beginTask(name, 1);
                    try {
                        Throwable failure = namedPromise.getSecond()
                            .getFailure();
                        if (failure != null)
                            Plugin.getDefault()
                                .getLog()
                                .log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Failed to load from template loader: " + name, failure));
                        else {
                            Collection<Template> loadedTemplates = namedPromise.getSecond()
                                .getValue();
                            templates.addAll(loadedTemplates);
                        }
                    } catch (InterruptedException e) {
                        Plugin.getDefault()
                            .getLog()
                            .log(new Status(IStatus.WARNING, Plugin.PLUGIN_ID, 0, "Interrupted while loading from template loader: " + name, e));
                    }
                }

                // Add empty template if provided
                if (emptyTemplate != null)
                    templates.add(emptyTemplate);

                // Display results
                Control control = viewer.getControl();
                if (control != null && !control.isDisposed()) {
                    control.getDisplay()
                        .asyncExec(new Runnable() {
                            @Override
                            public void run() {
                                setTemplates(templates);
                                IconLoaderJob iconLoaderJob = new IconLoaderJob(templates, viewer, loadedImages, 5);
                                iconLoaderJob.setSystem(true);
                                iconLoaderJob.schedule(0);
                            }
                        });
                }

            } catch (InvalidSyntaxException ex) {
                throw new InvocationTargetException(ex);
            } finally {
                // Restore the original message to the page
                if (!shell.isDisposed())
                    shell.getDisplay()
                        .asyncExec(new Runnable() {
                            @Override
                            public void run() {
                                setMessage(originalMessage);
                            }
                        });
                if (progress != null)
                    progress.done();
            }
        }

    }

    protected void loadTemplates() {
        final String oldMessage = getMessage();
        final Shell shell = getShell();

        setMessage("Loading templates...");
        try {
            ProgressRunner.execute(true, new LoadTemplatesJob(shell, oldMessage), getContainer(), getContainer().getShell()
                .getDisplay());
        } catch (InvocationTargetException e) {
            ErrorDialog.openError(getShell(), "Error", null, new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Error loading templates.", Exceptions.unrollCause(e, InvocationTargetException.class)));
        }
    }

    @Override
    public void dispose() {
        super.dispose();

        if (!defaultTemplateImage.isDisposed())
            defaultTemplateImage.dispose();

        for (Entry<Template, Image> entry : loadedImages.entrySet()) {
            Image img = entry.getValue();
            if (!img.isDisposed())
                img.dispose();
        }

        if (selected != null) {
            try {
                selected.close();
            } catch (IOException e) {
                Plugin.getDefault()
                    .getLog()
                    .log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Problem cleaning up template content", e));
            }
        }
    }

    private void setTemplates(final Collection<Template> templates) {
        viewer.setInput(templates);
        viewer.expandAll();

        Template templateToSelect = null;

        if (viewer.getFilters().length == 0) {
            templateToSelect = contentProvider.getFirstTemplate();
        } else {
            for (Object element : contentProvider.getElements(null)) {
                if (element instanceof Category) {
                    Object[] filteredTemplates = latestFilter.filter(viewer, element, contentProvider.getChildren(element));
                    if (filteredTemplates.length > 0) {
                        templateToSelect = (Template) filteredTemplates[0];
                        break;
                    }
                } else {
                    templateToSelect = (Template) element;
                    break;
                }
            }
        }

        if (templateToSelect == null) {
            return;
        }

        viewer.setSelection(new StructuredSelection(templateToSelect));
    }

    public void setTemplate(final Template template) {
        Template old = this.selected;
        this.selected = template;
        propSupport.firePropertyChange(PROP_TEMPLATE, old, template);

        if (template != null) {
            txtDescription.setText(String.format("<form>Loading help content for template '%s'...</form>", template.getName()));
            Job updateDescJob = new UpdateDescriptionJob(template, txtDescription);
            updateDescJob.setSystem(true);
            updateDescJob.schedule();
        } else {
            txtDescription.setText(NO_HELP_CONTENT);
        }
    }

    public Template getTemplate() {
        return selected;
    }

    private final class UpdateDescriptionJob extends Job {
        private final Template template;
        private final ScrolledFormText control;

        private UpdateDescriptionJob(Template template, ScrolledFormText control) {
            super("update description");
            this.template = template;
            this.control = control;
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            String tmp = NO_HELP_CONTENT;
            if (template != null) {
                URI uri = template.getHelpContent();
                if (uri != null) {
                    try {
                        URLConnection conn = uri.toURL()
                            .openConnection();
                        conn.setUseCaches(false);
                        tmp = IO.collect(conn.getInputStream());
                    } catch (IOException e) {
                        log.log(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Error loading template help content.", e));
                    }
                }
            }

            final String text = tmp;
            if (control != null && !control.isDisposed()) {
                control.getDisplay()
                    .asyncExec(new Runnable() {
                        @Override
                        public void run() {
                            if (!control.isDisposed())
                                control.setText(text);
                        }
                    });
            }

            return Status.OK_STATUS;
        }
    }

}
