package bndtools.editor.project;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.bndtools.core.ui.icons.Icons;
import org.bndtools.utils.jface.StrikeoutStyler;
import org.bndtools.utils.swt.AddRemoveButtonBarPart;
import org.bndtools.utils.swt.AddRemoveButtonBarPart.AddRemoveListener;
import org.bndtools.utils.swt.SWTConcurrencyUtil;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.ICheckStateProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StackLayout;
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.Table;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.forms.IMessageManager;
import org.eclipse.ui.forms.events.HyperlinkAdapter;
import org.eclipse.ui.forms.events.HyperlinkEvent;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Hyperlink;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.part.EditorPart;
import org.osgi.service.repository.Repository;

import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.build.model.clauses.HeaderClause;
import aQute.bnd.deployer.repository.AbstractIndexedRepo;
import aQute.bnd.exceptions.Exceptions;
import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Constants;
import aQute.bnd.repository.osgi.OSGiRepository;
import aQute.bnd.service.Actionable;
import aQute.bnd.service.RepositoryPlugin;
import bndtools.Plugin;
import bndtools.central.Central;
import bndtools.central.EclipseWorkspaceRepository;
import bndtools.editor.common.BndEditorPart;
import bndtools.editor.common.UpDownButtonBarPart;
import bndtools.editor.common.UpDownButtonBarPart.UpDownListener;
import bndtools.shared.URLDialog;

public class RepositorySelectionPart extends BndEditorPart implements IResourceChangeListener {

	private static final Image			projectImg			= Icons.image(IDE.SharedImages.IMG_OBJ_PROJECT);
	private static final Image			repoImg				= Icons.image(ISharedImages.IMG_OBJ_FOLDER);
	private final Object				MESSAGE_KEY			= new Object();
	private final EditorPart			editor;
	private final Button				btnStandaloneCheckbox;
	private final StackLayout			stackLayout;
	private final Control				saveToRefreshControl;
	private final CheckboxTableViewer	runReposViewer;
	private final UpDownButtonBarPart	upDownReposPart;
	private RepositoriesEditModel		repositories;
	private AddRemoveButtonBarPart		addRemove;
	private Set<IFile>					workspaceIndexFiles	= Collections.emptySet();

	/**
	 * Create the SectionPart.
	 *
	 * @param parent
	 * @param toolkit
	 * @param style
	 */
	public RepositorySelectionPart(final EditorPart editor, Composite parent, FormToolkit toolkit, int style) {
		super(parent, toolkit, style);
		this.editor = editor;

		Section section = getSection();
		section.setText("Repositories");

		GridLayout gl;
		GridData gd;

		// Create main container with -standalone checkbox
		Composite cmpMainContainer = toolkit.createComposite(section);
		section.setClient(cmpMainContainer);

		gl = new GridLayout(1, false);
		gl.marginWidth = 0;
		gl.marginHeight = 0;
		cmpMainContainer.setLayout(gl);

		// Create -standalone checkbox
		btnStandaloneCheckbox = toolkit.createButton(cmpMainContainer, "Standalone Mode", SWT.CHECK);
		btnStandaloneCheckbox.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
		btnStandaloneCheckbox.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				try {
					boolean standalone = btnStandaloneCheckbox.getSelection();
					repositories = repositories.setStandalone(standalone, model);
					markDirty();
					refreshFromModel();
				} catch (Exception e1) {
					throw Exceptions.duck(e1);
				}
			}
		});

		// Create stacked container for the three(!) possible contents
		Composite cmpStackContainer = toolkit.createComposite(cmpMainContainer);
		stackLayout = new StackLayout();
		stackLayout.marginHeight = 0;
		stackLayout.marginWidth = 0;
		cmpStackContainer.setLayout(stackLayout);
		gd = new GridData(SWT.FILL, SWT.TOP, true, false);
		gd.heightHint = 100;
		cmpStackContainer.setLayoutData(gd);

		// Create contents for the "save to refresh" control
		Composite cmpSaveToRefresh = toolkit.createComposite(cmpStackContainer);
		Hyperlink btnSaveToRefresh = toolkit.createHyperlink(cmpSaveToRefresh, "Save file to reload repositories...",
			SWT.NONE);
		saveToRefreshControl = cmpSaveToRefresh;
		stackLayout.topControl = saveToRefreshControl;
		gl = new GridLayout(1, true);
		btnSaveToRefresh.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
		cmpSaveToRefresh.setLayout(gl);
		btnSaveToRefresh.addHyperlinkListener(new HyperlinkAdapter() {
			@Override
			public void linkActivated(HyperlinkEvent ev) {
				IRunnableWithProgress runnable = monitor -> editor.doSave(monitor);
				try {
					editor.getSite()
						.getWorkbenchWindow()
						.run(false, false, runnable);
				} catch (InterruptedException e) {
					// let it go
				} catch (InvocationTargetException e) {
					ErrorDialog.openError(editor.getSite()
						.getShell(), "Error", null,
						new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Save error",
							Exceptions.unrollCause(e, InvocationTargetException.class)));
				}
			}
		});

		// Create contents for bnd layout workspace
		Composite cmpBndLayout = toolkit.createComposite(cmpStackContainer);
		gl = new GridLayout(2, false);
		gl.marginWidth = 0;
		gl.marginHeight = 0;
		cmpBndLayout.setLayout(gl);

		Table table = toolkit.createTable(cmpBndLayout,
			SWT.CHECK | SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
		gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		gd.widthHint = 50;
		table.setLayoutData(gd);

		runReposViewer = new CheckboxTableViewer(table);
		runReposViewer.setContentProvider(ArrayContentProvider.getInstance());
		runReposViewer.setCheckStateProvider(new ICheckStateProvider() {
			@Override
			public boolean isChecked(Object element) {
				return repositories.isIncluded((Repository) element);
			}

			@Override
			public boolean isGrayed(Object element) {
				return false;
			}
		});
		runReposViewer.addCheckStateListener(
			event -> repositories.setIncluded(event.getChecked(), (Repository) event.getElement()));

		upDownReposPart = new UpDownButtonBarPart(runReposViewer);
		Control upDownReposControl = upDownReposPart.createControl(cmpBndLayout, SWT.FLAT | SWT.VERTICAL);
		upDownReposControl.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, false, false));
		upDownReposPart.setEnabledUp(false);
		upDownReposPart.setEnabledDown(false);
		upDownReposPart.addListener(new UpDownListener() {
			@SuppressWarnings("unchecked")
			@Override
			public void changed(List<Object> order) {
				@SuppressWarnings("rawtypes")
				List l = order;
				repositories.setOrder(l);
				updateButtons();
				markDirty();
			}
		});
		addRemove = new AddRemoveButtonBarPart();
		Control addRemoveControl = addRemove.createControl(cmpBndLayout, SWT.FLAT | SWT.HORIZONTAL);
		addRemoveControl.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
		addRemove.setRemoveEnabled(false);
		addRemove.setAddEnabled(false);
		addRemove.addListener(new AddRemoveListener() {
			@Override
			public void addSelected() {
				doAddStandaloneLink();
			}

			@Override
			public void removeSelected() {
				doRemoveStandaloneLink();
			}
		});

		final Styler strikeoutStyler = new StrikeoutStyler(StyledString.QUALIFIER_STYLER,
			JFaceResources.getColorRegistry()
				.get(JFacePreferences.QUALIFIER_COLOR));

		runReposViewer.setLabelProvider(new StyledCellLabelProvider() {
			@Override
			public void update(ViewerCell cell) {
				Object element = cell.getElement();

				String label = null;
				Image image = null;
				Styler styler = null;

				Repository repo = (Repository) element;
				if (repo instanceof RepositoryPlugin) {
					label = ((RepositoryPlugin) repo).getName();
				} else {
					label = repo.toString();
				}
				image = repoImg;

				if (repo instanceof EclipseWorkspaceRepository) {
					image = projectImg;
				}

				boolean included = repositories.isIncluded(repo);
				styler = included ? null : strikeoutStyler;

				StyledString styledLabel = new StyledString(label, styler);
				cell.setText(styledLabel.getString());
				cell.setStyleRanges(styledLabel.getStyleRanges());
				cell.setImage(image);
			}

			@Override
			public String getToolTipText(Object element) {
				String tooltip = null;
				if (element instanceof Actionable) {
					try {
						tooltip = ((Actionable) element).tooltip(element);
					} catch (Exception e) {
						// ignore
					}
				}
				if (tooltip != null)
					return tooltip;

				if (repositories.isIncluded((Repository) element)) {
					tooltip = "Included for resolution.";
				} else {
					tooltip = "Excluded from resolution.";
				}
				return tooltip;
			}
		});

		runReposViewer.addSelectionChangedListener(event -> updateButtons());

		ResourcesPlugin.getWorkspace()
			.addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.POST_BUILD);

		stackLayout.topControl = cmpBndLayout;
		gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		cmpStackContainer.setLayoutData(gd);
		cmpMainContainer.layout(true, true);

		ColumnViewerToolTipSupport.enableFor(runReposViewer, ToolTip.NO_RECREATE);
	}

	void updateButtons() {
		boolean enableDown = false;
		boolean enableUp = false;
		boolean remove = false;
		boolean add = repositories.isStandalone();

		IStructuredSelection sel = (IStructuredSelection) runReposViewer.getSelection();
		if (!sel.isEmpty()) {
			@SuppressWarnings({
				"unchecked"
			})
			List<Repository> list = sel.toList();

			List<Repository> ordered = repositories.getOrdered();
			for (Repository r : list) {
				int index = ordered.indexOf(r);
				if (index > 0) {
					enableUp = true;
				}
				if (index < ordered.size() - 1) {
					enableDown = true;
				}

				if (repositories.isStandaloneRepository(r)) {
					remove = true;
				}
			}
		}

		upDownReposPart.setEnabledUp(enableUp);
		upDownReposPart.setEnabledDown(enableDown);
		addRemove.setRemoveEnabled(remove);
		addRemove.setAddEnabled(add);
	}

	private void reloadRepos() {
		final IMessageManager messages = getManagedForm().getMessageManager();
		messages.removeMessage(MESSAGE_KEY, runReposViewer.getControl());
		final List<Repository> allRepos = new ArrayList<>();

		try {
			allRepos.addAll(repositories.getOrdered());
			runReposViewer.setInput(allRepos);
		} catch (Exception e) {
			messages.addMessage(MESSAGE_KEY, "Repository List: Unable to load OSGi Repositories. " + e.getMessage(), e,
				IMessageProvider.ERROR, runReposViewer.getControl());

			// Load the repos and clear the error message if the Workspace is
			// initialised later.
			Central.onAnyWorkspace(
				workspace -> SWTConcurrencyUtil.execForControl(runReposViewer.getControl(), true, () -> {
					allRepos.clear();
					allRepos.addAll(workspace.getPlugins(Repository.class));
					runReposViewer.setInput(allRepos);
					messages.removeMessage(MESSAGE_KEY, runReposViewer.getControl());
				}));
		}
		updateButtons();
	}

	private void doAddStandaloneLink() {
		try {
			URLDialog dialog = new URLDialog(editor.getSite()
				.getShell(), "Add repository URL");
			if (dialog.open() == Window.OK) {
				URI location = dialog.getLocation();

				Attrs attrs = new Attrs();
				if (dialog.getName() != null)
					attrs.put("name", dialog.getName());

				HeaderClause clause = new HeaderClause(location.toString(), attrs);
				repositories.add(clause);
				refreshFromModel();
				markDirty();
			}
		} catch (Exception e) {
			throw Exceptions.duck(e);
		}
	}

	private void doRemoveStandaloneLink() {
		try {
			ISelection selection = runReposViewer.getSelection();
			if (selection.isEmpty())
				return;

			if (selection instanceof IStructuredSelection) {
				@SuppressWarnings("unchecked")
				List<Object> list = ((IStructuredSelection) selection).toList();
				for (Object o : list) {
					if (repositories.remove((Repository) o)) {
						refreshFromModel();
						markDirty();
					}
				}
			}
		} catch (Exception e) {
			throw Exceptions.duck(e);
		}
	}

	@Override
	protected String[] getProperties() {
		return new String[] {
			Constants.RUNREPOS, BndEditModel.PROP_WORKSPACE
		};
	}

	@Override
	protected void refreshFromModel() {
		repositories = new RepositoriesEditModel(model);
		boolean standalone = repositories.isStandalone();
		btnStandaloneCheckbox.setSelection(standalone);
		workspaceIndexFiles = standalone ? getWorkspaceIndexFiles() : Collections.emptySet();
		updateButtons();
		reloadRepos();
	}

	private Set<IFile> getWorkspaceIndexFiles() {
		Set<IFile> files = new HashSet<>();
		for (Repository repository : repositories.getOrdered()) {
			List<URI> locations = getRepoLocations(repository);

			for (URI u : locations) {
				IFile[] found = ResourcesPlugin.getWorkspace()
					.getRoot()
					.findFilesForLocationURI(u, IContainer.INCLUDE_HIDDEN | IContainer.INCLUDE_TEAM_PRIVATE_MEMBERS);
				for (IFile file : found) {
					files.add(file);
				}
			}
		}
		return files;
	}

	private List<URI> getRepoLocations(Repository repository) {
		List<URI> locations = new ArrayList<>();
		if (repository instanceof AbstractIndexedRepo) {
			try {
				locations.addAll(((AbstractIndexedRepo) repository).getIndexLocations());
			} catch (Exception e) {
				throw new RuntimeException(
					"An error occurred trying to determine whether a standalone repository had changed", e);
			}
		}

		if (repository instanceof OSGiRepository) {
			String loc = ((OSGiRepository) repository).getLocation();
			if (loc != null) {
				for (String l : loc.split(",")) {
					try {
						URI uri = new URI(l);
						if ("file".equalsIgnoreCase(uri.getScheme())) {
							locations.add(uri);
						}
					} catch (URISyntaxException use) {
						// Is this a straight file path?
						try {
							File f = new File(((OSGiRepository) repository).getRoot(), l);
							if (f.exists()) {
								locations.add(f.toURI());
							}
						} catch (Exception e) {
							// Just ignore this location
						}
					}
				}
			}
		}
		return locations;
	}

	@Override
	protected void commitToModel(boolean onSave) {
		repositories.commitToModel(model);
	}

	@Override
	public void dispose() {
		ResourcesPlugin.getWorkspace()
			.removeResourceChangeListener(this);
		super.dispose();
	}

	@Override
	public void resourceChanged(IResourceChangeEvent event) {
		boolean reposChanged = false;
		for (IFile file : workspaceIndexFiles) {
			if (event.getDelta()
				.findMember(file.getFullPath()) != null) {
				reposChanged = true;
				break;
			}
		}

		if (reposChanged) {

			getSection().getDisplay()
				.asyncExec(() -> {
					try {
						repositories.updateStandaloneWorkspace(model);
					} catch (Exception e) {
						throw Exceptions.duck(e);
					}
				});
		}
	}

}
