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.IStructuredSelection;
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(event -> {
			Object element = ((IStructuredSelection) viewer.getSelection()).getFirstElement();
			setTemplate(element instanceof Template ? (Template) element : null);
			getContainer().updateButtons();
		});
		viewer.addOpenListener(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(() -> {
							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(() -> 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(() -> {
						if (!control.isDisposed())
							control.setText(text);
					});
			}

			return Status.OK_STATUS;
		}
	}

}
