package de.eiswind.mango.client.core.p2;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.engine.ProfileRegistryComponent;
import org.eclipse.equinox.p2.core.IProvisioningAgent;
import org.eclipse.equinox.p2.core.spi.IAgentServiceFactory;
import org.eclipse.equinox.p2.engine.IEngine;
import org.eclipse.equinox.p2.engine.IProfile;
import org.eclipse.equinox.p2.engine.IProfileRegistry;
import org.eclipse.equinox.p2.engine.ProvisioningContext;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.metadata.Version;
import org.eclipse.equinox.p2.metadata.VersionRange;
import org.eclipse.equinox.p2.operations.ProvisioningJob;
import org.eclipse.equinox.p2.operations.ProvisioningSession;
import org.eclipse.equinox.p2.operations.Update;
import org.eclipse.equinox.p2.operations.UpdateOperation;
import org.eclipse.equinox.p2.planner.IPlanner;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.query.IQueryable;
import org.eclipse.equinox.p2.query.QueryUtil;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;

import de.eiswind.paris.client.core.Activator;
import de.eiswind.paris.client.core.Messages;

public class UpdateManager {

	private String version;
	private IStatus status;
	private volatile SubMonitor sub;

	private static final String JUSTUPDATED = "justUpdated"; //$NON-NLS-1$

	public int searchForUpdates(BundleContext context) throws Exception {

		IProfile profile = getCurrentProfile(context);

		if (profile == null) {
			log(IStatus.INFO, "The current profile is null", null); //$NON-NLS-1$
			// return PlatformUI.RETURN_OK;
		}
		// get agent
		IProvisioningAgent agent = getAgent(context);

		// set provisioning cotext: get it as a System property repoURI
		ProvisioningContext provisioncontext = getContext(agent, context);

		// invoke the engine to perform installs/uninstalls
		executeUpdate(profile, agent, provisioncontext, context);

		return PlatformUI.RETURN_OK;

	}

	private Update getUpdate(IProfile profile, ProvisioningContext provisioningContext, IEngine engine, BundleContext context) {
		IInstallableUnit oldIUFromProfile;
		IInstallableUnit newIUFromRepo;
		if (profile == null) {
			return null;
		}
		IQueryResult<IInstallableUnit> allIUFromRepo = getAllInstallableUnitFromRepository(provisioningContext);

		List<IInstallableUnit> units = new ArrayList<IInstallableUnit>();
		for (Iterator<IInstallableUnit> it = allIUFromRepo.iterator(); it.hasNext();) {
			newIUFromRepo = it.next();
			units.add(newIUFromRepo);
		}
		if (units.size() == 0) {
			log(IStatus.WARNING, "No IUs found", null); //$NON-NLS-1$
			return null;
		}
		Collections.sort(units);
		// get the latest matching update
		newIUFromRepo = units.get(units.size() - 1);
		log(IStatus.INFO, "Found IU " + newIUFromRepo.getId(), null); //$NON-NLS-1$
		Version v = newIUFromRepo.getVersion();
		String vstring = v.getOriginal();

		oldIUFromProfile = getOldInstallableUnitById(profile, newIUFromRepo.getId());
		if (oldIUFromProfile != null) {

			log(IStatus.INFO, "Found IU " + newIUFromRepo.getId() + " with version " + vstring + " Installed IU has version " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					+ oldIUFromProfile.getVersion().getOriginal(), null);

			if (newIUFromRepo.compareTo(oldIUFromProfile) > 0) {
				log(IStatus.INFO, " Updating this IU", null); //$NON-NLS-1$

				Update update = new Update(oldIUFromProfile, newIUFromRepo);
				return update;
			}

		}
		return null;

	}

	IQueryResult<IInstallableUnit> getAllInstallableUnitFromRepository(ProvisioningContext provisioningContext) {

		IQueryable<IInstallableUnit> queryable = provisioningContext.getMetadata(new NullProgressMonitor());
		String[] versionstrings = version.split("\\."); //$NON-NLS-1$
		int major = Integer.parseInt(versionstrings[0]);
		int minor = Integer.parseInt(versionstrings[1]);
		int micro = Integer.parseInt(versionstrings[2]);
		log(IStatus.INFO, "Query for Version " + version, null); //$NON-NLS-1$
		IQueryResult<IInstallableUnit> matches = queryable.query(QueryUtil.createIUQuery("de.eiswind.mango.client.core.mango", //$NON-NLS-1$
				new VersionRange(Version.createOSGi(major, minor, micro), true, Version.createOSGi(major, minor, micro + 1), false)),
				new NullProgressMonitor());
		return matches;

	}

	IInstallableUnit getOldInstallableUnitById(IProfile profile, String id) {

		IQueryResult<IInstallableUnit> oldPlugInResult = profile.query(QueryUtil.createIUQuery(id), null);
		if (oldPlugInResult.isEmpty()) {
			return null;
		}
		return oldPlugInResult.iterator().next();

	}

	private void executeUpdate(final IProfile profile, IProvisioningAgent agent, final ProvisioningContext provisioningContext,
			final BundleContext context) {
		final IEngine engine = (IEngine) agent.getService(IEngine.SERVICE_NAME);

		// get the server-version-matching latest Update from the repository

		final IPreferenceStore prefStore = Activator.getDefault().getPreferenceStore();
		if (prefStore.getBoolean(JUSTUPDATED)) {
			prefStore.setValue(JUSTUPDATED, false);
			return;
		}
		//

		ProvisioningSession session = new ProvisioningSession(agent);
		final UpdateOperation operation = new UpdateOperation(session);
		log(IStatus.INFO, "Resolving the update", null); //$NON-NLS-1$

		final IRunnableWithProgress runnable = new IRunnableWithProgress() {
			public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {

				sub = SubMonitor.convert(monitor, Messages.getString("UpdateManager.searchforupdates"), 200); //$NON-NLS-1$
				final Update update = getUpdate(profile, provisioningContext, engine, context);

				status = operation.resolveModal(sub.newChild(100));
				LogHelper.log(status);
				if (status.getCode() == UpdateOperation.STATUS_NOTHING_TO_UPDATE) {
					status = null;
					return;
				}
				if (status.getSeverity() == IStatus.CANCEL)
					throw new OperationCanceledException();
				if (status.getSeverity() != IStatus.ERROR) {
					log(IStatus.INFO, "Checking for available update matches", null); //$NON-NLS-1$
					Update[] selected = new Update[1];
					operation.setSelectedUpdates(new Update[0]);
					for (Update available : operation.getPossibleUpdates()) {
						if (available.equals(update)) {
							log(IStatus.INFO, "Update matches available: " + update, null); //$NON-NLS-1$
							selected[0] = available;
							operation.setSelectedUpdates(selected);
						}
					}
					if (selected[0] == null) {
						status = null;
						monitor.setCanceled(true);

						log(IStatus.WARNING, "No Update matches selected", null); //$NON-NLS-1$
						return;
					}
					ProvisioningJob job = operation.getProvisioningJob(monitor);
					if (job != null) {
						status = job.runModal(sub.newChild(100));
						if (status.getSeverity() != IStatus.ERROR) {
							prefStore.setValue(JUSTUPDATED, true);
							Display.getDefault().syncExec(new Runnable() {
								public void run() {
									PlatformUI.getWorkbench().restart();
								}
							});

						} else {
							LogHelper.log(status);
						}

					} else {
						log(IStatus.INFO, "getJob returned null", null); //$NON-NLS-1$
						status = null;
					}
					if (status != null && status.getSeverity() == IStatus.CANCEL)
						throw new OperationCanceledException();
				}
			}
		};

		Display.getDefault().asyncExec(new Runnable() {
			public void run() {
				try {
					new ProgressMonitorDialog(null).run(true, true, runnable);
				} catch (InvocationTargetException x) {
					log(IStatus.ERROR, "Runnable failure", x); //$NON-NLS-1$
				} catch (InterruptedException e) {
				}
			}
		});

		// More complex status handling might include showing the
		// user what
		// updates
		// are available if there are multiples, differentiating
		// patches vs.
		// updates, etc.
		// In this example, we simply update as suggested by the
		// operation.

	}

	/*
	 * Return the current profile or null if it cannot be retrieved.
	 */
	public static IProfile getCurrentProfile(BundleContext context) {
		IProvisioningAgent agent = getAgent(context);
		ProfileRegistryComponent pr = new ProfileRegistryComponent();
		pr.createService(agent);
		Dictionary dic = new Hashtable();
		dic.put(IAgentServiceFactory.PROP_CREATED_SERVICE_NAME, IProfileRegistry.SERVICE_NAME);
		context.registerService(IAgentServiceFactory.class.getName(), pr, dic);
		IProfileRegistry profileRegistry = (IProfileRegistry) agent.getService(IProfileRegistry.SERVICE_NAME);
		if (profileRegistry == null) {
			log(IStatus.ERROR, "ProfileRegistry is null", null); //$NON-NLS-1$
			return null;
		}
		return profileRegistry.getProfile(IProfileRegistry.SELF);

	}

	private ProvisioningContext getContext(IProvisioningAgent agent, BundleContext context) {
		// String repoUri =
		// System.getProperty("de.eiswind.buchmanager.repository");
		// if (repoUri == null) {
		// repoUri = "http://www.eiswind.de/buchmanager/repository/";
		// }
		try {
			// URI[] repoURIs = { (new URI(repoUri)) };
			ProvisioningContext result = new ProvisioningContext(getAgent(context));
			// result.setArtifactRepositories(new URI[] { repoURIs[0] });
			// result.setMetadataRepositories(new URI[] { repoURIs[0] });
			// IMetadataRepositoryManager repoManager =
			// (IMetadataRepositoryManager)
			// agent.getService(IMetadataRepositoryManager.SERVICE_NAME);
			// if (repoManager == null) {
			// repoManager = new MetadataRepositoryManager(agent);
			// }
			//
			// repoManager.loadRepository(repoURIs[0], new
			// NullProgressMonitor());
			return result;

			// } catch (URISyntaxException e) {
			//
			// log(IStatus.ERROR, "The entered repository URI is not valid", e);
			//
			// } catch (ProvisionException e) {
			//
			// log(IStatus.ERROR, "Provision failed", e);
		} catch (OperationCanceledException e) {
			log(IStatus.ERROR, "Provision was canceled", e); //$NON-NLS-1$
		}

		return null;

	}

	private static IProvisioningAgent getAgent(BundleContext context) {
		return (IProvisioningAgent) getService(context, IProvisioningAgent.SERVICE_NAME);
	}

	private static IPlanner getPlanner(BundleContext context) {
		return (IPlanner) getService(context, IPlanner.SERVICE_NAME);
	}

	public static Object getService(BundleContext context, String name) {
		if (context == null)
			return null;
		ServiceReference reference = context.getServiceReference(name);
		if (reference == null)
			return null;
		Object result = context.getService(reference);
		context.ungetService(reference);
		return result;
	}

	private static void log(int level, String msg, Throwable x) {
		Activator.getDefault().getLog().log(new Status(level, Activator.PLUGIN_ID, msg, x));
	}

	public void setVersion(String version) {
		this.version = version;
	}

}
