/*******************************************************************************
 * Copyright (c) 2000, 2018 IBM Corporation and others.
 *
 * 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:
 *     IBM Corporation - initial API and implementation
 *     Kevin Cornell (Rational Software Corporation)
 *     Tom Tromey (Red Hat, Inc.)
 *******************************************************************************/

#include "eclipseCommon.h"
#include "eclipseOS.h"
#include "eclipseUtil.h"
#include "eclipseGtk.h"

#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <dlfcn.h>

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <semaphore.h>
#include <fcntl.h>

/* Global Variables */
char*  defaultVM     = "java";
char*  vmLibrary 	 = "libjvm.so";
char*  shippedVMDir  = "jre/bin/";

/* Define the special arguments for the various Java VMs. */
static char*  argVM_JAVA[]        = { NULL };


/* Define local variables . */
static GtkWidget*	splashHandle = 0;
static GtkWidget*   shellHandle = 0;

static _TCHAR** openFilePath = NULL; /* the files we want to open */
static int openFileTimeout = 60; 	 /* number of seconds to wait before timeout */
static int filesPassedToSWT = 0;	 /* set to 1 on success */
static const int FILEOPEN_RETRY_TIMEOUT_MS = 1000;

/** GDBus related */
static const gchar GDBUS_SERVICE[] = "org.eclipse.swt";
static const gchar GDBUS_OBJECT[] = "/org/eclipse/swt";
static const gchar GDBUS_INTERFACE[] = "org.eclipse.swt";
GDBusProxy *gdbus_proxy = NULL;

gboolean gdbus_initProxy ();
gboolean gdbus_testConnection();
gboolean gdbus_FileOpen_TimerProc(gpointer data);
gboolean gdbus_call_FileOpen ();

/*
 * Deals with opening files passed to eclipse.  e.g: ./eclipse /myfile
 *
 * return 1 = Files passed to eclipse. Don't spawn another instance.
 *        0 = Launch a new eclipse instance, will try to pass files to eclipse once instance is launched.
 */
gboolean reuseWorkbench(_TCHAR** filePath, int timeout) {
	openFileTimeout = timeout;
	openFilePath = filePath;

	if (initWindowSystem(&initialArgc, initialArgv, 1) != 0)
		return -1;

	if (!gdbus_initProxy()) {
		_ftprintf(stderr, "Launcher Error. Could not init gdbus proxy. Bug? Launching eclipse without opening files passed in.\n");
		return  0;
	}

	// If eclipse already open, just pass files.
	if (gdbus_testConnection()) {
		return gdbus_call_FileOpen();
	} else {
		// Otherwise add a timer that will keep trying to pass files to eclipse for a few minutes until it succeeds or times out.
		// Note, the while loop in launchJavaVM() ensures the launcher doesn't quit before the timer expired.
		gtk.g_timeout_add(FILEOPEN_RETRY_TIMEOUT_MS, gdbus_FileOpen_TimerProc, 0);
		return 0;
	}
}

/**
 * Initializes variables/structures for dbus connectivity to GDBus session.
 * Can be called multiple times, only first time initializes, other times just return early (1).
 *
 * DO NOT USE TO TEST CONNECTION. Use dbus_testConnection() instead.
 *
 * return: 0 (false) bug, something that shouldn't fail failed. This can happen if dynamic function calls failed or gdbus is not available etc..
 *         1 (true) proxy configured.
 */
gboolean gdbus_initProxy () {
	if (gdbus_proxy != NULL)
		return 1; // already initialized.

	// Construct service name: org.eclipse.swt.<name>
	const gint serviceNameLength = strlen(GDBUS_SERVICE) + strlen(getOfficialName()) + 2;
	gchar *serviceName = (gchar *) malloc(serviceNameLength * sizeof(gchar));
	snprintf(serviceName, serviceNameLength, "%s.%s", GDBUS_SERVICE, getOfficialName());
	// Replace any characters that are not valid in a GDBus name with a hyphen.
	int i;
	for (i = 0; i < serviceNameLength - 1; i++) {
		gchar c = serviceName[i];
		if (!((c >= '0' && c <= '9') ||
			(c >= 'A' && c <= 'Z') ||
			(c >= 'a' && c <= 'z') ||
			(c == '_') ||(c == '-') || (c == '.')
		)) {
			serviceName[i] = '-';
		}
	}

	// Function 'g_type_init()' is not needed anymore as of glib 2.36 as gtype system is initialized earlier. It is marked as deprecated.
	// It is here because at the time of writing, eclipse supports glib 2.28.
	// It is dynamic to prevent compile warning. But should be removed once min glib eclipse version >= 2.36
	gtk.g_type_init();

	GError *error = NULL; // Some functions return errors through params

	gdbus_proxy = gtk.g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, NULL, serviceName, GDBUS_OBJECT, GDBUS_INTERFACE, NULL, &error);
	if ((gdbus_proxy == NULL) || (error != NULL)) {
		fprintf(stderr, "Launcher error: GDBus proxy init failed to connect %s:%s on %s.\n", serviceName, GDBUS_OBJECT, GDBUS_INTERFACE);
		if (error != NULL) {
			_ftprintf(stderr, "Launcher error: GDBus gdbus_proxy init failed for reason: %s\n", error->message);
			gtk.g_error_free (error);
		}
		free(serviceName);
		return 0;
	} else {
		free(serviceName);
		return 1;
	}
}

/*
 * Test if we can reach org.eclipse.swt dbus session.
 *
 * return 0 (false) No connection.
 *        1 (true)  org.eclipse.swt listens to calls.
 */
gboolean gdbus_testConnection() {
	if (!gdbus_initProxy())
		return 0;
	GError *error = NULL; // Some functions return errors through params
	GVariant *result;     // The value result from a call
	result = gtk.g_dbus_proxy_call_sync(gdbus_proxy, "org.freedesktop.DBus.Peer.Ping", 0, G_DBUS_CALL_FLAGS_NONE, /* Proxy default timeout */ -1, NULL, &error);
	if (error != NULL) {
			gtk.g_error_free(error);
			return 0;
	}
	if (result != NULL) {
		gtk.g_variant_unref (result);
		return 1;
	}
	_ftprintf(stderr, "ERROR: testConnection failed due to unknown reason. Bug in eclipseGtk.c? Potential cause could be dynamic function not initialized properly\n");
	return 0;
}

/*
 * Timer callback function.
 *
 * Try to pass files to eclipse. If eclipse not up yet, try again later.
 *
 * Timer ends when it returns false. (Files passed to eclipse or timeout).
 */
gboolean gdbus_FileOpen_TimerProc(gpointer data) {
	if (openFileTimeout == 0)
		return 0; // stop timer.
	openFileTimeout--;
	if (gdbus_testConnection()) {
		gdbus_call_FileOpen();
		filesPassedToSWT = 1;
		return 0; // stop timer.
	}
	return 1; // run timer again.
}

/*
 * Call fileOpen method in SWT.  Note, in SWT, see GDBus.java. fileOpen GDBusMethod is defined in Display.java.
 * This call can be called multiple times if Eclipse hasn't launched yet.
 *
 * Return: FALSE (0) Call did not work. Probably eclipse not fired up yet. (try again later)
 *         TRUE (1) GDBus call completed successfully.
 */
gboolean gdbus_call_FileOpen () {
	if (!gdbus_initProxy())
		return 0;

	// Construct GDBus arguments based on files passed into launcher.
	GVariantBuilder *builder;
	GVariant *paramaters;
	builder = gtk.g_variant_builder_new ((const GVariantType *) "as");  // as = G_VARIANT_TYPE_STRING_ARRAY

	int i = -1;
	while (openFilePath[++i] != NULL) {
			gtk.g_variant_builder_add (builder, (const gchar *) (const GVariantType *) "s", (const gchar *) openFilePath[i]);  // s = G_VARIANT_TYPE_STRING
	}

	paramaters = gtk.g_variant_new ("(as)", builder);
	gtk.g_variant_builder_unref (builder);

	// Send a message
	GError *error = NULL;
	GVariant *result;
	result = gtk.g_dbus_proxy_call_sync(gdbus_proxy, "FileOpen", paramaters, G_DBUS_CALL_FLAGS_NONE, /* Proxy default timeout */ -1, NULL, &error);
	if (error != NULL) {
		gtk.g_error_free (error);
		return 0; // did not work. Eclipse probably not up yet. Try again later.
	} else {
		 if (result != NULL) {
			// Because this not straight forward, below is an example of how to retrieve string return value if needed in the future.
			// Note, arguments are packaged into a tuple because we deal with gdbus in dynamic way.
			// gchar *str;
			// g_variant_get(result, "(&s)", &str);
			 gtk.g_variant_unref(result);
		}
		return 1; // worked.
	}
}

/* Get current scaling-factor */
float scaleFactor () {
	float scaleFactor = 1;
	GdkScreen * screen;
	double resolution;
	screen = gtk.gdk_screen_get_default();
	resolution = gtk.gdk_screen_get_resolution (screen);
	if (resolution <= 0) resolution = 96; // in unix and windows 100% corresponds to dpi of 96
	resolution = ((int)((resolution + 24) / 96)) * 96; //rounding the resolution to 100% multiples,this implementation needs to be kept in sync with org.eclipse.swt.internal.DPIUtil#setDeviceZoom(int)
	scaleFactor = (float)(resolution / 96);
	return scaleFactor;
}

/* Create and Display the Splash Window */
int showSplash( const char* featureImage ) {
	GtkWidget *image;
	GdkPixbuf *pixbuf, *scaledPixbuf;
	int width, height;
	float scalingFactor;

	if (splashHandle != 0)
		return 0; /* already showing splash */
	if (featureImage == NULL)
		return -1;
	
	if (initialArgv == NULL)
		initialArgc = 0;
	
	if( initWindowSystem(&initialArgc, initialArgv, 1) != 0)
		return -1;
	
	shellHandle = gtk.gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk.gtk_window_set_decorated((GtkWindow*)(shellHandle), FALSE);
	gtk.gtk_window_set_type_hint((GtkWindow*)(shellHandle), 4 /*GDK_WINDOW_TYPE_HINT_SPLASHSCREEN*/);
	gtk.g_signal_connect_data((gpointer)shellHandle, "destroy", (GCallback)(gtk.gtk_widget_destroyed), &shellHandle, NULL, 0);
		
	pixbuf = gtk.gdk_pixbuf_new_from_file(featureImage, NULL);
	width = gtk.gdk_pixbuf_get_width(pixbuf);
	height = gtk.gdk_pixbuf_get_height(pixbuf);
	scalingFactor = scaleFactor();

	if (scalingFactor > 1) {
		scaledPixbuf = gtk.gdk_pixbuf_scale_simple(pixbuf, width * scalingFactor, height * scalingFactor, GDK_INTERP_BILINEAR);
	} else {
		scaledPixbuf = pixbuf;
	}
	
	image = gtk.gtk_image_new_from_pixbuf(scaledPixbuf);
	if (pixbuf) {
		gtk.g_object_unref(pixbuf);
	}
	gtk.gtk_container_add((GtkContainer*)(shellHandle), image);
	
	if (getOfficialName() != NULL)
		gtk.gtk_window_set_title((GtkWindow*)(shellHandle), getOfficialName());
	gtk.gtk_window_set_position((GtkWindow*)(shellHandle), GTK_WIN_POS_CENTER);
	gtk.gtk_window_resize((GtkWindow*)(shellHandle), gtk.gdk_pixbuf_get_width(scaledPixbuf), gtk.gdk_pixbuf_get_height(scaledPixbuf));
	gtk.gtk_widget_show_all((GtkWidget*)(shellHandle));
	splashHandle = shellHandle;
	dispatchMessages();
	return 0;
}

void dispatchMessages() {
	if (gtk.g_main_context_iteration != 0)
		while(gtk.g_main_context_iteration(0,0) != 0) {}
}

jlong getSplashHandle() {
	return (jlong) splashHandle;
}

void takeDownSplash() {
	if(shellHandle != 0) {
		gtk.gtk_widget_destroy(shellHandle);
		dispatchMessages();
		splashHandle = 0;
		shellHandle = NULL;
	}
}

/* Get the window system specific VM arguments */
char** getArgVM( char* vm ) {
    char** result;

/*    if (isJ9VM( vm )) 
        return argVM_J9;*/
    
    /* Use the default arguments for a standard Java VM */
    result = argVM_JAVA;
    return result;
}

JavaResults* launchJavaVM( char* args[] ) {
	JavaResults* jvmResults = NULL;
  	pid_t   jvmProcess, finishedProcess = 0;
  	int     exitCode;
  	
	jvmProcess = fork();
  	if (jvmProcess == 0) 
    {
    	/* Child process ... start the JVM */
      	execv(args[0], args);

      	/* The JVM would not start ... return error code to parent process. */
      	/* TODO, how to distinguish this as a launch problem to the other process? */
      	_exit(errno);
    }

  	jvmResults = malloc(sizeof(JavaResults));
  	memset(jvmResults, 0, sizeof(JavaResults));
  	
	/* If the JVM is still running, wait for it to terminate. */
	if (jvmProcess != 0)
	{
		/* When attempting a file open, we need to spin the event loop
		 * for setAppWindowTimerProc to run.  When that succeeds or times out, 
		 * we can stop the event loop and just wait on the child process.
		 */
		if (openFilePath != NULL) {
			struct timespec sleepTime;
			sleepTime.tv_sec = 0;
			sleepTime.tv_nsec = 5e+8; // 500 milliseconds
			
			// Ensure we don't quit the launcher until gdbus_FileOpen_TimerProc() finished or timed out.
			// If making any changes to this loop, ensure "./eclipse /myFile" still works.
			while(openFileTimeout > 0 && !filesPassedToSWT && (finishedProcess = waitpid(jvmProcess, &exitCode, WNOHANG)) == 0) {
				dispatchMessages();
				nanosleep(&sleepTime, NULL);
			}
		}
		if (finishedProcess == 0)
			waitpid(jvmProcess, &exitCode, 0);
      	if (WIFEXITED(exitCode))
      		/* TODO, this should really be a runResult if we could distinguish the launch problem above */
			jvmResults->launchResult = WEXITSTATUS(exitCode);
    }
	return jvmResults;
}
