/*******************************************************************************
 * Copyright (c) 2011, 2012 Anton Gorenkov
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Anton Gorenkov - initial API and implementation
 *******************************************************************************/
package org.eclipse.cdt.testsrunner.internal.ui.view;

import java.text.MessageFormat;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.eclipse.cdt.testsrunner.internal.model.ITestingSessionsManagerListener;
import org.eclipse.cdt.testsrunner.internal.model.TestingSessionsManager;
import org.eclipse.cdt.testsrunner.model.ITestCase;
import org.eclipse.cdt.testsrunner.model.ITestItem;
import org.eclipse.cdt.testsrunner.model.ITestSuite;
import org.eclipse.cdt.testsrunner.model.ITestingSession;
import org.eclipse.cdt.testsrunner.model.ITestingSessionListener;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.progress.UIJob;

/**
 * Tracks and collects the changes in active testing session and updates the UI
 * periodically. It allows to significantly improve the UI performance.
 */
public class UIUpdater {

	/** Access to the results showing view. */
	private ResultsView resultsView;

	/** Access to the tests hierarchy showing widget. */
	private TestsHierarchyViewer testsHierarchyViewer;

	/** Access to the statistics showing widget. */
	private ProgressCountPanel progressCountPanel;

	/** Listener for the changes in active testing session. */
	private ITestingSessionListener sessionListener;

	/**
	 * Specifies whether tests hierarchy scrolling should be done during the
	 * testing process.
	 */
	private boolean autoScroll = true;

	/** Access to the testing sessions manager. */
	private TestingSessionsManager sessionsManager;

	/** Listener to handle active testing session change. */
	private TestingSessionsManagerListener sessionsManagerListener;

	/** Reference to the active testing session. */
	ITestingSession testingSession;

	/** Storage for the UI changes that should be done on update. */
	UIChangesCache uiChangesCache = new UIChangesCache();

	/** A job that makes an UI update periodically. */
	UpdateUIJob updateUIJob = null;

	/** Time interval over which the UI should be updated. */
	private static final int REFRESH_INTERVAL = 200;

	/**
	 * Storage for the UI changes that should be done on update.
	 */
	private class UIChangesCache {

		/**
		 * Specifies whether Progress Counter Panel should be updated during the
		 * next UI update.
		 */
		private boolean needProgressCountPanelUpdate;

		/**
		 * Specifies whether view actions should be updated during the next UI
		 * update.
		 */
		private boolean needActionsUpdate;

		/**
		 * A test item which path should be shown as a view caption during the
		 * next UI update.
		 */
		private ITestItem testItemForNewViewCaption;

		/**
		 * Set of tree objects on which <code>refresh()</code> should be called
		 * during the next UI update.
		 */
		private Set<Object> treeItemsToRefresh = new HashSet<>();

		/**
		 * Set of tree objects on which <code>update()</code> should be called
		 * during the next UI update.
		 */
		private Set<Object> treeItemsToUpdate = new HashSet<>();

		/** Tree object that should be revealed during the next UI update. */
		private Object treeItemToReveal;

		/** Map of tree objects that should be expanded or collapsed to their new states. */
		private Map<Object, Boolean> treeItemsToExpand = new LinkedHashMap<>();

		UIChangesCache() {
			resetChanges();
		}

		/**
		 * Schedules the Progress Counter Panel update during the next UI update.
		 */
		public void scheduleProgressCountPanelUpdate() {
			synchronized (this) {
				needProgressCountPanelUpdate = true;
			}
		}

		/**
		 * Schedules the view actions update during the next UI update.
		 */
		public void scheduleActionsUpdate() {
			synchronized (this) {
				needActionsUpdate = true;
			}
		}

		/**
		 * Schedules the view caption update to the path to specified test item
		 * during the next UI update.
		 *
		 * @param testItem specified test item
		 */
		public void scheduleViewCaptionChange(ITestItem testItem) {
			synchronized (this) {
				testItemForNewViewCaption = testItem;
			}
		}

		/**
		 * Schedules the <code>update()</code> call for the specified tree
		 * object during the next UI update.
		 *
		 * @param item tree object to update
		 */
		public void scheduleTreeItemUpdate(Object item) {
			synchronized (this) {
				treeItemsToUpdate.add(item);
			}
		}

		/**
		 * Schedules the revealing of the specified tree object. Overrides
		 * the previously specified tree object to reveal (if any).
		 *
		 * @param item tree object to reveal
		 */
		public void scheduleTreeItemReveal(Object item) {
			synchronized (this) {
				treeItemToReveal = item;
			}
		}

		/**
		 * Schedules the expanding or collapsing of the specified tree object.
		 * Overrides the previous state for the same tree object (if any).
		 *
		 * @param item tree object to expand or collapse
		 * @param expandedState true if the node is expanded, and false if
		 * collapsed
		 */
		public void scheduleTreeItemExpand(Object item, boolean expandedState) {
			synchronized (this) {
				treeItemsToExpand.put(item, expandedState);
			}
		}

		/**
		 * Schedules the <code>refresh()</code> call for the specified tree
		 * object during the next UI update.
		 *
		 * @param item tree object to refresh
		 */
		public void scheduleTreeItemRefresh(Object item) {
			synchronized (this) {
				treeItemsToRefresh.add(item);
			}
		}

		/**
		 * Apply any scheduled changes to UI.
		 */
		public void applyChanges() {
			synchronized (this) {
				TreeViewer treeViewer = testsHierarchyViewer.getTreeViewer();
				// View statistics widgets update
				if (needProgressCountPanelUpdate) {
					progressCountPanel.updateInfoFromSession();
				}
				// View actions update
				if (needActionsUpdate) {
					resultsView.updateActionsFromSession();
				}
				// View caption update
				if (testItemForNewViewCaption != null) {
					resultsView.setCaption(MessageFormat.format(UIViewMessages.UIUpdater_view_caption_format,
							testItemForNewViewCaption.getName(),
							TestPathUtils.getTestItemPath(testItemForNewViewCaption.getParent())));
				}
				// Tree view update
				if (!treeItemsToRefresh.isEmpty()) {
					for (Object item : treeItemsToRefresh) {
						treeViewer.refresh(item, false);
					}
				}
				if (!treeItemsToUpdate.isEmpty()) {
					treeViewer.update(treeItemsToUpdate.toArray(), null);
				}
				if (treeItemToReveal != null) {
					treeViewer.reveal(treeItemToReveal);
				}
				if (!treeItemsToExpand.isEmpty()) {
					for (Map.Entry<Object, Boolean> entry : treeItemsToExpand.entrySet()) {
						treeViewer.setExpandedState(entry.getKey(), entry.getValue());
					}
				}
				// All changes are applied, remove them
				resetChangesImpl();
			}
		}

		/**
		 * Reset all the scheduled changes to UI.
		 */
		public void resetChanges() {
			synchronized (this) {
				resetChangesImpl();
			}
		}

		/**
		 * Reset all the scheduled changes to UI. Note, this method is not
		 * synchronized so it should be used carefully
		 */
		private void resetChangesImpl() {
			needProgressCountPanelUpdate = false;
			needActionsUpdate = false;
			testItemForNewViewCaption = null;
			treeItemsToUpdate.clear();
			treeItemToReveal = null;
			treeItemsToExpand.clear();
		}
	}

	/**
	 * A job that makes an UI update periodically.
	 */
	private class UpdateUIJob extends UIJob {

		/** Controls whether the job should be scheduled again. */
		private boolean isRunning = true;

		public UpdateUIJob() {
			super(UIViewMessages.UIUpdater_update_ui_job);
			setSystem(true);
		}

		@Override
		public IStatus runInUIThread(IProgressMonitor monitor) {
			if (!resultsView.isDisposed()) {
				uiChangesCache.applyChanges();
				scheduleSelf();
			}
			return Status.OK_STATUS;
		}

		/**
		 * Schedule self for running after time interval.
		 */
		public void scheduleSelf() {
			schedule(REFRESH_INTERVAL);
		}

		/**
		 * Sets the flag that prevents planning this job again.
		 */
		public void stop() {
			isRunning = false;
		}

		@Override
		public boolean shouldSchedule() {
			return isRunning;
		}

	}

	/**
	 * Listener for the changes in active testing session.
	 */
	private class SessionListener implements ITestingSessionListener {

		/**
		 * Common implementation for test case and test suite entering.
		 *
		 * @param testItem test case or test suite
		 */
		private void enterTestItem(ITestItem testItem) {
			uiChangesCache.scheduleViewCaptionChange(testItem);
			uiChangesCache.scheduleTreeItemUpdate(testItem);
			if (autoScroll) {
				uiChangesCache.scheduleTreeItemReveal(testItem);
			}
		}

		@Override
		public void enterTestSuite(ITestSuite testSuite) {
			enterTestItem(testSuite);
		}

		@Override
		public void exitTestSuite(ITestSuite testSuite) {
			uiChangesCache.scheduleTreeItemUpdate(testSuite);
			if (autoScroll) {
				uiChangesCache.scheduleTreeItemExpand(testSuite, false);
			}
		}

		@Override
		public void enterTestCase(ITestCase testCase) {
			enterTestItem(testCase);
		}

		@Override
		public void exitTestCase(ITestCase testCase) {
			uiChangesCache.scheduleActionsUpdate();
			uiChangesCache.scheduleProgressCountPanelUpdate();
			uiChangesCache.scheduleTreeItemUpdate(testCase);
		}

		@Override
		public void childrenUpdate(ITestSuite parent) {
			uiChangesCache.scheduleTreeItemRefresh(parent);
		}

		@Override
		public void testingStarted() {
			resultsView.updateActionsFromSession();
			Display.getDefault().syncExec(() -> {
				resultsView.setCaption(testingSession.getStatusMessage());
				progressCountPanel.updateInfoFromSession();
				testsHierarchyViewer.getTreeViewer().refresh();
			});
			startUpdateUIJob();
		}

		@Override
		public void testingFinished() {
			stopUpdateUIJob();
			resultsView.updateActionsFromSession();
			Display.getDefault().syncExec(() -> {
				uiChangesCache.applyChanges();
				resultsView.setCaption(testingSession.getStatusMessage());
				progressCountPanel.updateInfoFromSession();
				testsHierarchyViewer.getTreeViewer().refresh();
				testsHierarchyViewer.getTreeViewer().collapseAll();
				testsHierarchyViewer.getTreeViewer().expandToLevel(2);
			});
		}
	}

	/**
	 * Listener to handle active testing session change.
	 */
	private class TestingSessionsManagerListener implements ITestingSessionsManagerListener {

		@Override
		public void sessionActivated(ITestingSession newTestingSession) {
			if (testingSession != newTestingSession) {
				stopUpdateUIJob();
				uiChangesCache.resetChanges();

				unsubscribeFromSessionEvent();
				testingSession = newTestingSession;
				subscribeToSessionEvent();

				resultsView.updateActionsFromSession();
				Display.getDefault().syncExec(() -> {
					progressCountPanel.setTestingSession(testingSession);
					testsHierarchyViewer.setTestingSession(testingSession);
					resultsView.setCaption(testingSession != null ? testingSession.getStatusMessage() : ""); //$NON-NLS-1$
				});
				if (newTestingSession != null && !newTestingSession.isFinished()) {
					startUpdateUIJob();
				}
			}
		}
	}

	public UIUpdater(ResultsView resultsView, TestsHierarchyViewer testsHierarchyViewer,
			ProgressCountPanel progressCountPanel, TestingSessionsManager sessionsManager) {
		this.resultsView = resultsView;
		this.testsHierarchyViewer = testsHierarchyViewer;
		this.progressCountPanel = progressCountPanel;
		this.sessionsManager = sessionsManager;
		sessionListener = new SessionListener();
		sessionsManagerListener = new TestingSessionsManagerListener();
		sessionsManager.addListener(sessionsManagerListener);
	}

	/**
	 * Returns whether tests hierarchy scrolling should be done during the
	 * testing process.
	 *
	 * @return auto scroll state
	 */
	public boolean getAutoScroll() {
		return autoScroll;
	}

	/**
	 * Sets whether whether tests hierarchy scrolling should be done during the
	 * testing process.
	 *
	 * @param autoScroll new filter state
	 */
	public void setAutoScroll(boolean autoScroll) {
		this.autoScroll = autoScroll;
	}

	/**
	 * Disposes of the UI Updater. Make the necessary clean up.
	 */
	public void dispose() {
		unsubscribeFromSessionEvent();
		sessionsManager.removeListener(sessionsManagerListener);
	}

	/**
	 * Subscribes to the events of currently set testing session.
	 */
	private void subscribeToSessionEvent() {
		if (testingSession != null) {
			testingSession.getModelAccessor().addChangesListener(sessionListener);
		}
	}

	/**
	 * Unsubscribe from the events of currently set testing session.
	 */
	private void unsubscribeFromSessionEvent() {
		if (testingSession != null) {
			testingSession.getModelAccessor().removeChangesListener(sessionListener);
		}
	}

	/**
	 * Starts the UI updating job. Stops the previously running (if any).
	 */
	private void startUpdateUIJob() {
		stopUpdateUIJob();
		uiChangesCache.resetChanges();
		updateUIJob = new UpdateUIJob();
		updateUIJob.scheduleSelf();
	}

	/**
	 * Stops the UI updating job (if any).
	 */
	private void stopUpdateUIJob() {
		if (updateUIJob != null) {
			updateUIJob.stop();
			updateUIJob = null;
		}
	}

	/**
	 * Fakes the testing session activation and makes all necessary steps to
	 * handle it.
	 */
	public void reapplyActiveSession() {
		sessionsManagerListener.sessionActivated(sessionsManager.getActiveSession());
	}

}
