package bndtools.wizards.bndfile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.bndtools.api.BndtoolsConstants;
import org.bndtools.utils.workspace.FileUtils;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.core.runtime.CoreException;
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.jface.window.Window;
import org.eclipse.swt.widgets.Shell;

import aQute.bnd.build.Project;
import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.osgi.Constants;
import aQute.bnd.properties.Document;
import aQute.bnd.unmodifiable.Lists;
import aQute.bnd.unmodifiable.Sets;
import bndtools.Plugin;
import bndtools.central.Central;

public class EnableSubBundlesOperation implements IWorkspaceRunnable {

	private static final Set<String> BUNDLE_SPECIFIC_HEADERS;

	static {
		Set<String> headers = new HashSet<>(Constants.BUNDLE_SPECIFIC_HEADERS);
		headers.add(Constants.SERVICE_COMPONENT);
		headers.add(Constants.BUNDLE_VERSION);
		BUNDLE_SPECIFIC_HEADERS = Sets.copyOf(headers);
	}

	private static final Set<String>	PROJECT_ONLY_HEADERS	= Sets.of(Constants.BUILDPATH, Constants.RUNBUNDLES,
		Constants.RUNSYSTEMPACKAGES, Constants.RUNPATH, Constants.RUNVM, Constants.RUNTRACE, Constants.RUNFRAMEWORK,
		Constants.RUNFW, Constants.SUB);

	private final Shell					parentShell;
	private final IWorkspace			workspace;
	private final IPath					containerPath;

	private InputStream					newBundleInputStream	= null;

	public EnableSubBundlesOperation(Shell parentShell, IWorkspace workspace, IPath containerPath) {
		this.parentShell = parentShell;
		this.workspace = workspace;
		this.containerPath = containerPath;
	}

	@Override
	public void run(final IProgressMonitor monitor) throws CoreException {
		IResource container = workspace.getRoot()
			.findMember(containerPath);

		if (container == null || !container.exists())
			throw newCoreException("Container path does not exist", null);

		// Create new project model
		BndEditModel newBundleModel;
		try {
			newBundleModel = new BndEditModel(Central.getWorkspace());
		} catch (Exception e) {
			System.err.println("Unable to create BndEditModel with Workspace, defaulting to without Workspace");
			newBundleModel = new BndEditModel();
		}
		// Load project file and model
		IProject project = container.getProject();
		IFile projectFile = project.getFile(Project.BNDFILE);
		BndEditModel projectModel;
		final Document projectDocument;
		try {
			if (projectFile.exists()) {
				byte[] bytes = FileUtils.readFully(projectFile.getContents());
				projectDocument = new Document(new String(bytes, projectFile.getCharset()));
			} else {
				projectDocument = new Document("");
			}
			try {
				projectModel = new BndEditModel(Central.getWorkspace());
			} catch (Exception e) {
				System.err.println("Unable to create BndEditModel with Workspace, defaulting to without Workspace");
				projectModel = new BndEditModel();
			}
			projectModel.loadFrom(projectDocument);
		} catch (IOException e) {
			throw new CoreException(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, e.getMessage(), e));
		}

		// Sub-bundles are meaningless outside of Bndtools
		if (project.hasNature(BndtoolsConstants.NATURE_ID)) {
			// Check if we need to enable sub-bundles on the project file
			boolean enableSubs;
			List<String> subBndFiles = projectModel.getSubBndFiles();
			List<String> availableHeaders = calculateProjectOnlyHeaders(projectModel.getAllPropertyNames());
			Collection<String> bundleSpecificHeaders = calculateBundleSpecificHeaders(availableHeaders);

			if (subBndFiles == null || subBndFiles.isEmpty()) {
				final EnableSubBundlesDialog subBundlesDialog = new EnableSubBundlesDialog(parentShell,
					availableHeaders, bundleSpecificHeaders);

				if (subBundlesDialog.open() != Window.OK) {
					monitor.setCanceled(true);
					return;
				}

				enableSubs = subBundlesDialog.isEnableSubBundles();
				bundleSpecificHeaders = subBundlesDialog.getSelectedProperties();
			} else {
				enableSubs = false;
			}

			// Enable subs and copy entries from project model to new bundle
			// model
			if (enableSubs) {
				projectModel.setSubBndFiles(Lists.of("*.bnd"));
				for (String propertyName : bundleSpecificHeaders) {
					Object value = projectModel.genericGet(propertyName);
					projectModel.genericSet(propertyName, null);
					newBundleModel.genericSet(propertyName, value);
				}

				// Save the project model
				projectModel.saveChangesTo(projectDocument);
				FileUtils.writeFully(projectDocument.get(), projectFile, false);
			}
		}

		// Generate the new bundle model
		Document newBundleDocument = new Document("");
		newBundleModel.saveChangesTo(newBundleDocument);

		try {
			newBundleInputStream = new ByteArrayInputStream(newBundleDocument.get()
				.getBytes("UTF-8"));
		} catch (UnsupportedEncodingException e) {
			newBundleInputStream = null;
		}
	}

	private static List<String> calculateProjectOnlyHeaders(List<String> allPropertyNames) {
		List<String> result = new ArrayList<>(allPropertyNames.size());

		for (String propertyName : allPropertyNames) {
			if (!PROJECT_ONLY_HEADERS.contains(propertyName))
				result.add(propertyName);
		}

		return result;
	}

	public static List<String> calculateBundleSpecificHeaders(Collection<String> propertyNames) {
		List<String> result = new ArrayList<>(propertyNames.size());

		for (String propertyName : propertyNames) {
			if (BUNDLE_SPECIFIC_HEADERS.contains(propertyName))
				result.add(propertyName);
		}

		return result;
	}

	public static void moveBundleContentProperties(BndEditModel sourceModel, BndEditModel destModel,
		List<String> properties) {
		for (String property : properties) {
			Object value = sourceModel.genericGet(property);
			destModel.genericSet(property, value);
			sourceModel.genericSet(property, null);
		}
	}

	private static CoreException newCoreException(String message, Throwable cause) {
		return new CoreException(new Status(IStatus.ERROR, Plugin.PLUGIN_ID, 0, message, cause));
	}

	public InputStream getNewBundleInputStream() {
		return newBundleInputStream;
	}
}
