/*******************************************************************************
 * Copyright (c) 2008, 2010 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Sonatype, Inc. - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.ui.sdk.scheduler;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
import org.eclipse.equinox.internal.p2.core.helpers.CollectionUtils;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.IProvisioningEventBus;
import org.eclipse.equinox.internal.provisional.p2.core.eventbus.ProvisioningListener;
import org.eclipse.equinox.internal.provisional.p2.updatechecker.IUpdateListener;
import org.eclipse.equinox.internal.provisional.p2.updatechecker.UpdateEvent;
import org.eclipse.equinox.p2.engine.*;
import org.eclipse.equinox.p2.engine.query.UserVisibleRootQuery;
import org.eclipse.equinox.p2.metadata.IInstallableUnit;
import org.eclipse.equinox.p2.operations.*;
import org.eclipse.equinox.p2.query.IQuery;
import org.eclipse.equinox.p2.query.IQueryResult;
import org.eclipse.equinox.p2.ui.ProvisioningUI;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.*;
import org.eclipse.ui.*;
import org.eclipse.ui.statushandlers.StatusManager;

/**
 * @since 3.5
 */
public class AutomaticUpdater implements IUpdateListener {

	StatusLineCLabelContribution updateAffordance;
	IStatusLineManager statusLineManager;
	Collection<IInstallableUnit> iusWithUpdates;
	String profileId;
	ProvisioningListener profileListener;
	AutomaticUpdatesPopup popup;
	boolean alreadyDownloaded = false;
	UpdateOperation operation;
	private static final String AUTO_UPDATE_STATUS_ITEM = "AutoUpdatesStatus"; //$NON-NLS-1$

	public AutomaticUpdater() {
		createProfileListener();
	}

	private void createProfileListener() {
		profileListener = new ProvisioningListener() {
			public void notify(EventObject o) {
				if (o instanceof IProfileEvent) {
					IProfileEvent event = (IProfileEvent) o;
					if (event.getReason() == IProfileEvent.CHANGED && sameProfile(event.getProfileId())) {
						triggerNewUpdateNotification();
					}
				}
			}
		};
		getProvisioningEventBus().addListener(profileListener);
	}

	boolean sameProfile(String another) {
		if (another.equals(IProfileRegistry.SELF)) {
			another = getProfileRegistry().getProfile(another).getProfileId();
		}
		if (profileId.equals(IProfileRegistry.SELF)) {
			profileId = getProfileRegistry().getProfile(profileId).getProfileId();
		}
		return profileId.equals(another);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.equinox.internal.provisional.p2.updatechecker.IUpdateListener
	 * #
	 * updatesAvailable(org.eclipse.equinox.internal.provisional.p2.updatechecker
	 * .UpdateEvent)
	 */
	public void updatesAvailable(UpdateEvent event) {
		updatesAvailable(event, true);
	}

	void updatesAvailable(final UpdateEvent event, final boolean notifyWithPopup) {
		final boolean download = getPreferenceStore().getBoolean(PreferenceConstants.PREF_DOWNLOAD_ONLY);
		profileId = event.getProfileId();
		iusWithUpdates = event.getIUs();
		validateIusToUpdate();
		alreadyDownloaded = false;

		// Create an update operation to reflect the new updates that are available.
		operation = new UpdateOperation(getSession(), iusWithUpdates);
		operation.setProfileId(event.getProfileId());
		IStatus status = operation.resolveModal(new NullProgressMonitor());

		if (!status.isOK() || operation.getPossibleUpdates() == null || operation.getPossibleUpdates().length == 0) {
			if (PlatformUI.isWorkbenchRunning()) {
				PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
					public void run() {
						clearUpdateAffordances();
					}
				});
			}
			return;
		}
		// Download the items before notifying user if the
		// preference dictates.

		if (download) {
			ProfileModificationJob job = new ProfileModificationJob(AutomaticUpdateMessages.AutomaticUpdater_AutomaticDownloadOperationName, getSession(), event.getProfileId(), operation.getProvisioningPlan(), new ProvisioningContext(getSession().getProvisioningAgent()));
			job.setPhaseSet(PhaseSetFactory.createPhaseSetIncluding(new String[] {PhaseSetFactory.PHASE_COLLECT}));
			job.setUser(false);
			job.setSystem(true);
			job.addJobChangeListener(new JobChangeAdapter() {
				public void done(IJobChangeEvent jobEvent) {
					IStatus jobStatus = jobEvent.getResult();
					if (jobStatus.isOK()) {
						alreadyDownloaded = true;
						PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
							public void run() {
								notifyUserOfUpdates(operation.getResolutionResult().isOK(), notifyWithPopup);
							}
						});
					} else if (jobStatus.getSeverity() != IStatus.CANCEL) {
						StatusManager.getManager().handle(jobStatus, StatusManager.LOG);
					}
				}
			});
			job.schedule();
		} else {
			PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() {
				public void run() {
					notifyUserOfUpdates(operation.getResolutionResult().isOK(), notifyWithPopup);
				}
			});
		}

	}

	ProvisioningSession getSession() {
		return AutomaticUpdatePlugin.getDefault().getSession();
	}

	/*
	 * Use with caution, as this still start the whole UI bundle.  Shouldn't be used
	 * in any of the update checking code, only the code that presents updates when notified.
	 */
	ProvisioningUI getProvisioningUI() {
		return ProvisioningUI.getDefaultUI();
	}

	/*
	 * Filter out the ius that aren't visible to the user or are
	 * locked for updating.
	 */

	void validateIusToUpdate() {
		ArrayList<IInstallableUnit> list = new ArrayList<IInstallableUnit>(iusWithUpdates.size());
		IProfile profile = getProfileRegistry().getProfile(profileId);

		for (IInstallableUnit iuWithUpdate : iusWithUpdates) {
			try {
				if (validToUpdate(profile, iuWithUpdate))
					list.add(iuWithUpdate);
			} catch (OperationCanceledException e) {
				// Nothing to report
			}
		}
		iusWithUpdates = list;
	}

	// A proposed update is valid if it is still visible to the user as an
	// installed item (it is a root)
	// and if it is not locked for updating.
	private boolean validToUpdate(IProfile profile, IInstallableUnit iu) {
		int lock = IProfile.LOCK_NONE;
		boolean isRoot = false;
		try {
			String value = profile.getInstallableUnitProperty(iu, IProfile.PROP_PROFILE_LOCKED_IU);
			if (value != null)
				lock = Integer.parseInt(value);
			value = profile.getInstallableUnitProperty(iu, IProfile.PROP_PROFILE_ROOT_IU);
			isRoot = value == null ? false : Boolean.valueOf(value).booleanValue();
		} catch (NumberFormatException e) {
			// ignore and assume no lock
		}
		return isRoot && (lock & IProfile.LOCK_UPDATE) == 0;
	}

	Shell getWorkbenchWindowShell() {
		IWorkbenchWindow activeWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		return activeWindow != null ? activeWindow.getShell() : null;

	}

	IStatusLineManager getStatusLineManager() {
		if (statusLineManager != null)
			return statusLineManager;
		IWorkbenchWindow activeWindow = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
		if (activeWindow == null)
			return null;
		// YUCK! YUCK! YUCK!
		// IWorkbenchWindow does not define getStatusLineManager(), yet
		// WorkbenchWindow does
		try {
			Method method = activeWindow.getClass().getDeclaredMethod("getStatusLineManager", new Class[0]); //$NON-NLS-1$
			try {
				Object statusLine = method.invoke(activeWindow, new Object[0]);
				if (statusLine instanceof IStatusLineManager) {
					statusLineManager = (IStatusLineManager) statusLine;
					return statusLineManager;
				}
			} catch (InvocationTargetException e) {
				// oh well
			} catch (IllegalAccessException e) {
				// I tried
			}
		} catch (NoSuchMethodException e) {
			// can't blame us for trying.
		}

		IWorkbenchPartSite site = activeWindow.getActivePage().getActivePart().getSite();
		if (site instanceof IViewSite) {
			statusLineManager = ((IViewSite) site).getActionBars().getStatusLineManager();
		} else if (site instanceof IEditorSite) {
			statusLineManager = ((IEditorSite) site).getActionBars().getStatusLineManager();
		}
		return statusLineManager;
	}

	void updateStatusLine() {
		IStatusLineManager manager = getStatusLineManager();
		if (manager != null)
			manager.update(true);
	}

	void createUpdateAffordance() {
		updateAffordance = new StatusLineCLabelContribution(AUTO_UPDATE_STATUS_ITEM, 5);
		updateAffordance.addListener(SWT.MouseDown, new Listener() {
			public void handleEvent(Event event) {
				launchUpdate();
			}
		});
		IStatusLineManager manager = getStatusLineManager();
		if (manager != null) {
			manager.add(updateAffordance);
			manager.update(true);
		}
	}

	void notifyUserOfUpdates(boolean isValid, boolean showPopup) {
		if (updateAffordance == null)
			createUpdateAffordance();
		if (isValid) {
			if (showPopup)
				openUpdatePopup();
			updateAffordance.setTooltip(AutomaticUpdateMessages.AutomaticUpdater_ClickToReviewUpdates);
			updateAffordance.setImage(AutomaticUpdatePlugin.getDefault().getImageRegistry().get((AutomaticUpdatePlugin.IMG_TOOL_UPDATE)));
		} else {
			updateAffordance.setTooltip(AutomaticUpdateMessages.AutomaticUpdater_ClickToReviewUpdatesWithProblems);
			updateAffordance.setImage(AutomaticUpdatePlugin.getDefault().getImageRegistry().get((AutomaticUpdatePlugin.IMG_TOOL_UPDATE_PROBLEMS)));
		}
		IStatusLineManager manager = getStatusLineManager();
		if (manager != null) {
			manager.update(true);
		}
	}

	void checkUpdateAffordanceEnablement() {
		// We don't currently support enablement in the affordance,
		// so we hide it if it should not be enabled.
		if (updateAffordance == null)
			return;
		boolean shouldBeVisible = getProvisioningUI().hasScheduledOperations();
		if (updateAffordance.isVisible() != shouldBeVisible) {
			IStatusLineManager manager = getStatusLineManager();
			if (manager != null) {
				updateAffordance.setVisible(shouldBeVisible);
				manager.update(true);
			}
		}
	}

	void openUpdatePopup() {
		if (popup == null)
			popup = new AutomaticUpdatesPopup(getWorkbenchWindowShell(), alreadyDownloaded, getPreferenceStore());
		popup.open();

	}

	void clearUpdateAffordances() {
		if (updateAffordance != null) {
			IStatusLineManager manager = getStatusLineManager();
			if (manager != null) {
				manager.remove(updateAffordance);
				manager.update(true);
			}
			updateAffordance.dispose();
			updateAffordance = null;
		}
		if (popup != null) {
			popup.close(false);
			popup = null;
		}
	}

	public void launchUpdate() {
		getProvisioningUI().openUpdateWizard(false, operation, null);
	}

	/*
	 * The profile has changed. Make sure our toUpdate list is still valid and
	 * if there is nothing to update, get rid of the update popup and
	 * affordance.
	 */
	void triggerNewUpdateNotification() {
		Job notifyJob = new Job("Update validate job") { //$NON-NLS-1$
			public IStatus run(IProgressMonitor monitor) {
				if (monitor.isCanceled())
					return Status.CANCEL_STATUS;
				// notify that updates are available for all roots.  We don't know for sure that
				// there are any, but this will cause everything to be rechecked. Don't trigger
				// a popup, just update the affordance and internal state.
				updatesAvailable(new UpdateEvent(profileId, getInstalledIUs()));
				return Status.OK_STATUS;
			}
		};
		notifyJob.setSystem(true);
		notifyJob.setUser(false);
		notifyJob.setPriority(Job.LONG);
		notifyJob.schedule();
	}

	/*
	 * Get the IInstallable units for the specified profile
	 * 
	 * @param profileId the profile in question
	 * @param all <code>true</code> if all IInstallableUnits in the profile should
	 * be returned, <code>false</code> only those IInstallableUnits marked as (user visible) roots
	 * should be returned.
	 * 
	 * @return an array of IInstallableUnits installed in the profile.
	 */
	public Collection<IInstallableUnit> getInstalledIUs() {
		IProfile profile = getProfileRegistry().getProfile(profileId);
		if (profile == null)
			return CollectionUtils.emptyList();
		IQuery<IInstallableUnit> query = new UserVisibleRootQuery();
		IQueryResult<IInstallableUnit> queryResult = profile.query(query, null);
		return queryResult.toUnmodifiableSet();
	}

	public void shutdown() {
		statusLineManager = null;
		if (profileListener != null) {
			getProvisioningEventBus().removeListener(profileListener);
			profileListener = null;
		}
	}

	IProfileRegistry getProfileRegistry() {
		return (IProfileRegistry) getSession().getProvisioningAgent().getService(IProfileRegistry.SERVICE_NAME);
	}

	IProvisioningEventBus getProvisioningEventBus() {
		return (IProvisioningEventBus) getSession().getProvisioningAgent().getService(IProvisioningEventBus.SERVICE_NAME);
	}

	IPreferenceStore getPreferenceStore() {
		return AutomaticUpdatePlugin.getDefault().getPreferenceStore();
	}

}
