/* 
-*- coding: latin-1 -*-

This file is part of RefactorErl.

RefactorErl is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

RefactorErl is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with RefactorErl.  If not, see <http://plc.inf.elte.hu/erlang/>.

The Original Code is RefactorErl.

The Initial Developer of the Original Code is Eötvös Loránd University.
Portions created  by Eötvös Loránd University and ELTE-Soft Ltd.
are Copyright 2007-2025 Eötvös Loránd University, ELTE-Soft Ltd.
and Ericsson Hungary. All Rights Reserved.
*/

package com.refactorerl.ui.presentation;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.math.BigInteger;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.ConsoleHandler;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.SimpleFormatter;

import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.eclipse.ui.progress.UIJob;
import org.osgi.framework.BundleContext;

import com.refactorerl.ui.common.Environment;
import com.refactorerl.ui.communication.MessageTopicHelper;
import com.refactorerl.ui.communication.ReferlProxy;
import com.refactorerl.ui.communication.event.IEventHandler;
import com.refactorerl.ui.communication.exceptions.AbortRequestException;
import com.refactorerl.ui.communication.exceptions.CommunicationException;
import com.refactorerl.ui.communication.exceptions.ConnectionException;
import com.refactorerl.ui.communication.exceptions.ReferlException;
import com.refactorerl.ui.communication.exceptions.RequestException;
import com.refactorerl.ui.communication.process.IOHandler;
import com.refactorerl.ui.communication.process.ReferlProcess;
import com.refactorerl.ui.communication.process.ReferlProcess.DatabaseType;
import com.refactorerl.ui.logic.AbstractRequest;
import com.refactorerl.ui.presentation.event.ReferlEventDispatcher;
import com.refactorerl.ui.presentation.filelist.FileListView;

/**
 * The activator class controls the plug-in life cycle. This class is
 * responsible for initializing and making accessible any source of data, which
 * the plugin's handlers called by the Eclipse Runtime will need. The class
 * itself will be initialized as a singleton by the Eclipse Runtime, the start() must
 * not be called externally.
 * 
 * @author Daniel Lukacs, 2014 ELTE IK
 *
 */
public class Activator extends AbstractUIPlugin {

	public static class ErrorContainer{
		public boolean hasError = false;
		public String errorMsg = "";
		public boolean activate(String errorMsg){
			if(!hasError){
				hasError = true;
				this.errorMsg = errorMsg;
				return true;
			} else return false;
		}		
	}
	
	// The plug-in ID
	public static final String PLUGIN_ID = "RefactorErl"; //$NON-NLS-1$
	/**
	 * Internal string used for sending and receiving events which should
	 * trigger a status bar update.
	 */
	public static final String STATUS_BAR_EVENT_CHANNEL = "com_refactorerl_ui_statusbar"; //$NON-NLS-1$

	private static final int LOG_SIZE = 1000000 ; // 1 MB
	private static final int LOG_CYCLES = 3; 
	private static final String COMM_LOG_NAME = "communication.log";
	private static final String PROCESS_LOG_NAME = "process.log";

	// The shared instance
	private static Activator plugin;

	/**
	 * The constructor
	 */
	public Activator() {
	}

	private ReferlProcess process;

	private static Random random = new Random();

	private ReferlProxy proxy;

	private ReferlEventDispatcher edp;

	private StatusBarEventHandler statusBarHandler;

	/**
	 * Used for locking when a modifier request is in progress. Requests not
	 * able to acquire a lock will be queued until a suitable lock will be
	 * available for them.
	 */
	private volatile ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true);

	private IResourceChangeListener rcl = null;

	private boolean started = false;

	private IEventHandler changeEventHandler = null;

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext
	 * )
	 */
	/**
	 * Initializes the plugin-wide EventDispatcher. Initializes a
	 * ReferlResourceChangeListener which will handle the changes of those local
	 * resources, which were added to the RefactorErl database. Initializes a
	 * ReferlChangeEventHandler which will respond to progress events sent from RefactorErl, and
	 * StatusBarEventHandler which will react to events sent to the
	 * STATUS_BAR_EVENT_CHANNEL topic and display the message contained by the
	 * event in the Workbench status bar.
	 */
	public void start(BundleContext context) throws Exception {
		super.start(context);
		plugin = this;

		rcl = new ReferlResourceChangeListener(getDefault().getWorkbench().getActiveWorkbenchWindow());

		edp = new ReferlEventDispatcher();
		edp.start();

		restart();

		statusBarHandler = new StatusBarEventHandler();
		edp.subscribe(STATUS_BAR_EVENT_CHANNEL, statusBarHandler);

		ResourcesPlugin.getWorkspace().addResourceChangeListener(rcl);

		changeEventHandler = new ReferlChangeEventHandler();

		Activator.getEdp().subscribe(MessageTopicHelper.getStatusInfoTopic(), changeEventHandler);
	}

	/**
	 * If local mode is set in the preferences, it will attempt to start a
	 * RefactorErl server process. If this succeeds, it will attempt to connect
	 * to this server. If local mode is not set, it will attempt to connect to a
	 * server on the address set in the preferences.
	 */
	public void restart() {
		stopProxy();
		stopProcess();

		new Job(Messages.Activator_1) {

			@Override
			protected IStatus run(IProgressMonitor monitor) {
				showInfoOnStatusBar(null);
				
				if (getDefault().getPreferenceStore().getBoolean("local_server")) { //$NON-NLS-1$
					if (!startReferlProcess()) {
						started = false;
						showInfoOnStatusBar("RefactorErl was unable to start.");
						return Status.OK_STATUS;
					} 
						showInfoOnStatusBar(Messages.Activator_3);
				}

				started = startProxy();

				if (started) {
					showInfoOnStatusBar(Messages.Activator_4);
					new UIJob(Messages.Activator_5) {
						@Override
						public IStatus runInUIThread(IProgressMonitor monitor) {
							final FileListView fileListView = (FileListView) getDefault().getWorkbench()
									.getActiveWorkbenchWindow().getActivePage().findView("com.refactorerl.ui.fileListView"); //$NON-NLS-1$
							if (fileListView != null)
								fileListView.reload();
							return Status.OK_STATUS;
						}

					}.schedule();

				} else {
					showInfoOnStatusBar("Connecting to RefactorErl failed.");
				}

				return Status.OK_STATUS;
			}

		}.schedule();

	}

	
	private boolean startProxy() {
		
		final String remoteServerAddress = getDefault().getPreferenceStore().getString("server_address"); //$NON-NLS-1$
	
		boolean localMode = getDefault().getPreferenceStore().getBoolean("local_server");
		final String serverAddress = localMode ? process.getFullAddress() : remoteServerAddress; //$NON-NLS-1$
		String cookie = localMode ? null : getDefault().getPreferenceStore().getString("cookie");
		final String clientName = localMode ? 
				getDefault().getPreferenceStore().getDefaultString("client_alivename") //$NON-NLS-1$
				: getDefault().getPreferenceStore().getString("client_alivename"); //$NON-NLS-1$
	
		proxy = new ReferlProxy(edp, clientName, cookie, serverAddress,  
				createLogHandlers(getCommunicationsLogDir()));

		if (!proxy.start()) {
			proxy = null;
			showErrorDialog(Messages.Activator_15, Messages.Activator_16 + Messages.Activator_17
					+ Messages.Activator_18 + getDefault().getPreferenceStore().getString("logs_dir")); //$NON-NLS-1$
			return false;
		}

		return true;
	}

	private boolean startReferlProcess() {

		String baseDir = getDefault().getPreferenceStore().getString("refactorerl_base_dir"); //$NON-NLS-1$
		if (baseDir == null || baseDir.isEmpty()) {
			showErrorDialog(Messages.Activator_22, Messages.Activator_23 + Messages.Activator_24);
			return false;
		}
		
		ReferlProcess.DatabaseType databaseType = DatabaseType.valueOf(getDefault().getPreferenceStore().getString("db_type")); //$NON-NLS-1$
		String dataDir = getDefault().getPreferenceStore().getString("refactorerl_data_dir"); //$NON-NLS-1$
		String localServerAddress = getDefault().getPreferenceStore().getString("local_server_address");
		
		IOHandler ioHandler =  getDefault().getPreferenceStore().getBoolean("debug_mode") ?
				new DebugConsoles.DebugConsole() : null; 
		
		process = new ReferlProcess(baseDir, localServerAddress, databaseType, 
				dataDir, createLogHandlers(getProcessLogDir()), ioHandler); //$NON-NLS-1$
		if (!process.start()) {
			process = null;
			showErrorDialog(Messages.Activator_29, Messages.Activator_30 + Messages.Activator_31
					+ Messages.Activator_32); //$NON-NLS-1$
			return false;

		}
		return true;
	}

	static public ReferlProxy getProxy() {
		return getDefault().proxy;
	}

	static public ReferlProcess getProcess() {
		return getDefault().process;
	}

	static public ReferlEventDispatcher getEdp() {
		return getDefault().edp;
	}

	static public boolean isStarted() {
		return getDefault().started;
	}

	private void stopProxy() {
		if (getDefault().proxy != null) {
			getDefault().proxy.stop();
			getDefault().proxy = null;
		}
	}

	private void stopProcess() {
		if (getDefault().process != null) {
			getDefault().process.stop();
			getDefault().process = null;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext
	 * )
	 */
	/**
	 * Stops every service started in the start() method. Removes the temporary
	 * files created in the External Files project.
	 */
	public void stop(BundleContext context) throws Exception {

		Activator.getEdp().unsubscribe(changeEventHandler);
		ResourcesPlugin.getWorkspace().removeResourceChangeListener(rcl);

		// cleaning up after RemoteTemporaryFileEditorInput
		IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject("External Files"); //$NON-NLS-1$

		if (project.exists()) {
			IFolder folder = project.getFolder(getLocalTempStorePrefix());
			if (folder.exists())
				folder.delete(false, null);

			project.refreshLocal(IResource.DEPTH_INFINITE, null);
		}
		
		// removing temp dir
		removeTempDir();
		
		// cleaning up after connection and event system
		edp.unsubscribe(statusBarHandler);

		super.stop(context);

		stopProxy();

		stopProcess();

		edp.stop();
		plugin = null;
	}

	/**
	 * Returns the shared instance
	 *
	 * @return the shared instance
	 */
	public static Activator getDefault() {
		return plugin;
	}

	private static void showErrorOnStatusBar(String message) {
		getEdp().fire(STATUS_BAR_EVENT_CHANNEL,
				new StatusBarEventHandler.StatusMessage(StatusBarEventHandler.Status.ERROR, message));
	}

	private static void showInfoOnStatusBar(String message) {
		getEdp().fire(STATUS_BAR_EVENT_CHANNEL,
				new StatusBarEventHandler.StatusMessage(StatusBarEventHandler.Status.INFO, message));
	}

	private void showErrorDialog(final String title, final String message) {
		new UIJob(title) {

			@Override
			public IStatus runInUIThread(IProgressMonitor monitor) {
				MessageDialog.openError(getDefault().getWorkbench().getActiveWorkbenchWindow().getShell(), title,
						message);
				return Status.OK_STATUS;
			}

		}.schedule();

	}

	public static void showErrorDialog(Exception e, Shell sh) {
		// e.printStackTrace();
		Map<Class<? extends ReferlException>, String> titles = new HashMap<>();
		titles.put(AbortRequestException.class, Messages.Activator_35);

		final String title = titles.get(e.getClass()) != null ? titles.get(e.getClass()) : e.getClass().getSimpleName();
		final String msg = e.getMessage();

		Activator.getDefault().showErrorDialog(title, msg);

	}

	private static Handler[] createLogHandlers(String filepath) {
		// todo: clear log directory
		try {
			FileHandler fh =  new FileHandler(filepath,LOG_SIZE, LOG_CYCLES, true); 
			fh.setLevel(Level.ALL);
			fh.setFormatter(new SimpleFormatter());

			if(getDefault().getPreferenceStore().getBoolean("debug_mode")){
				ConsoleHandler ch = new ConsoleHandler();
				ch.setLevel(Level.ALL);
				return new Handler[] { fh, ch };
			} else {
				return new Handler[] { fh};
			}			

		} catch (SecurityException | FileNotFoundException e) {
			System.err.println(Messages.Activator_36);

		} catch (IOException e) {
			System.err.println(Messages.Activator_37);
		}
		return null;
	}
	
	private static String getLogDir(){
		String logsDir = getDefault().getPreferenceStore().getString("logs_dir"); //$NON-NLS-1$

		File logsDirFile = new File(logsDir);
		if (!logsDirFile.exists() || !logsDirFile.isDirectory())
			logsDirFile.mkdir();
		
		return logsDir;
	}

	private static String getProcessLogDir(){		
		return getLogDir() + File.separator + PROCESS_LOG_NAME; //$NON-NLS-1$
	}
	
	private static String getCommunicationsLogDir(){		
		return getLogDir() + File.separator + COMM_LOG_NAME; //$NON-NLS-1$
	}
	
	public static void resetLogHandlers(){
	
		if(getProcess() != null){
			Handler[] phs = createLogHandlers(getProcessLogDir());
			getProcess().resetLogHandlers(phs);		
		}
		if(getProxy() != null){
			Handler[] chs = createLogHandlers(getCommunicationsLogDir());
			getProxy().resetLogHandlers(chs);
		}
	}

	public static String generateRandomName() {
		String name = "ecl" + new BigInteger(130, random).toString(32).substring(0, 7); //$NON-NLS-1$
		return name;
	}

	static Cursor waitCursor = null;

	/**
	 * 
	 * @param composite
	 * @param busy
	 *            If true, switches the cursor icon to busy status on the
	 *            supplied composite. If false, it switches the cursor back to
	 *            normal.
	 */
	public static void showBusy(Composite composite, boolean busy) {

		if (busy) {
			if (waitCursor == null)
				waitCursor = new Cursor(composite.getDisplay(), SWT.CURSOR_WAIT);

		} else {
			if (waitCursor != null) {
				waitCursor.dispose();
				waitCursor = null;
			}
		}
		composite.setCursor(waitCursor);

	}

	/**
	 * @return The directory name in the External Files for storing temporary
	 *         Eclipse resources.
	 */
	public static String getLocalTempStorePrefix() {
		return "com.refactorerl.ui"; //$NON-NLS-1$
	}

	private static synchronized ReentrantReadWriteLock getReadWriteLock() {
		return getDefault().rwLock;
	}

	public static <T> T executeRequest(AbstractRequest<T> req, Shell shell) {
		return executeRequest(req, shell, false);
	}
	
	public static <T> T executeRequest(AbstractRequest<T> req, Shell shell, boolean suppressErrorMessage){
		return executeRequest(req, shell, false, null);
	}

	/**
	 * This method should be used by every class in the presentation layer to
	 * execute requests. This will handle plugin-wise concurrency regarding the
	 * modifier and non-modifier nature of requests. Modifier requests will
	 * never be executed concurrently with any other request. This is
	 * implemented internally with a read-write-lock. If the call of this method
	 * would result in the concurrent execution with a modifier request, the
	 * supplied request will be queued and will be executed when the execution
	 * of the request becomes possible. Until then the method blocks.
	 * 
	 * @param req
	 * @param shell
	 * @param suppressErrorMessage
	 *            If the request server auxiliary purposes, the caller can set
	 *            this true so no error dialog will be shown if the requests
	 *            fail, only an error message in the status bar.
	 * @param errorContainer Contains whether the request has failed.          
	 * @return The return value of the supplied request.
	 */	
	public static <T> T executeRequest(final AbstractRequest<T> req, final Shell shell, final boolean suppressErrorMessage, ErrorContainer errorContainer) {
		
		// if not supplied, we create a new one which no one will read
		final ErrorContainer err = errorContainer == null ? new ErrorContainer() : errorContainer;
		
		if (req.isModifier()) {
			class Wrapper {
				T result = null;
				// responsible for blocking the calling thread, until we can
				// return with the result
				CountDownLatch lock = new CountDownLatch(1);
			}

			final Wrapper w = new Wrapper();

			try {

				IRunnableWithProgress runnable = new IRunnableWithProgress() {
					@Override
					public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
						Activator.getReadWriteLock().writeLock().lock();
						try {
							ReferlProgressMonitor rmon = new ReferlProgressMonitor(monitor, shell);
							if (!req.hasProgressEvent()) {
								rmon.beginTask(req.toString(), 100);
								rmon.worked(50, req.toString());
								w.result = req.execute(null);
							} else {
								w.result = req.execute(rmon);
							}
						} catch (RequestException | ConnectionException | CommunicationException e) {
							if (suppressErrorMessage)
								showErrorOnStatusBar(req.getClass() + Messages.Activator_40 + e.getMessage());
							else
								showErrorDialog(e, shell);
							
							err.activate(e.getMessage());
							
						} catch (Exception e) {
							// we wont let any exception out, since the locks
							// must be unlocked
							e.printStackTrace();
							err.activate(e.getMessage());
						}
						Activator.getReadWriteLock().writeLock().unlock();
						w.lock.countDown();
					}
				};
				ProgressMonitorDialog progressDialog = new ProgressMonitorDialog(shell);
				
				// NOTE fork=true is important here
				progressDialog.run(true, false, runnable);

				w.lock.await();

			} catch (InvocationTargetException | InterruptedException e) {
				if (suppressErrorMessage)
					showErrorOnStatusBar(req.getClass() + Messages.Activator_41 + e.getMessage());
				else
					Activator.showErrorDialog(e, shell);
				
				err.activate(err.errorMsg);
			}
			return w.result;
		} else {

			Activator.getReadWriteLock().readLock().lock();

			T result = null;
			try {
				result = req.execute();
			} catch (RequestException | ConnectionException | CommunicationException e) {
				if (suppressErrorMessage)
					showErrorOnStatusBar(req.getClass() + Messages.Activator_42 + e.getMessage());
				else
					Activator.showErrorDialog(e, shell);

				result = null;
				err.activate(e.getMessage());
			} catch (Exception e) {
				// we wont let any exception out, since the locks must be
				// unlocked
				e.printStackTrace();
				result = null;
				err.activate(e.getMessage());
			}

			Activator.getReadWriteLock().readLock().unlock();
			return result;
		}
	}
	
	private void removeTempDir() throws IOException {
		Path directory = Paths.get(Environment.getTempDir().getPath());
		
		SimpleFileVisitor<Path> visitor = new SimpleFileVisitor<Path>() {
		   @Override
		   public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
			   Files.delete(file);
			   return FileVisitResult.CONTINUE;
		   }

		   @Override
		   public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
		       if (exc == null) {
                 Files.delete(dir);
                 return FileVisitResult.CONTINUE;
		       } else {
                 throw exc; // directory iteration failed
		       }
			
		   }
		};
		if(directory.endsWith(Environment.TMP_DIR_NAME)) // just making sure
			Files.walkFileTree(directory, visitor); 
		
		
	}


}
