/*******************************************************************************
 * Copyright (c) 2016 Andrey Loskutov.
 *
 * 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:
 *     Andrey Loskutov <loskutov@gmx.de> - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.editors.tests;

import static org.eclipse.ui.editors.tests.FileDocumentProviderTest.closeIntro;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.nio.file.Files;
import java.nio.file.attribute.FileTime;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.eclipse.swt.widgets.Display;

import org.eclipse.core.internal.localstore.FileSystemResourceManager;
import org.eclipse.core.internal.resources.File;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;

import org.eclipse.core.resources.IFolder;

import org.eclipse.core.filebuffers.tests.ResourceHelper;

import org.eclipse.jface.action.IStatusLineManager;

import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.internal.dialogs.EventLoopProgressMonitor;

import org.eclipse.ui.editors.text.TextFileDocumentProvider;

/**
 * Test checking UI deadlock on modifying the TextFileDocumentProvider's
 * underlined file.
 */
public class TextFileDocumentProviderTest {

	private File file;
	private AtomicBoolean stoppedByTest;
	private AtomicBoolean stopLockingFlag;
	private LockJob lockJob;
	private LockJob2 lockJob2;
	private TextFileDocumentProvider fileProvider;
	private FileSystemResourceManager fsManager;
	private IEditorPart editor;
	private IWorkbenchPage page;

	@Before
	public void setUp() throws Exception {
		closeIntro(PlatformUI.getWorkbench());
		IFolder folder = ResourceHelper.createFolder("FileDocumentProviderTestProject/test");
		file = (File) ResourceHelper.createFile(folder, "file.txt", "");
		assertTrue(file.exists());
		fsManager = file.getLocalManager();
		assertTrue(fsManager.fastIsSynchronized(file));
		stopLockingFlag = new AtomicBoolean(false);
		stoppedByTest = new AtomicBoolean(false);
		fileProvider = new TextFileDocumentProvider();
		lockJob = new LockJob("Locking workspace", file, stopLockingFlag, stoppedByTest);
		lockJob2 = new LockJob2("Locking workspace", file, stopLockingFlag, stoppedByTest);

		// We need the editor only to get the default editor status line manager
		IWorkbench workbench = PlatformUI.getWorkbench();
		page = workbench.getActiveWorkbenchWindow().getActivePage();
		editor = IDE.openEditor(page, file);
		TestUtil.runEventLoop();

		IStatusLineManager statusLineManager = editor.getEditorSite().getActionBars().getStatusLineManager();
		// This is default monitor which almost all editors are using
		IProgressMonitor progressMonitor = statusLineManager.getProgressMonitor();
		assertNotNull(progressMonitor);
		assertFalse(progressMonitor instanceof NullProgressMonitor);
		assertFalse(progressMonitor instanceof EventLoopProgressMonitor);

		// Because this monitor is not EventLoopProgressMonitor, it will not
		// process UI events while waiting on workspace lock
		fileProvider.setProgressMonitor(progressMonitor);

		TestUtil.waitForJobs(500, 5000);
		Job[] jobs = Job.getJobManager().find(null);
		String jobsList = Arrays.toString(jobs);
		System.out.println("Still running jobs: " + jobsList);
		if (!Job.getJobManager().isIdle()) {
			jobs = Job.getJobManager().find(null);
			for (Job job : jobs) {
				System.out.println("Going to cancel: " + job.getName() + " / " + job);
				job.cancel();
			}
		}
	}

	@After
	public void tearDown() throws Exception {
		stopLockingFlag.set(true);
		lockJob.cancel();
		lockJob2.cancel();
		if (editor != null) {
			page.closeEditor(editor, false);
		}
		ResourceHelper.deleteProject(file.getProject().getName());
		TestUtil.runEventLoop();
		TestUtil.cleanUp();
	}

	@Test
	public void testSynchronizeInputWhileWorkspaceIsLocked1() throws Exception {
		// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=482354
		assertNotNull("Test must run in UI thread", Display.getCurrent());
		fileProvider.connect(editor.getEditorInput());

		// Start workspace job which will lock workspace operations on file via
		// rule
		lockJob.schedule();

		// touch the file of the editor
		makeSureResourceIsOutOfDate();

		// Put an UI event in the queue which will stop the workspace lock job
		// after a delay so that we can verify the UI events are still
		// dispatched after the call to refreshFile() below
		Display.getCurrent().timerExec(500, () -> {
			stopLockingFlag.set(true);
			System.out.println("UI event dispatched, lock removed");
		});

		// Original code will lock UI thread here because it will try to acquire
		// resource lock and no one will process UI events anymore
		fileProvider.synchronize(editor.getEditorInput());

		System.out.println("Busy wait terminated, UI thread is operable again!");
		assertFalse("Test deadlocked while waiting on resource lock", stoppedByTest.get());
		assertTrue(stopLockingFlag.get());
	}

	@Test
	public void testSynchronizeInputWhileWorkspaceIsLocked2() throws Exception {
		// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=482354
		assertNotNull("Test must run in UI thread", Display.getCurrent());

		fileProvider.connect(editor.getEditorInput());

		// Start workspace job which will lock workspace operations on file via
		// rule
		lockJob.schedule();

		// touch the file of the editor
		makeSureResourceIsOutOfDate();

		// Put an UI event in the queue which will stop the workspace lock job
		// after a delay so that we can verify the UI events are still
		// dispatched after the call to refreshFile() below
		Display.getCurrent().timerExec(500, () -> {
			stopLockingFlag.set(true);
			System.out.println("UI event dispatched, lock removed");
		});

		// Original code will lock UI thread here because it will try to acquire
		// resource lock and no one will process UI events anymore
		fileProvider.synchronize(editor.getEditorInput());

		System.out.println("Busy wait terminated, UI thread is operable again!");
		assertFalse("Test deadlocked while waiting on resource lock", stoppedByTest.get());
		assertTrue(stopLockingFlag.get());
	}

	@Test
	public void testValidateStateForFileWhileWorkspaceIsLocked() throws Exception {
		assertNotNull("Test must run in UI thread", Display.getCurrent());

		fileProvider.connect(editor.getEditorInput());

		// Start workspace job which will lock workspace operations on file
		lockJob2.schedule();

		Thread.sleep(100);

		// Put an UI event in the queue which will stop the workspace lock job
		// after a delay
		Display.getCurrent().timerExec(600, () -> {
			stopLockingFlag.set(true);
			System.out.println("UI event dispatched, lock removed");
		});

		// Original code will lock UI thread here because it will try to acquire
		// workspace lock and no one will process UI events anymore
		fileProvider.validateState(editor.getEditorInput(), editor.getSite().getShell());

		System.out.println("Busy wait terminated, UI thread is operable again!");
		assertFalse("Test deadlocked while waiting on resource lock", stoppedByTest.get());
		assertTrue(stopLockingFlag.get());
	}

	/*
	 * Set current time stamp via java.nio to make sure
	 * org.eclipse.core.internal.resources.File.refreshLocal(int,
	 * IProgressMonitor) will call super.refreshLocal(IResource.DEPTH_ZERO,
	 * monitor) and so lock the UI by trying to access resource locked by the
	 * job
	 */
	private void makeSureResourceIsOutOfDate() throws Exception {
		int count = 0;
		Files.setLastModifiedTime(file.getLocation().toFile().toPath(),
				FileTime.fromMillis(System.currentTimeMillis()));
		// Give the file system a chance to have a *different* timestamp
		Thread.sleep(100);
		while (fsManager.fastIsSynchronized(file) && count < 1000) {
			Files.setLastModifiedTime(file.getLocation().toFile().toPath(),
					FileTime.fromMillis(System.currentTimeMillis()));
			Thread.sleep(10);
			count++;
		}
		System.out.println("Managed to update file after " + count + " attempts");
		assertFalse(fsManager.fastIsSynchronized(file));
	}

}


