/*******************************************************************************
 * Copyright (c) 2018, 2020 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.codewind.core.internal.connection;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.eclipse.codewind.core.internal.CodewindApplication;
import org.eclipse.codewind.core.internal.CodewindApplicationFactory;
import org.eclipse.codewind.core.internal.CoreUtil;
import org.eclipse.codewind.core.internal.HttpUtil;
import org.eclipse.codewind.core.internal.Logger;
import org.eclipse.codewind.core.internal.cli.AuthToken;
import org.eclipse.codewind.core.internal.cli.ProjectLinks.LinkInfo;
import org.eclipse.codewind.core.internal.console.ProjectLogInfo;
import org.eclipse.codewind.core.internal.console.SocketConsole;
import org.eclipse.codewind.core.internal.constants.CoreConstants;
import org.eclipse.codewind.core.internal.constants.ProjectType;
import org.eclipse.codewind.core.internal.constants.StartMode;
import org.eclipse.codewind.core.internal.messages.Messages;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.osgi.util.NLS;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import io.socket.client.IO;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;
import okhttp3.OkHttpClient;

/**
 * Wrapper for a SocketIO client socket, which connects to Codewind and listens for project state changes,
 * then updates the corresponding CodewindApplication's state.
 * One of these exists for each CodewindConnection. That connection is stored here so we can access
 * its applications.
 */
public class CodewindSocket {
	
	private final CodewindConnection connection;

	public final Socket socket;

	public final URI socketUri;

	private boolean hasLostConnection = false;

	private volatile boolean hasConnected = false;

	private Set<SocketConsole> socketConsoles = new HashSet<>();
	
	// Track the previous Exception so we don't spam the logs with the same connection failure message
	private Exception previousException;

	// SocketIO Event names
	private static final String
			EVENT_PROJECT_CREATION = "projectCreation",				//$NON-NLS-1$
			EVENT_PROJECT_CHANGED = "projectChanged", 				//$NON-NLS-1$
			EVENT_PROJECT_STATUS_CHANGE = "projectStatusChanged", 	//$NON-NLS-1$
			EVENT_PROJECT_RESTART = "projectRestartResult", 		//$NON-NLS-1$
			EVENT_PROJECT_CLOSED = "projectClosed", 				//$NON-NLS-1$
			EVENT_PROJECT_DELETION = "projectDeletion", 			//$NON-NLS-1$
			EVENT_PROJECT_VALIDATED = "projectValidated",			//$NON-NLS-1$
			EVENT_LOG_UPDATE = "log-update",						//$NON-NLS-1$
			EVENT_PROJECT_LOGS_LIST_CHANGED = "projectLogsListChanged",		//$NON-NLS-1$
			EVENT_PROJECT_SETTINGS_CHANGED = "projectSettingsChanged",	//$NON-NLS-1$
			EVENT_PROJECT_WATCH_STATUS_CHANGED = "projectWatchStatusChanged",	//$NON-NLS-1$
			EVENT_PROJECT_LINK = "projectLink", //$NON-NLS-1$
			EVENT_AUTHENTICATED = "authenticated", //$NON-NLS-1$
			EVENT_UNAUTHORIZED = "unauthorized"; //$NON-NLS-1$

	public CodewindSocket(CodewindConnection connection) throws URISyntaxException, IOException, JSONException {
		this.connection = connection;
		
		URI uri = connection.getBaseURI();
		if (connection.getSocketNamespace() != null) {
			uri = uri.resolve(connection.getSocketNamespace());
		}
		socketUri = uri;

		OkHttpClient.Builder builder = new OkHttpClient.Builder();
		if (connection.getAuthToken(false) != null) {
			builder
				.hostnameVerifier(HttpUtil.hostnameVerifier)
				.sslSocketFactory(HttpUtil.sslContext.getSocketFactory(), HttpUtil.trustManager);
		}
		OkHttpClient okHttpClient = builder
				.readTimeout(0L, TimeUnit.MILLISECONDS)
				.build();
		IO.setDefaultOkHttpCallFactory(okHttpClient);
		IO.setDefaultOkHttpWebSocketFactory(okHttpClient);
		IO.Options opts = new IO.Options();
		opts.callFactory = okHttpClient;
		opts.webSocketFactory = okHttpClient;
		socket = IO.socket(socketUri, opts);
		
		socket.on(Socket.EVENT_CONNECT, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				try {
					AuthToken authToken = connection.getAuthToken(false);
					if (authToken != null && authToken.getToken() != null) {
						JSONObject obj = new JSONObject();
						obj.put("token", authToken.getToken());
						socket.emit("authentication", obj);
					}
				} catch (Exception e) {
					Logger.logError("An error occurred trying to pass the authentication token to the socket", e);
					return;
				}
				Logger.log("SocketIO connect success @ " + socketUri); //$NON-NLS-1$
				if (!hasConnected) {
					hasConnected = true;
				}
				if (hasLostConnection) {
					connection.clearConnectionError();
					previousException = null;
				}
			}
		})
		.on(EVENT_AUTHENTICATED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log("SocketIO authentication successful");
			}
		})
		.on(EVENT_UNAUTHORIZED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.logError("SocketIO authentication failed: " + arg0[0]);
				// Completely disconnect in this case
				connection.disconnect();
				CoreUtil.updateConnection(connection);
				CoreUtil.openDialog(true, Messages.Connection_ErrConnection_AuthFailedTitle, NLS.bind(Messages.Connection_ErrConnection_AuthFailed, connection.getName()));
			}
		})
		.on(Socket.EVENT_CONNECT_ERROR, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				if (arg0[0] instanceof Exception) {
					Exception e = (Exception) arg0[0];
					if (previousException == null || !e.getMessage().equals(previousException.getMessage())) {
						previousException = e;
						Logger.logError("SocketIO Connect Error @ " + socketUri, e); //$NON-NLS-1$
					}
				}
				connection.onConnectionError();
				hasLostConnection = true;
			}
		})
		.on(Socket.EVENT_ERROR, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				if (arg0[0] instanceof Exception) {
					Exception e = (Exception) arg0[0];
					Logger.logError("SocketIO Error @ " + socketUri, e); //$NON-NLS-1$
				}
			}
		})
		.on(Socket.EVENT_MESSAGE, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				// Don't think this is ever used
				Logger.log("SocketIO EVENT_MESSAGE " + arg0[0].toString()); //$NON-NLS-1$
			}
		})
		.on(EVENT_PROJECT_CREATION, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_CREATION + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectCreation(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_CHANGED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_CHANGED + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectChanged(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_SETTINGS_CHANGED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_SETTINGS_CHANGED + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectSettingsChanged(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_STATUS_CHANGE, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_STATUS_CHANGE + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectStatusChanged(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_RESTART, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_RESTART + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectRestart(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_CLOSED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_CLOSED + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectClosed(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_DELETION, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_DELETION + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectDeletion(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_LOGS_LIST_CHANGED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_LOGS_LIST_CHANGED + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectLogsListChanged(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_LOG_UPDATE, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				// can't print this whole thing because the logs strings flood the output
				Logger.log(EVENT_LOG_UPDATE);

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onLogUpdate(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		})
		.on(EVENT_PROJECT_VALIDATED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_VALIDATED + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onValidationEvent(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		}).on(EVENT_PROJECT_WATCH_STATUS_CHANGED, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_WATCH_STATUS_CHANGED + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectWatchStatusChanged(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		}).on(EVENT_PROJECT_LINK, new Emitter.Listener() {
			@Override
			public void call(Object... arg0) {
				Logger.log(EVENT_PROJECT_LINK + ": " + arg0[0].toString()); //$NON-NLS-1$

				try {
					JSONObject event = new JSONObject(arg0[0].toString());
					onProjectLink(event);
				} catch (JSONException e) {
					Logger.logError("Error parsing JSON: " + arg0[0].toString(), e); //$NON-NLS-1$
				}
			}
		});

		socket.connect();

		Logger.log("Created CodewindSocket connected to " + socketUri); //$NON-NLS-1$
	}
	
	public void close() {
		if (socket != null) {
			if (socket.connected()) {
				socket.disconnect();
			}
			socket.close();
		}
	}
	
	private void onProjectCreation(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		connection.refreshApps(projectID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app != null) {
			app.setEnabled(true);
		} else {
			Logger.logError("No application found matching the project id for the project creation event: " + projectID); //$NON-NLS-1$
		}
		CoreUtil.updateConnection(connection);
	}

	private void onProjectChanged(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found matching the project id for the project changed event: " + projectID); //$NON-NLS-1$
			return;
		}
		
		CodewindApplicationFactory.updateApp(app, event);

		// Reconnect debugger if necessary
		if (StartMode.DEBUG_MODES.contains(app.getStartMode()) && app.getDebugPort() != -1) {
			app.reconnectDebugger();
		}
		
		CoreUtil.updateApplication(app);
	}
	
	private void onProjectSettingsChanged(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found matching the project id for the project settings changed event: " + projectID); //$NON-NLS-1$
			return;
		}
		
		app.setEnabled(true);
		
		// Check the status
		if (event.has(CoreConstants.KEY_STATUS)) {
			String status = event.getString(CoreConstants.KEY_STATUS);
			if (!CoreConstants.VALUE_STATUS_SUCCESS.equals(status)) {
				if (event.has(CoreConstants.KEY_ERROR)) {
					String errorMsg = event.getString(CoreConstants.KEY_ERROR);
					CoreUtil.openDialog(true, Messages.ProjectSettingsUpdateErrorTitle, errorMsg);
				} else {
					Logger.logError("The project settings request failed but there is no error message in the result");
				}
				return;
			}
		}
		
		// Update project
		if (event.has(CoreConstants.KEY_CONTEXT_ROOT)) {
			app.setContextRoot(event.getString(CoreConstants.KEY_CONTEXT_ROOT));
		}
		CodewindApplicationFactory.setPorts(event, app);
		
		CoreUtil.updateApplication(app);
	}

	private void onProjectStatusChanged(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			// Likely a new project is being created
			connection.refreshApps(projectID);
			CoreUtil.updateConnection(connection);
			return;
		}
		
		CodewindApplicationFactory.updateApp(app, event);
		CoreUtil.updateApplication(app);
	}

	private void onProjectRestart(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found matching the project id for the project restart event: " + projectID); //$NON-NLS-1$
			return;
		}
		
		app.setEnabled(true);
		
		String status = event.getString(CoreConstants.KEY_STATUS);
		if (!CoreConstants.REQUEST_STATUS_SUCCESS.equalsIgnoreCase(status)) {
			Logger.logError("Project restart failed on the application: " + event.toString()); //$NON-NLS-1$
			CoreUtil.openDialog(true,
					Messages.Socket_ErrRestartingProjectDialogTitle,
					NLS.bind(Messages.Socket_ErrRestartingProjectDialogMsg,
							app.name, status));
			return;
		}
		
		// Set the application base URL
		if (event.has(CoreConstants.KEY_APP_BASE_URL)) {
			app.setAppBaseUrl(event.getString(CoreConstants.KEY_APP_BASE_URL));
		}
		
		// Set the pod name
		if (event.has(CoreConstants.KEY_POD_NAME)) {
			app.setPodName(event.getString(CoreConstants.KEY_POD_NAME));
		}
		
		// Update the ports
		CodewindApplicationFactory.setPorts(event, app);
		
		StartMode startMode = StartMode.get(event);
		app.setStartMode(startMode);
		
		if (event.has(CoreConstants.KEY_CONTAINER_ID)) {
		    String containerId = event.getString(CoreConstants.KEY_CONTAINER_ID);
		    app.setContainerId(containerId);
		}
		
		// Update the application
		CoreUtil.updateApplication(app);
		
		// Make sure no old debugger is running
		app.clearDebugger();
		
		if (app.readyForDebugSession()) {
			app.connectDebugger();
		}
	}
	
	private void onProjectClosed(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found for project being closed: " + projectID); //$NON-NLS-1$
			return;
		}
		app.connection.refreshApps(app.projectID);
		CoreUtil.updateApplication(app);
	}

	private void onProjectDeletion(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.log("No application found for project being deleted: " + projectID);
			return;
		}
		connection.removeApp(projectID);
	}

	public void registerSocketConsole(SocketConsole console) {
		Logger.log("Register socketConsole for project: " + console.app.name); //$NON-NLS-1$
		this.socketConsoles.add(console);
	}

	public void deregisterSocketConsole(SocketConsole console) {
		this.socketConsoles.remove(console);
	}

	private void onLogUpdate(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		String type = event.getString(CoreConstants.KEY_LOG_TYPE);
		String logName = event.getString(CoreConstants.KEY_LOG_NAME);
		Logger.log("Update the " + logName + " log for project: " + projectID); //$NON-NLS-1$ //$NON-NLS-2$

		for (SocketConsole console : this.socketConsoles) {
			if (console.app.projectID.equals(projectID) && console.logInfo.isThisLogInfo(type, logName)) {
				try {
					String logContents = event.getString(CoreConstants.KEY_LOGS);
					boolean reset = event.getBoolean(CoreConstants.KEY_LOG_RESET);
					console.update(logContents, reset);
				}
				catch(IOException e) {
					Logger.logError("Error updating console " + console.getName(), e);	// $NON-NLS-1$
				}
			}
		}
	}
	
	private void onProjectLogsListChanged(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			// Likely a new project is being created
			connection.refreshApps(projectID);
			CoreUtil.updateConnection(connection);
			return;
		}

		String type;
		if (event.has(CoreConstants.KEY_LOG_BUILD)) {
			type = CoreConstants.KEY_LOG_BUILD;
			JSONArray logs = event.getJSONArray(CoreConstants.KEY_LOG_BUILD);
			List<ProjectLogInfo> logInfos = CodewindConnection.getLogs(logs, type);
			app.addLogInfos(logInfos);
		}
		if (event.has(CoreConstants.KEY_LOG_APP)) {
			type = CoreConstants.KEY_LOG_APP;
			JSONArray logs = event.getJSONArray(CoreConstants.KEY_LOG_APP);
			List<ProjectLogInfo> logInfos = CodewindConnection.getLogs(logs, type);
			app.addLogInfos(logInfos);
		}
		CoreUtil.updateApplication(app);
	}
	
	private void onValidationEvent(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found for project: " + projectID); //$NON-NLS-1$
			return;
		}
		
		// Clear out any old validation objects
		app.resetValidation();
		
		// If the validation is successful then just return
		String status = event.getString(CoreConstants.KEY_VALIDATION_STATUS);
		if (CoreConstants.VALUE_STATUS_SUCCESS.equals(status)) {
			// Nothing to do
			return;
		}
		
		// If the validation is not successful, create validation objects for each problem
		if (event.has(CoreConstants.KEY_VALIDATION_RESULTS)) {
			JSONArray results = event.getJSONArray(CoreConstants.KEY_VALIDATION_RESULTS);
			for (int i = 0; i < results.length(); i++) {
				JSONObject result = results.getJSONObject(i);
				String severity = result.getString(CoreConstants.KEY_SEVERITY);
				String filename = result.getString(CoreConstants.KEY_FILENAME);
				String filepath = result.getString(CoreConstants.KEY_FILEPATH);
				String type = null;
				if (result.has(CoreConstants.KEY_TYPE)) {
					type = result.getString(CoreConstants.KEY_TYPE);
				}
				String details = result.getString(CoreConstants.KEY_DETAILS);
				String quickFixId = null;
				String quickFixDescription = null;
				if (result.has(CoreConstants.KEY_QUICKFIX) && supportsQuickFix(app, type, filename)) {
					JSONObject quickFix = result.getJSONObject(CoreConstants.KEY_QUICKFIX);
					quickFixId = quickFix.getString(CoreConstants.KEY_FIXID);
					quickFixDescription = quickFix.getString(CoreConstants.KEY_DESCRIPTION);
				}
				if (CoreConstants.VALUE_SEVERITY_WARNING.equals(severity)) {
					app.validationWarning(filepath, details, quickFixId, quickFixDescription);
				} else {
					app.validationError(filepath, details, quickFixId, quickFixDescription);
				}
			}
		} else {
			Logger.log("Validation event indicates failure but no validation results,"); //$NON-NLS-1$
		}
	}
	
	private void onProjectWatchStatusChanged(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found matching the project id for the project watch status changed event: " + projectID); //$NON-NLS-1$
			return;
		}
		
		app.setEnabled(true);
		
		if (event.has(CoreConstants.KEY_STATUS) && !CoreConstants.VALUE_STATUS_SUCCESS.equals(event.getString(CoreConstants.KEY_STATUS))) {
			// Just log for now until the JSON object includes the path that caused the issue
			Logger.logError("Project watch status failure for: " + app.name + ", with id: " + app.projectID); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}
	
	private void onProjectLink(JSONObject event) throws JSONException {
		String projectID = event.getString(CoreConstants.KEY_PROJECT_ID);
		CodewindApplication app = connection.getAppByID(projectID);
		if (app == null) {
			Logger.logError("No application found matching the project id for the project link event: " + projectID); //$NON-NLS-1$
			return;
		}
		
		app.setEnabled(true);
		
		// Make sure the source and target are updated
		connection.refreshApps(app.projectID);
		CoreUtil.updateApplication(app);
		JSONObject link = event.has(CoreConstants.KEY_LINK) ? event.getJSONObject(CoreConstants.KEY_LINK) : null;
		if (link != null) {
			LinkInfo linkInfo = new LinkInfo(link);
			CodewindApplication targetApp = connection.getAppByID(linkInfo.getProjectId());
			if (targetApp != null) {
				CoreUtil.updateApplication(targetApp);
			}
		}
		String status = event.has(CoreConstants.KEY_STATUS) ? event.getString(CoreConstants.KEY_STATUS) : null;
		String error = event.has(CoreConstants.KEY_ERROR) ? event.getString(CoreConstants.KEY_ERROR) : null;
		if (!CoreConstants.VALUE_STATUS_SUCCESS.equals(status)) {
			// Show the error to the user
			if (error != null && !error.isEmpty()) {
				CoreUtil.openDialog(CoreUtil.DialogType.ERROR, Messages.ProjectLinkErrorTitle, error);
			} else {
				Logger.logError("Project link event had failed status but the error message is null."); //$NON-NLS-1$
			}
		}
	}
	
	private boolean supportsQuickFix(CodewindApplication app, String type, String filename) {
		// The regenerate job only works in certain cases so only show the quickfix in the working cases
		if (!CoreConstants.VALUE_TYPE_MISSING.equals(type) || app.projectType == ProjectType.TYPE_DOCKER) {
			return false;
		}
		if (CoreConstants.DOCKERFILE.equals(filename)) {
			return true;
		}
		if (app.projectType == ProjectType.TYPE_LIBERTY && CoreConstants.DOCKERFILE_BUILD.equals(filename)) {
			return true;
		}
		return false;
	}

	boolean blockUntilFirstConnection(IProgressMonitor monitor) {
		SubMonitor mon = SubMonitor.convert(monitor, 2500);
		final int delay = 100;
		final int timeout = 2500;
		int waited = 0;
		while(!hasConnected && waited < timeout) {
			mon.split(100);
			try {
				Thread.sleep(delay);
				waited += delay;

				if (waited % (5 * delay) == 0) {
					Logger.log("Waiting for CodewindSocket initial connection"); //$NON-NLS-1$
				}
			}
			catch(InterruptedException e) {
				Logger.logError(e);
			}
			if (mon.isCanceled()) {
				return false;
			}
		}
		Logger.log("CodewindSocket initialized in time ? " + hasConnected); //$NON-NLS-1$
		return hasConnected;
	}
}