/*
 *      libpaths.c
 *      
 *      Copyright 2013 Alex <alex@linuxonly.ru>
 *      
 *      This program is free software: you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation; either version 3 of the License, or
 *      (at your option) any later version.
 *      
 *      This program 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 General Public License for more details.
 *      
 *      You should have received a copy of the GNU General Public License
 *      along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <glib.h>
#include <stdio.h>
#include <stdarg.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>

#include "libpaths.h"

#define MMGUI_LIBPATHS_CACHE_FILE         "/etc/ld.so.cache"
#define MMGUI_LIBPATHS_CACHE_SOEXT        ".so"
#define MMGUI_LIBPATHS_CACHE_LIB_TEMP     "/usr/lib/%s.so"
#define MMGUI_LIBPATHS_CACHE_PATH_TEMP    "/usr/lib/%s.so"
/*Cache file*/
#define MMGUI_LIBPATHS_LOCAL_CACHE_XDG    ".cache"
#define MMGUI_LIBPATHS_LOCAL_CACHE_DIR    "modem-manager-gui"
#define MMGUI_LIBPATHS_LOCAL_CACHE_FILE   "libpaths.conf"
#define MMGUI_LIBPATHS_LOCAL_CACHE_PERM   0755
/*Cache file sections*/
#define MMGUI_LIBPATHS_FILE_ROOT_SECTION  "cache"
#define MMGUI_LIBPATHS_FILE_TIMESTAMP     "timestamp"
#define MMGUI_LIBPATHS_FILE_NAME          "name"
#define MMGUI_LIBPATHS_FILE_PATH          "path"


struct _mmgui_libpaths_entry {
	gchar *id;
	gchar *libname;
	gchar *libpath;
};

typedef struct _mmgui_libpaths_entry *mmgui_libpaths_entry_t;


static gboolean mmgui_libpaths_cache_open_local_cache_file(mmgui_libpaths_cache_t libcache, guint64 dbtimestamp)
{
	const gchar *homepath; 
	gchar *confpath;
	guint64 localtimestamp;
	GError *error;
	
	if (libcache == NULL) return FALSE;
	
	homepath = g_get_home_dir();
	
	if (homepath == NULL) return FALSE;
	
	confpath = g_build_filename(homepath, MMGUI_LIBPATHS_LOCAL_CACHE_XDG, MMGUI_LIBPATHS_LOCAL_CACHE_DIR, NULL);
	
	if (g_mkdir_with_parents(confpath, MMGUI_LIBPATHS_LOCAL_CACHE_PERM) != 0) {
		g_debug("No write access to program settings directory");
		g_free(confpath);
		return FALSE;
	}
	
	g_free(confpath);
	
	libcache->localfilename = g_build_filename(homepath, MMGUI_LIBPATHS_LOCAL_CACHE_XDG, MMGUI_LIBPATHS_LOCAL_CACHE_DIR, MMGUI_LIBPATHS_LOCAL_CACHE_FILE, NULL);
	
	libcache->localkeyfile = g_key_file_new();
	
	error = NULL;
	
	if (!g_key_file_load_from_file(libcache->localkeyfile, libcache->localfilename, G_KEY_FILE_NONE, &error)) {
		libcache->updatelocal = TRUE;
		g_debug("Local cache file loading error: %s", error->message);
		g_error_free(error);
	} else {
		error = NULL;
		if (g_key_file_has_key(libcache->localkeyfile, MMGUI_LIBPATHS_FILE_ROOT_SECTION, MMGUI_LIBPATHS_FILE_TIMESTAMP, &error)) {
			error = NULL;
			localtimestamp = g_key_file_get_uint64(libcache->localkeyfile, MMGUI_LIBPATHS_FILE_ROOT_SECTION, MMGUI_LIBPATHS_FILE_TIMESTAMP, &error);
			if (error == NULL) {
				if (localtimestamp == dbtimestamp) {
					libcache->updatelocal = FALSE;
				} else {
					libcache->updatelocal = TRUE;
				}
			} else {
				libcache->updatelocal = TRUE;
				g_debug("Local cache contain unreadable timestamp: %s", error->message);
				g_error_free(error);
			}
		} else {
			libcache->updatelocal = TRUE;
			g_debug("Local cache does not contain timestamp: %s", error->message);
			g_error_free(error);
		}
	}
	
	return !libcache->updatelocal;
}

static gboolean mmgui_libpaths_cache_close_local_cache_file(mmgui_libpaths_cache_t libcache, gboolean update)
{
	gchar *filedata;
	gsize datasize;
	GError *error;
	
	if (libcache == NULL) return FALSE;
	if ((libcache->localfilename == NULL) || (libcache->localkeyfile == NULL)) return FALSE;
	
	if (update) {
		/*Save timestamp*/
		g_key_file_set_int64(libcache->localkeyfile, MMGUI_LIBPATHS_FILE_ROOT_SECTION, MMGUI_LIBPATHS_FILE_TIMESTAMP, (gint64)libcache->modtime);
		/*Write to file*/
		error = NULL;
		filedata = g_key_file_to_data(libcache->localkeyfile, &datasize, &error);
		if (filedata != NULL) {
			if (!g_file_set_contents(libcache->localfilename, filedata, datasize, &error)) {
				g_debug("No data saved to local cache file file: %s", error->message);
				g_error_free(error);
			}
		}
		g_free(filedata);
	}
	/*Free resources*/
	g_free(libcache->localfilename);
	g_key_file_free(libcache->localkeyfile);
	
	return TRUE;
}

static gboolean mmgui_libpaths_cache_add_to_local_cache_file(mmgui_libpaths_cache_t libcache, mmgui_libpaths_entry_t cachedlib)
{
	if ((libcache == NULL) || (cachedlib == NULL)) return FALSE;
	
	if ((libcache->updatelocal) && (libcache->localkeyfile != NULL)) {
		if (cachedlib->id != NULL) {
			/*Library name*/
			if (cachedlib->libname != NULL) {
				g_key_file_set_string(libcache->localkeyfile, cachedlib->id, MMGUI_LIBPATHS_FILE_NAME, cachedlib->libname);
			}
			/*Library path*/
			if (cachedlib->libpath != NULL) {
				g_key_file_set_string(libcache->localkeyfile, cachedlib->id, MMGUI_LIBPATHS_FILE_PATH, cachedlib->libpath);
			}
		}
	}
	
	return TRUE;
}

static gboolean mmgui_libpaths_cache_get_from_local_cache_file(mmgui_libpaths_cache_t libcache, mmgui_libpaths_entry_t cachedlib)
{
	GError *error;
	
	if ((libcache == NULL) || (cachedlib == NULL)) return FALSE;
	
	if ((!libcache->updatelocal) && (libcache->localkeyfile != NULL)) {
		if (cachedlib->id != NULL) {
			/*Library name*/
			error = NULL;
			if (g_key_file_has_key(libcache->localkeyfile, cachedlib->id, MMGUI_LIBPATHS_FILE_NAME, &error)) {
				error = NULL;
				cachedlib->libname = g_key_file_get_string(libcache->localkeyfile, cachedlib->id, MMGUI_LIBPATHS_FILE_NAME, &error);
				if (error != NULL) {
					g_debug("Local cache contain unreadable library name: %s", error->message);
					g_error_free(error);
				}
			} else {
				cachedlib->libname = NULL;
				g_debug("Local cache does not contain library name: %s", error->message);
				g_error_free(error);
			}
			/*Library path*/
			error = NULL;
			if (g_key_file_has_key(libcache->localkeyfile, cachedlib->id, MMGUI_LIBPATHS_FILE_PATH, &error)) {
				error = NULL;
				cachedlib->libpath = g_key_file_get_string(libcache->localkeyfile, cachedlib->id, MMGUI_LIBPATHS_FILE_PATH, &error);
				if (error != NULL) {
					g_debug("Local cache contain unreadable library path: %s", error->message);
					g_error_free(error);
				}
			} else {
				cachedlib->libpath = NULL;
				g_debug("Local cache does not contain library path: %s", error->message);
				g_error_free(error);
			}
		}
	}
	
	return TRUE;
}

static void mmgui_libpaths_cache_destroy_entry(gpointer data)
{
	mmgui_libpaths_entry_t cachedlib;
	
	cachedlib = (mmgui_libpaths_entry_t)data;
	
	if (cachedlib == NULL) return;
	
	if (cachedlib->id != NULL) {
		g_free(cachedlib->id);
	}
	if (cachedlib->libname != NULL) {
		g_free(cachedlib->libname);
	}
	if (cachedlib->libpath != NULL) {
		g_free(cachedlib->libpath);
	}
	g_free(cachedlib);
}

static gboolean mmgui_libpaths_cache_get_entry(mmgui_libpaths_cache_t libcache, gchar *libpath)
{
	mmgui_libpaths_entry_t cachedlib;
	gchar *libext, *libid;
	guint pathlen, sym, lnsym, lilen, lnlen;
	gboolean res;
	
	if ((libcache == NULL) || (libpath == NULL)) return FALSE;
	
	pathlen = strlen(libpath);
	
	if (pathlen == 0) return FALSE;
	
	libext = strstr(libpath, MMGUI_LIBPATHS_CACHE_SOEXT);
	
	if (libext == NULL) return FALSE;
	
	lnsym = 0;
	lilen = 0;
	lnlen = 0;
	
	for (sym = libext-libpath; sym >= 0; sym--) {
		if (libpath[sym] == '/') {
			lnsym = sym + 1;
			lilen = libext - libpath - sym - 1;
			lnlen = pathlen - sym - 1;
			break;
		}
	}
	
	if ((lilen == 0) || (lnlen == 0)) return FALSE;
	
	/*library identifier*/
	libid = g_malloc0(lilen+1);
	strncpy(libid, libpath+lnsym, lilen);
	/*search in hash table*/
	cachedlib = (mmgui_libpaths_entry_t)g_hash_table_lookup(libcache->cache, libid);
	res = FALSE;
	
	if (cachedlib != NULL) {
		if (cachedlib->libname == NULL) {
			/*library name*/
			cachedlib->libname = g_malloc0(lnlen+1);
			strncpy(cachedlib->libname, libpath+lnsym, lnlen);
			/*library name found*/
			mmgui_libpaths_cache_add_to_local_cache_file(libcache, cachedlib);
			g_debug("Library name: %s (%s)\n", cachedlib->libname, libid);
		} 
		if (cachedlib->libpath == NULL) {
			/*full library path*/
			cachedlib->libpath = g_strdup(libpath);
			/*library path found*/
			mmgui_libpaths_cache_add_to_local_cache_file(libcache, cachedlib);
			g_debug("Library path: %s (%s)\n", cachedlib->libpath, libid);
		}
		res = TRUE;
	}
	
	g_free(libid);
	
	return res;
}

static guint mmgui_libpaths_cache_parse_db(mmgui_libpaths_cache_t libcache)
{
	guint ptr, start, end, entry, entries;
	/*gchar *entryhash, *entryname, *entryext;*/
	
	if (libcache == NULL) return 0;
	
	/*Cache file must be terminated with value 0x00*/
	if (libcache->mapping[libcache->mapsize-1] != 0x00) {
		g_debug("Cache file seems to be non-valid\n");
		return 0;
	}
	
	start = 0;
	end = libcache->mapsize-1;
	entries = 0;
	entry = 0;
		
	for (ptr = libcache->mapsize-1; ptr > 0; ptr--) {
		if (libcache->mapping[ptr] == 0x00) {
			/*String separator - value 0x00*/
			if ((end - ptr) == 1) {
				/*Termination sequence - two values 0x00 0x00*/
				if (start < end) {
					if (libcache->mapping[start] == '/') {
						mmgui_libpaths_cache_get_entry(libcache, libcache->mapping+start);
						entries++;
					}
					entry++;
				}
				break;
			} else {
				/*Regular cache entry*/
				if (start < end) {
					if (libcache->mapping[start] == '/') {
						mmgui_libpaths_cache_get_entry(libcache, libcache->mapping+start);
						entries++;
					}
					entry++;
				}
				/*Set end pointer to string end*/
				end = ptr;
			}
		} else if (isprint(libcache->mapping[ptr])) {
			/*Move start pointer because this value is print symbol*/
			start = ptr;
		}
	}
	
	return entries;
}

mmgui_libpaths_cache_t mmgui_libpaths_cache_new(gchar *libname, ...)
{
	va_list libnames;
	gchar *currentlib;
	struct stat statbuf;
	gboolean localcopy;
	mmgui_libpaths_cache_t libcache;
	mmgui_libpaths_entry_t cachedlib;
		
	if (libname == NULL) return NULL;
	
	libcache = (mmgui_libpaths_cache_t)g_new0(struct _mmgui_libpaths_cache, 1);
	
	if (stat(MMGUI_LIBPATHS_CACHE_FILE, &statbuf) == -1) {
		g_debug("Failed to get library paths cache file size\n");
		g_free(libcache);
		return NULL;
	}
	
	libcache->modtime = statbuf.st_mtime;
	
	localcopy = mmgui_libpaths_cache_open_local_cache_file(libcache, (guint64)libcache->modtime);
	
	if (!localcopy) {
		/*Open system cache*/
		libcache->fd = open(MMGUI_LIBPATHS_CACHE_FILE, O_RDONLY);
		if (libcache->fd == -1) {
			g_debug("Failed to open library paths cache file\n");
			mmgui_libpaths_cache_close_local_cache_file(libcache, FALSE);
			g_free(libcache);
			return NULL;
		}
		/*Memory mapping size*/
		libcache->mapsize = (size_t)statbuf.st_size;
		
		if (libcache->mapsize == 0) {
			g_debug("Failed to map empty library paths cache file\n");
			mmgui_libpaths_cache_close_local_cache_file(libcache, FALSE);
			close(libcache->fd);
			g_free(libcache);
			return NULL;
		}
		/*Map file into memory*/
		libcache->mapping = mmap(NULL, libcache->mapsize, PROT_READ, MAP_PRIVATE, libcache->fd, 0);
		
		if (libcache->mapping == MAP_FAILED) {
			g_debug("Failed to map library paths cache file into memory\n");
			mmgui_libpaths_cache_close_local_cache_file(libcache, FALSE);
			close(libcache->fd);
			g_free(libcache);
			return NULL;
		}
	}
	
	/*When no entry found in cache, form safe name adding .so extension*/
	libcache->safename = NULL;
	
	/*Cache for requested libraries*/
	libcache->cache = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)mmgui_libpaths_cache_destroy_entry);
	
	va_start(libnames, libname);
	
	/*Dont forget about first library name*/
	currentlib = libname;
	
	do {
		/*Allocate structure*/
		cachedlib = (mmgui_libpaths_entry_t)g_new0(struct _mmgui_libpaths_entry, 1);
		cachedlib->id = g_strdup(currentlib);
		cachedlib->libname = NULL;
		cachedlib->libpath = NULL;
		g_hash_table_insert(libcache->cache, cachedlib->id, cachedlib);
		/*If available, get from local cache*/
		if (localcopy) {
			mmgui_libpaths_cache_get_from_local_cache_file(libcache, cachedlib);
		}
		/*Next library name*/
		currentlib = va_arg(libnames, gchar *);
	} while (currentlib != NULL);
	
	va_end(libnames);
	
	if (!localcopy) {
		/*Parse system database*/
		mmgui_libpaths_cache_parse_db(libcache);
		/*Save local cache*/
		mmgui_libpaths_cache_close_local_cache_file(libcache, TRUE);
	} else {
		/*Close used cache file*/
		mmgui_libpaths_cache_close_local_cache_file(libcache, FALSE);
	}
	
	return libcache;
}

void mmgui_libpaths_cache_close(mmgui_libpaths_cache_t libcache)
{
	if (libcache == NULL) return;
	
	if (libcache->safename != NULL) {
		g_free(libcache->safename);
	}
	
	g_hash_table_destroy(libcache->cache);
	
	munmap(libcache->mapping, libcache->mapsize);
	
	g_free(libcache);
}

gchar *mmgui_libpaths_cache_get_library_name(mmgui_libpaths_cache_t libcache, gchar *libname)
{
	mmgui_libpaths_entry_t cachedlib;
	
	if ((libcache == NULL) || (libname == NULL)) return NULL;
	
	cachedlib = (mmgui_libpaths_entry_t)g_hash_table_lookup(libcache->cache, libname);
	
	if (cachedlib != NULL) {
		if (cachedlib->libname != NULL) {
			/*Cached library name*/
			return cachedlib->libname;
		} else {
			/*Safe library name*/
			if (libcache->safename != NULL) {
				g_free(libcache->safename);
			}
			libcache->safename = g_strdup_printf(MMGUI_LIBPATHS_CACHE_LIB_TEMP, libname);
			return libcache->safename;
		}
	} else {
		/*Safe library name*/
		if (libcache->safename != NULL) {
			g_free(libcache->safename);
		}
		libcache->safename = g_strdup_printf(MMGUI_LIBPATHS_CACHE_LIB_TEMP, libname);
		return libcache->safename;
	}
}

gchar *mmgui_libpaths_cache_get_library_path(mmgui_libpaths_cache_t libcache, gchar *libname)
{
	mmgui_libpaths_entry_t cachedlib;
	
	if ((libcache == NULL) || (libname == NULL)) return NULL;
	
	cachedlib = (mmgui_libpaths_entry_t)g_hash_table_lookup(libcache->cache, libname);
	
	if (cachedlib != NULL) {
		if (cachedlib->libpath != NULL) {
			/*Cached library path*/
			return cachedlib->libpath;
		} else {
			/*Safe library path*/
			if (libcache->safename != NULL) {
				g_free(libcache->safename);
			}
			libcache->safename = g_strdup_printf(MMGUI_LIBPATHS_CACHE_PATH_TEMP, libname);
			return libcache->safename;
		}
	} else {
		/*Safe library path*/
		if (libcache->safename != NULL) {
			g_free(libcache->safename);
		}
		libcache->safename = g_strdup_printf(MMGUI_LIBPATHS_CACHE_PATH_TEMP, libname);
		return libcache->safename;
	}
}
