package bndtools.editor.project;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;

import org.bndtools.core.ui.wizards.jpm.AddJpmDependenciesWizard;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.dialogs.ErrorDialog;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.MessageDialogWithToggle;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.ViewerDropAdapter;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.dnd.URLTransfer;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.editor.IFormPage;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.part.ResourceTransfer;

import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.build.model.clauses.VersionedClause;
import aQute.bnd.header.Attrs;
import aQute.bnd.osgi.Constants;
import aQute.bnd.service.repository.SearchableRepository.ResourceDescriptor;
import aQute.bnd.version.Version;
import bndtools.Plugin;
import bndtools.editor.common.BndEditorPart;
import bndtools.model.clauses.VersionedClauseLabelProvider;
import bndtools.model.repo.DependencyPhase;
import bndtools.model.repo.ProjectBundle;
import bndtools.model.repo.RepositoryBundle;
import bndtools.model.repo.RepositoryBundleUtils;
import bndtools.model.repo.RepositoryBundleVersion;
import bndtools.model.repo.RepositoryResourceElement;
import bndtools.preferences.BndPreferences;
import bndtools.types.Pair;
import bndtools.wizards.repo.RepoBundleSelectionWizard;
import bndtools.wizards.workspace.AddFilesToRepositoryWizard;

public abstract class RepositoryBundleSelectionPart extends BndEditorPart implements PropertyChangeListener {
	private final String			propertyName;
	private final DependencyPhase	phase;

	private Table					table;
	protected TableViewer			viewer;

	private BndEditModel			model;
	protected List<VersionedClause>	bundles;
	protected ToolItem				removeItemTool;

	protected RepositoryBundleSelectionPart(String propertyName, DependencyPhase phase, Composite parent,
		FormToolkit toolkit, int style) {
		super(parent, toolkit, style);
		this.propertyName = propertyName;
		this.phase = phase;
		createSection(getSection(), toolkit);
	}

	@Override
	protected String[] getProperties() {
		return new String[] {
			propertyName
		};
	}

	protected ToolItem createAddItemTool(ToolBar toolbar) {
		ToolItem tool = new ToolItem(toolbar, SWT.PUSH);

		tool.setImage(PlatformUI.getWorkbench()
			.getSharedImages()
			.getImage(ISharedImages.IMG_OBJ_ADD));
		tool.setToolTipText("Add Bundle");
		tool.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				doAdd();
			}
		});

		return tool;
	}

	protected ToolItem createRemoveItemTool(ToolBar toolbar) {
		ToolItem tool = new ToolItem(toolbar, SWT.PUSH);

		tool.setImage(PlatformUI.getWorkbench()
			.getSharedImages()
			.getImage(ISharedImages.IMG_TOOL_DELETE));
		tool.setDisabledImage(PlatformUI.getWorkbench()
			.getSharedImages()
			.getImage(ISharedImages.IMG_TOOL_DELETE_DISABLED));
		tool.setToolTipText("Remove");
		tool.setEnabled(false);
		tool.addSelectionListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				doRemove();
			}
		});

		return tool;
	}

	protected ToolItem getRemoveItemTool() {
		return removeItemTool;
	}

	protected void fillToolBar(ToolBar toolbar) {
		createAddItemTool(toolbar);
		this.removeItemTool = createRemoveItemTool(toolbar);
	}

	protected IBaseLabelProvider getLabelProvider() {
		return new VersionedClauseLabelProvider();
	}

	void createSection(Section section, FormToolkit toolkit) {
		// Toolbar buttons
		ToolBar toolbar = new ToolBar(section, SWT.FLAT);
		section.setTextClient(toolbar);
		fillToolBar(toolbar);

		Composite composite = toolkit.createComposite(section);
		section.setClient(composite);

		table = toolkit.createTable(composite, SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER | SWT.H_SCROLL);

		viewer = new TableViewer(table);
		viewer.setContentProvider(new ArrayContentProvider());
		viewer.setLabelProvider(getLabelProvider());

		// Listeners
		viewer.addSelectionChangedListener(event -> {
			ToolItem remove = getRemoveItemTool();
			if (remove != null)
				remove.setEnabled(isRemovable(event.getSelection()));
		});
		ViewerDropAdapter dropAdapter = new ViewerDropAdapter(viewer) {
			@Override
			public void dragEnter(DropTargetEvent event) {
				super.dragEnter(event);
				event.detail = DND.DROP_COPY;
			}

			@Override
			public boolean validateDrop(Object target, int operation, TransferData transferType) {
				if (FileTransfer.getInstance()
					.isSupportedType(transferType))
					return true;

				if (ResourceTransfer.getInstance()
					.isSupportedType(transferType))
					return true;

				if (URLTransfer.getInstance()
					.isSupportedType(transferType))
					return true;

				ISelection selection = LocalSelectionTransfer.getTransfer()
					.getSelection();
				if (selection.isEmpty() || !(selection instanceof IStructuredSelection)) {
					return false;
				}

				Iterator<?> iterator = ((IStructuredSelection) selection).iterator();
				while (iterator.hasNext()) {
					if (!selectionIsDroppable(iterator.next())) {
						return false;
					}
				}
				return true;
			}

			private boolean selectionIsDroppable(Object element) {
				return element instanceof RepositoryBundle || element instanceof RepositoryBundleVersion
					|| element instanceof ProjectBundle || element instanceof RepositoryResourceElement;
			}

			@Override
			public boolean performDrop(Object data) {
				TransferData transfer = getCurrentEvent().currentDataType;

				if (URLTransfer.getInstance()
					.isSupportedType(transfer)) {
					String urlStr = (String) URLTransfer.getInstance()
						.nativeToJava(transfer);
					return handleURLDrop(urlStr);
				} else if (data instanceof String[]) {
					return handleFileNameDrop((String[]) data);
				} else if (data instanceof IResource[]) {
					return handleResourceDrop((IResource[]) data);
				} else {
					return handleSelectionDrop();
				}
			}

			private boolean handleResourceDrop(IResource[] resources) {
				File[] files = new File[resources.length];
				for (int i = 0; i < resources.length; i++) {
					files[i] = resources[i].getLocation()
						.toFile();
				}
				return handleFileDrop(files);
			}

			private boolean handleFileNameDrop(String[] paths) {
				File[] files = new File[paths.length];
				for (int i = 0; i < paths.length; i++) {
					files[i] = new File(paths[i]);
				}
				return handleFileDrop(files);
			}

			private boolean handleFileDrop(File[] files) {
				if (files.length > 0) {
					BndPreferences prefs = new BndPreferences();
					boolean hideWarning = prefs.getHideWarningExternalFile();
					if (!hideWarning) {
						MessageDialogWithToggle dialog = MessageDialogWithToggle.openWarning(getSection().getShell(),
							"Add External Files",
							"External files cannot be directly added to a project, they must be added to a local repository first.",
							"Do not show this warning again.", false, null, null);
						if (Window.CANCEL == dialog.getReturnCode())
							return false;
						if (dialog.getToggleState()) {
							prefs.setHideWarningExternalFile(true);
						}
					}

					AddFilesToRepositoryWizard wizard = new AddFilesToRepositoryWizard(null, files);
					WizardDialog dialog = new WizardDialog(getSection().getShell(), wizard);
					if (Window.OK == dialog.open()) {
						List<Pair<String, String>> addingBundles = wizard.getSelectedBundles();
						List<VersionedClause> addingClauses = new ArrayList<>(addingBundles.size());

						for (Pair<String, String> addingBundle : addingBundles) {
							Attrs attribs = new Attrs();
							attribs.put(Constants.VERSION_ATTRIBUTE, addingBundle.getSecond());
							addingClauses.add(new VersionedClause(addingBundle.getFirst(), attribs));
						}

						handleAdd(addingClauses);
					}
					return true;
				}
				return false;
			}

			private boolean handleSelectionDrop() {
				ISelection selection = LocalSelectionTransfer.getTransfer()
					.getSelection();
				if (selection.isEmpty() || !(selection instanceof IStructuredSelection)) {
					return false;
				}
				List<VersionedClause> adding = new LinkedList<>();
				Iterator<?> iterator = ((IStructuredSelection) selection).iterator();
				while (iterator.hasNext()) {
					Object item = iterator.next();
					if (item instanceof RepositoryBundle) {
						VersionedClause newClause = RepositoryBundleUtils.convertRepoBundle((RepositoryBundle) item);
						adding.add(newClause);
					} else if (item instanceof RepositoryBundleVersion) {
						RepositoryBundleVersion bundleVersion = (RepositoryBundleVersion) item;
						VersionedClause newClause = RepositoryBundleUtils.convertRepoBundleVersion(bundleVersion,
							phase);
						adding.add(newClause);
					} else if (item instanceof RepositoryResourceElement) {
						RepositoryResourceElement elt = (RepositoryResourceElement) item;
						VersionedClause newClause = RepositoryBundleUtils
							.convertRepoBundleVersion(elt.getRepositoryBundleVersion(), phase);
						adding.add(newClause);
					}
				}

				handleAdd(adding);
				return true;
			}

			private boolean handleURLDrop(String urlStr) {
				try {
					URI uri = new URI(sanitizeUrl(urlStr));
					AddJpmDependenciesWizard wizard = new AddJpmDependenciesWizard(uri);
					WizardDialog dialog = new WizardDialog(getSection().getShell(), wizard);
					if (dialog.open() == Window.OK) {
						Set<ResourceDescriptor> resources = wizard.getResult();
						List<VersionedClause> newBundles = new ArrayList<>(resources.size());
						for (ResourceDescriptor resource : resources) {
							Attrs attrs = new Attrs();
							attrs.put(Constants.VERSION_ATTRIBUTE,
								resource.version != null ? resource.version.toString()
									: Version.emptyVersion.toString());
							VersionedClause clause = new VersionedClause(resource.bsn, attrs);
							newBundles.add(clause);
						}

						handleAdd(newBundles);
						return true;
					}
					return false;
				} catch (URISyntaxException e) {
					MessageDialog.openError(getSection().getShell(), "Error", "The dropped URL was invalid: " + urlStr);
					return false;
				}
			}

			private String sanitizeUrl(String urlStr) {
				int newline = urlStr.indexOf('\n');
				if (newline > -1)
					return urlStr.substring(0, newline)
						.trim();
				return urlStr;
			}

			private void handleAdd(Collection<VersionedClause> newClauses) {
				if (newClauses == null || newClauses.isEmpty())
					return;

				List<VersionedClause> toAdd = new LinkedList<>();
				for (VersionedClause newClause : newClauses) {
					boolean found = false;
					for (ListIterator<VersionedClause> iter = bundles.listIterator(); iter.hasNext();) {
						VersionedClause existing = iter.next();
						if (newClause.getName()
							.equals(existing.getName())) {
							int index = iter.previousIndex();
							iter.set(newClause);
							viewer.replace(newClause, index);

							found = true;
							break;
						}
					}
					if (!found)
						toAdd.add(newClause);
				}

				bundles.addAll(toAdd);
				viewer.add(toAdd.toArray());

				markDirty();
			}
		};
		dropAdapter.setFeedbackEnabled(false);
		dropAdapter.setExpandEnabled(false);
		viewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, new Transfer[] {
			LocalSelectionTransfer.getTransfer(), FileTransfer.getInstance(), ResourceTransfer.getInstance(),
			URLTransfer.getInstance()
		}, dropAdapter);

		table.addKeyListener(new KeyAdapter() {
			@Override
			public void keyReleased(KeyEvent e) {
				if (e.character == SWT.DEL) {
					doRemove();
				} else if (e.character == '+') {
					doAdd();
				}
			}
		});

		// Layout
		GridLayout layout = new GridLayout(1, false);
		layout.horizontalSpacing = 0;
		layout.verticalSpacing = 0;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		composite.setLayout(layout);

		GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		gd.widthHint = 50;
		gd.heightHint = getTableHeightHint();
		table.setLayoutData(gd);
	}

	private static boolean isRemovable(ISelection selection) {
		if (selection.isEmpty())
			return false;

		if (selection instanceof IStructuredSelection) {
			List<?> list = ((IStructuredSelection) selection).toList();
			for (Object object : list) {
				if (!(object instanceof VersionedClause)) {
					return false;
				}
			}
			return true;
		}

		return false;
	}

	protected int getTableHeightHint() {
		return SWT.DEFAULT;
	}

	protected List<VersionedClause> getBundles() {
		return bundles;
	}

	protected void setBundles(final List<VersionedClause> bundles) {
		this.bundles = bundles;
		Display.getDefault()
			.asyncExec(() -> viewer.setInput(bundles));
	}

	private void doAdd() {
		try {
			RepoBundleSelectionWizard wizard = createBundleSelectionWizard(getBundles());
			if (wizard != null) {
				WizardDialog dialog = new WizardDialog(getSection().getShell(), wizard);
				if (dialog.open() == Window.OK) {
					setBundles(wizard.getSelectedBundles());
					markDirty();
				}
			}
		} catch (Exception e) {
			ErrorDialog.openError(getSection().getShell(), "Error", null,
				new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, "Error opening bundle resolver wizard.", e));
		}
	}

	private void doRemove() {
		if (!isRemovable(viewer.getSelection()))
			return;

		IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
		if (!selection.isEmpty()) {
			Iterator<?> elements = selection.iterator();
			List<Object> removed = new LinkedList<>();
			while (elements.hasNext()) {
				Object element = elements.next();
				if (bundles.remove(element))
					removed.add(element);
			}

			if (!removed.isEmpty()) {
				viewer.remove(removed.toArray());
				markDirty();
			}
		}
	}

	@Override
	public void commitToModel(boolean onSave) {
		saveToModel(model, bundles);
	}

	protected abstract void saveToModel(BndEditModel model, List<VersionedClause> bundles);

	protected abstract List<VersionedClause> loadFromModel(BndEditModel model);

	protected final RepoBundleSelectionWizard createBundleSelectionWizard(List<VersionedClause> bundles)
		throws Exception {
		RepoBundleSelectionWizard wizard = new RepoBundleSelectionWizard(bundles, phase);
		setSelectionWizardTitleAndMessage(wizard);

		return wizard;
	}

	protected abstract void setSelectionWizardTitleAndMessage(RepoBundleSelectionWizard wizard);

	@Override
	public void refreshFromModel() {
		List<VersionedClause> bundles = loadFromModel(model);
		if (bundles != null) {
			setBundles(new ArrayList<>(bundles));
		} else {
			setBundles(new ArrayList<VersionedClause>());
		}
	}

	@Override
	public void initialize(IManagedForm form) {
		super.initialize(form);

		model = (BndEditModel) form.getInput();
		model.addPropertyChangeListener(propertyName, this);
	}

	@Override
	public void dispose() {
		super.dispose();
		if (model != null)
			model.removePropertyChangeListener(propertyName, this);
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		IFormPage page = (IFormPage) getManagedForm().getContainer();
		if (page.isActive()) {
			refresh();
		} else {
			markStale();
		}
	}
}
