/***************************************************************************
  
  gbi.c
  
  (c) Benoît Minisini <benoit.minisini@gambas-basic.org>

  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 2, 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, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.
  
***************************************************************************/

#define __GBI_C

#include "config.h"

#include "gb_limit.h"
#include "gb_common.h"
#include "gb_alloc.h"

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <dirent.h>

#include <dlfcn.h>

#ifdef DONT_USE_LTDL
	#define lt_dlinit() (0)
	#define lt_dlhandle void *
	#define lt_dlopenext(_path) dlopen(_path, RTLD_LAZY)
	#define lt_dlsym(_handle, _symbol) dlsym(_handle, _symbol)
	#define lt_dlclose(_handle) dlclose(_handle)
	#define lt_dlerror() dlerror()
#else
	#include <ltdl.h>
#endif

#include <ctype.h>

#include "gb_component.h"
#include "gb_file.h"
#include "gb_str.h"
#include "gb_arch.h"
#include "gb_common_swap.h"
#include "gb_array.h"
#include "gb_table.h"
#include "gambas.h"

static char _root[PATH_MAX + 1] = { 0 };
static char _lib_path[PATH_MAX + 1];
static char _info_path[PATH_MAX + 1];
static char _buffer[PATH_MAX + 16];
static char _env[PATH_MAX + 32];

static FILE *out_info;
static FILE *out_list;
static bool _verbose = FALSE;
static bool _format = FALSE;
static bool _nopreload = FALSE;
static bool _root_set = FALSE;
static bool _analyze = FALSE;
static bool _force = FALSE;
static bool _no_include_warning = TRUE;

static bool _include_fail;

static char **_components = NULL;

static TABLE *_classes = NULL;

static char *_current_name = NULL;

static bool analyze(const char *comp, bool include);

#if HAVE_GETOPT_LONG
static struct option LongOptions[] =
{
	{ "version", 0, NULL, 'V' },
	{ "verbose", 0, NULL, 'v' },
	{ "help", 0, NULL, 'h' },
	{ "license", 0, NULL, 'L' },
	{ "root", 1, NULL, 'r' },
	{ "force", 0, NULL, 'f' },
	{ 0 }
};
#endif


static int compare_components(char **a, char **b)
{
	return strcmp(*a, *b);
}

static void print(const char *fmt, ...)
{
	va_list args;

	va_start(args, fmt);

	vfprintf(out_info, fmt, args);
	
	va_end(args);
}

static void warning(const char *fmt, ...)
{
	va_list args;

	fprintf(stderr, "gbi" GAMBAS_VERSION_STRING ": warning: ");
		
	va_start(args, fmt);

	if (_current_name)
		fprintf(stderr, "%s: ", _current_name);
	
	vfprintf(stderr, fmt, args);
	
	putc('\n', stderr);
	
	va_end(args);
}

static void error(bool must_exit, const char *fmt, ...)
{
	va_list args;

	fprintf(stderr, "gbi" GAMBAS_VERSION_STRING ": ERROR: ");
		
	va_start(args, fmt);

	if (_current_name)
		fprintf(stderr, "%s: ", _current_name);
	
	vfprintf(stderr, fmt, args);
	
	putc('\n', stderr);
	
	va_end(args);

	if (must_exit)
		exit(1);
}

// static void error2(const char *msg, const char *msg2, bool must_exit)
// {
//   fprintf(stderr, "gbi" GAMBAS_VERSION_STRING ": ERROR: %s: %s%s%s\n", msg, msg2, (errno != 0) ? ": " : "", (errno != 0) ? strerror(errno) : "");
//   if (must_exit)
//   	exit(1);
// }


static void init(void)
{
	const char *path;
	char *env;

	/* chemin d'installation de Gambas */

	if (!_root[0])
	{
		const char *dir;
		
		path = FILE_find_gambas();
		dir = FILE_get_dir(FILE_get_dir(path));
		if (dir)
			strncpy(_root, dir, PATH_MAX);
	}

	#ifdef OS_64BITS
	strcpy(_lib_path, FILE_cat(_root, GAMBAS_LIB64_PATH, NULL));
	if (access(FILE_cat(_lib_path, "gb.component", NULL), F_OK))
	#endif
		strcpy(_lib_path, FILE_cat(_root, GAMBAS_LIB_PATH, NULL));
		
	strcpy(_info_path, FILE_cat(_root, "share/gambas" GAMBAS_VERSION_STRING "/info", NULL));

	if (lt_dlinit())
		error(TRUE, "Cannot initialize plug-in management: %s", lt_dlerror());

	env = getenv("GBI_DEBUG");
	if (env && *env && atoi(env))
		_no_include_warning = FALSE;
}


static void newline()
{
	print("\n");
}

#if 0
static bool print_type(const char *type)
{
	switch (*type)
	{
		case 'b': print("Boolean"); break;
		case 'i': print("Integer"); break;
		case 's': print("String"); break;
		case 'd': print("Date"); break;
		case 'f': print("Float"); break;
		case 'v': print("Variant"); break;
		case 'o': print("Object"); break;

		default:

			while (*type && *type != ';')
			{
				print("%c", *type);
				type++;
			}
			return TRUE;
	}

	return FALSE;
}
#endif

static void dump_symbol(GB_DESC *desc)
{
	const char *name;
	const char *p;

	name = &desc->name[1];

	print("%s\n", name);
	print("%c\n", *desc->name);
	if (desc->val1)
		print("%s", desc->val1);
	newline();

	if (*desc->name == 'C')
	{
		switch(*(char *)desc->val1)
		{
			case 'i':
				print("%d", desc->val2);
				break;

			case 's':
				p = (const char *)desc->val2;
				while (*p)
				{
					if (*p == '\n')
						print("\\n");
					else if (*p == '\t')
						print("\\t");
					else
						print("%c", *p);
					p++;
				}
				break;
				
			case 'b':
				if (desc->val2)
					print("T");
				break;
				
			case 'f':
				print("%.15g", desc->val4);
				break;

			default:
				print("?");
				break;
		}
	}
	else if (desc->val3)
		print("%s", (char *)desc->val3);

	newline();
}


static GB_DESC *_sort_symbol;

static int sort_symbol(const int *a, const int *b)
{
	return strcmp(_sort_symbol[*a].name, _sort_symbol[*b].name);
}

static bool add_class(const char *name, bool has_static)
{
	int index;
	
	if (out_list)
	{
		if (!TABLE_add_symbol_exist(_classes, name, strlen(name), &index))
		{
			fputs(name, out_list);
			if (has_static)
				fputc('!', out_list);
			fputc('\n', out_list);
			return TRUE;
		}
	}
	
	return FALSE;
}

static void analyze_class(GB_DESC *desc)
{
	const char *name = desc->name;
	char *parent = NULL;
	bool autocreate = FALSE;
	bool nocreate = FALSE;
	uintptr_t hook;
	int nsymbol;
	int *sort;
	GB_DESC *p;
	int i;
	bool has_static = FALSE;
	bool has_enum = FALSE;

	desc++;

	while (desc->name)
	{
		hook = (uintptr_t)desc->name;

		if (hook == (intptr_t)GB_INHERITS_ID)
			parent = (char *)desc->val1;
		else if (hook == (intptr_t)GB_NOT_CREATABLE_ID)
			nocreate = TRUE;
		else if (hook == (intptr_t)GB_AUTO_CREATABLE_ID)
			autocreate = TRUE;
		else if (hook > 16)
			break;

		desc++;
	}

	p = desc;
	nsymbol = 0;
	while (p->name)
	{
		nsymbol++;
		p++;
	}

	if (_format)
	{
		print("CLASS %s\n", name);
		if (parent) print("INHERITS %s\n", parent);
		if (!nocreate) print("CREATABLE\n");
		if (autocreate) print("AUTOCREATABLE\n");
	}
	else
	{
		print("#%s\n", name);
		if (parent)
			print("%s", parent);
		newline();
		if (!nocreate)
			print("C");
		if (autocreate)
			print("A");
		newline();
	}

	ALLOC(&sort, sizeof(int) * nsymbol);
	for (i = 0; i < nsymbol; i++)
		sort[i] = i;
	
	_sort_symbol = desc;
	qsort(sort, nsymbol, sizeof(int), (int (*)(const void *, const void *))sort_symbol);

	for (i = 0; i < nsymbol; i++)
	{
		p = &desc[sort[i]];
		if (strchr("PCMR", *p->name) && p->name[1] != '_')
			has_static = TRUE;
		if (*p->name == 'E')
			has_enum = TRUE;
		dump_symbol(p);
	}

	if (add_class(name, has_static) && has_enum)
	{
		for (i = 0; i < nsymbol; i++)
		{
			p = &desc[sort[i]];
			if (*p->name == 'E')
			{
				fputs(&p->name[1], out_list);
				fputc('\n', out_list);
			}
		}
	}
	
	FREE(&sort);
	
	if (_format)
		newline();

}


static GB_DESC **_sort_desc;

static int sort_desc(const int *a, const int *b)
{
	return strcmp(_sort_desc[*a]->name, _sort_desc[*b]->name);
}

static void analyze_include(char *include_list)
{
	char *includes[8];
	int nincludes;
	char *include;
	int i;
	bool ok;
	bool all_ok;
	
	include_list = STR_copy(include_list);
	
	if (_verbose)
		fprintf(stderr, "Including %s\n", include_list);
	
	nincludes = 0;
	include = strtok(include_list, ",");
	while (include && nincludes < 8)
	{
		includes[nincludes++] = include;
		include = strtok(NULL, ",");
	}
	
	all_ok = TRUE;
	
	for (i = 0; i < nincludes; i++)
	{
		ok = FALSE;
		
		include = strtok(includes[i], "|");
		
		while (include)
		{
			if (!analyze(include, TRUE))
			{
				ok = TRUE;
				break;
			}
			include = strtok(NULL, "|");
		}
		
		if (!ok)
			all_ok = FALSE;
	}
		
	STR_free(include_list);
	
	if (!all_ok)
		_include_fail = TRUE;
}

static void analyze_classes(GB_DESC **desc)
{
	int nclass;
	int *sort;
	int i;
	GB_DESC **p;

	nclass = 0;
	
	p = desc;
	while (*p)
	{
		nclass++;
		p++;
	}
	
	ALLOC(&sort, sizeof(int) * nclass);
	for (i = 0; i < nclass; i++)
		sort[i] = i;
	
	_sort_desc = desc;
	qsort(sort, nclass, sizeof(int), (int (*)(const void *, const void *))sort_desc);

	for (i = 0; i < nclass; i++)
		analyze_class(desc[sort[i]]);
		
	FREE(&sort);
}

static bool analyze_native_component(const char *path, bool for_include)
{
	lt_dlhandle lib;
	GB_DESC **desc;
	GB_DESC **desc_opt;
	char **include;

	if (_verbose)
		fprintf(stderr, "Loading native component: %s\n", path);

	//lt_dlopen_flag = RTLD_LAZY; /* | RTLD_GLOBAL;*/

	lib = lt_dlopenext(path);
	if (!lib)
	{
		if (!for_include || _verbose)
			error(FALSE, "Cannot load shared library: %s", lt_dlerror());
		return TRUE;
	}

	include = lt_dlsym(lib, LIB_INCLUDE);
	if (include)
		analyze_include(*include);

	desc = lt_dlsym(lib, LIB_CLASS);
	desc_opt = lt_dlsym(lib, LIB_OPTIONAL);

	if (desc)
	{
		analyze_classes(desc);
		if (desc_opt)
			analyze_classes(desc_opt);
	}
	else
	{
		if (_verbose)
			warning("cannot find '" LIB_CLASS "' symbol in shared library.");
	}
	
	// Do not close shared libraries, except on openbsd that seems to feel better with
	#ifdef OS_OPENBSD
	lt_dlclose(lib);
	#endif

	return FALSE;
}


static bool analyze_gambas_component(const char *path)
{
	ARCH *arch;
	ARCH_FIND find;
	bool ret = TRUE;
	char *buffer;
	char *line;

	if (_verbose)
		fprintf(stderr, "Loading gambas component: %s\n", path);

	arch = ARCH_open(path);

	if (!ARCH_find(arch, ".component", 0, &find))
	{
		ALLOC(&buffer, find.len + 1);
		memcpy(buffer, &arch->addr[find.pos], find.len);
		buffer[find.len] = 0;

		for (line = strtok(buffer, "\n"); line; line = strtok(NULL, "\n"))
		{
			if (strncmp(line, "Include=", 8))
				continue;
			analyze_include(&line[8]);
			break;
		}

		FREE(&buffer);
	}

	if (ARCH_find(arch, ".info", 0, &find))
	{
		warning("'.info' file not found in component archive.");
		goto __RETURN;
	}

	fwrite(&arch->addr[find.pos], 1, find.len, out_info);

	if (ARCH_find(arch, ".list", 0, &find))
	{
		warning("'.list' file not found in component archive.");
		goto __RETURN;
	}

	ALLOC(&buffer, find.len + 1);
	memcpy(buffer, &arch->addr[find.pos], find.len);
	buffer[find.len] = 0;

	for (line = strtok(buffer, "\n"); line; line = strtok(NULL, "\n"))
		add_class(line, FALSE);

	FREE(&buffer);

	//fwrite(&arch->addr[find.pos], 1, find.len, out_list);
	ret = FALSE;

__RETURN:
	
	ARCH_close(arch);
	return ret;
}

#if 0
static void preload(char **argv, char *lib)
{
#if DO_PRELOADING
	if (_nopreload || getenv("GB_PRELOAD") || !lib || !*lib)
		return;

	snprintf(_env, sizeof(_env), "LD_PRELOAD=%s", lib);
	putenv(_env);
	putenv("GB_PRELOAD=1");

	execvp(argv[0], argv);
#endif
}
#endif


static const char *find_native_component(const char *name)
{
	snprintf(_buffer, sizeof(_buffer), LIB_PATTERN, _lib_path, name);
	return (access(_buffer, F_OK) == 0) ? _buffer : NULL;
}


static const char *find_gambas_component(const char *name)
{
	snprintf(_buffer, sizeof(_buffer), ARCH_PATTERN, _lib_path, name);
	return (access(_buffer, F_OK) == 0) ? _buffer : NULL;
}


static bool analyze(const char *comp, bool include)
{
	bool native, gambas;
	char *path_list = NULL;
	char *path_info = NULL;
	char *path_fail;
	FILE *out_fail;
	bool ok = FALSE;
	char *save_name;

	save_name = _current_name;
	_current_name = STR_copy(comp);

	if (_verbose)
		fprintf(stderr, "%s component %s\n", include ? "Including" : "Analyzing", _current_name);
	/*else if (!include)
		puts(name);*/
	
	native = find_native_component(_current_name) != NULL;
	gambas = find_gambas_component(_current_name) != NULL;
		
	if (!native && !gambas)
	{
		if (!include || !_no_include_warning)
			warning("component not found");
		goto __RETURN;
	}

	if (!include)
	{
		path_info = STR_cat(FILE_cat(_info_path, _current_name, NULL), ".info", NULL);
		path_list = STR_cat(FILE_cat(_info_path, _current_name, NULL), ".list", NULL);
	
		out_info = fopen(path_info, "w");
		if (!out_info)
		{
			error(FALSE, "Cannot write file: %s", path_info);
			goto __RETURN;
		}
	
		out_list = fopen(path_list, "w");
		if (!out_list)
		{
			error(FALSE, "Cannot write file: %s", path_list);
			goto __RETURN;
		}

		TABLE_create(&_classes, sizeof(SYMBOL), TF_IGNORE_CASE);
	}
	
	fflush(stdout);
	ok = TRUE;
	_include_fail = FALSE;
	
	if (native)
	{
		if (analyze_native_component(find_native_component(_current_name), include))
			ok = FALSE;
	}

	if (gambas)
	{
		if (analyze_gambas_component(find_gambas_component(_current_name)))
			if (!native)
				ok = FALSE;
	}

	if (!include)
	{
		TABLE_delete(&_classes);

		fclose(out_info);
		fclose(out_list);
	
		if (!ok)
		{
			FILE_unlink(path_info);
			FILE_unlink(path_list);
		}
		else
		{
			if (_verbose)
			{
				fprintf(stderr, "Wrote %s\n", path_info);
				fprintf(stderr, "Wrote %s\n", path_list);
			}
		}

		STR_free(path_info);
		STR_free(path_list);
		
		path_fail = STR_cat(FILE_cat(_info_path, _current_name, NULL), ".fail", NULL);
		FILE_unlink(path_fail);
		if (_include_fail)
		{
			if (_verbose)
				fprintf(stderr, "Wrote %s\n", path_fail);
			out_fail = fopen(path_fail, "w");
			fclose(out_fail);
		}
		STR_free(path_fail);
	}
	
__RETURN:
	
	STR_free(_current_name);
	_current_name = save_name;
	
	return !ok;
}

static void run_myself(const char *path, const char *name)
{
	const char *argv[10];
	int n = 0;
	pid_t pid;
	int status;
	char *path_info;
	char *path_fail;
	time_t time_info;
	time_t time_file;
	bool exist;
	
	if (_verbose)
		fprintf(stderr, "Running myself for component '%s'...\n", name);
	
	argv[n++] = path;
	if (_verbose)
		argv[n++] = "-v";
	if (_nopreload)
		argv[n++] = "-p";
	if (_root_set)
	{
		argv[n++] = "-r";
		argv[n++] = _root;
	}
	argv[n++] = "-a";
	argv[n++] = name;
	argv[n] = NULL;

	if (!_force)
	{
		path_fail = STR_cat(FILE_cat(_info_path, name, NULL), ".fail", NULL);
		exist = FILE_exist(path_fail);
		STR_free(path_fail);
		if (exist)
			goto MUST_REDONE;
		
		path_info = STR_cat(FILE_cat(_info_path, name, NULL), ".info", NULL);
		time_info = FILE_get_time(path_info);
		STR_free(path_info);
		if (time_info == ((time_t)-1))
			goto MUST_REDONE;
		
		time_file = FILE_get_time(find_native_component(name));
		if (time_file != ((time_t)-1) && time_file >= time_info)
			goto MUST_REDONE;
		
		time_file = FILE_get_time(find_gambas_component(name));
		if (time_file != ((time_t)-1) && time_file >= time_info)
			goto MUST_REDONE;
		
		if (_verbose)
			fprintf(stderr, "Component files are up to date. No need to rebuild them.\n");
		
		return;
	}
	
MUST_REDONE:

	//puts(name);
	
	if (find_native_component(name))
	{
		snprintf(_env, sizeof(_env), "LD_PRELOAD=%s", _buffer);
		putenv(_env);
	}
	else
		unsetenv("LD_PRELOAD");

	pid = fork();
	switch (pid)
	{
		case 0:
			execvp(path, (char **)argv);
			error(TRUE, "Cannot run sub-process: %s", strerror(errno));
		case -1:
			error(TRUE, "Cannot run sub-process: %s", strerror(errno));
		default:
			waitpid(pid, &status, 0);
	}
	
	if (_verbose)
		fputc('\n', stderr);
}

static void make_component_list()
{
	DIR *dir;
	struct dirent *dirent;
	const char *name;
			
	dir = opendir(_lib_path);
	if (dir == NULL)
		error(TRUE, "Cannot read directory: %s", _lib_path);

	//save_fd = dup(STDOUT_FILENO);

	ARRAY_create(&_components);
	
	while ((dirent = readdir(dir)) != NULL)
	{
		name = dirent->d_name;
		if (strcmp(FILE_get_ext(name), "component"))
			continue;
		name = FILE_get_basename(name);
		if (strcmp(name, "gb") == 0)
			continue;
		*((char **)ARRAY_add(&_components)) = STR_copy(name);
	}

	closedir(dir);
	
	qsort(_components, ARRAY_count(_components), sizeof(*_components), (int (*)(const void *, const void *))compare_components);
}

static void print_version()
{
	#ifdef TRUNK_VERSION
	printf(VERSION " " TRUNK_VERSION "\n");
	#else /* no TRUNK_VERSION */
	printf(VERSION "\n");
	#endif
}

static void print_title()
{
	printf("\nGambas informer version ");
	print_version();
}

int main(int argc, char **argv)
{
	int i;
	char *name;
	int opt;
	int ind = 0;

	for(;;)
	{
		#if HAVE_GETOPT_LONG
			opt = getopt_long(argc, argv, "vVhLpafr:", LongOptions, &ind);
		#else
			opt = getopt(argc, argv, "vVhLpafr:");
		#endif
		if (opt < 0) break;

		switch (opt)
		{
			case 'V':
				print_version();
				exit(0);
				break;

			case 'v':
				_verbose = TRUE;
				break;
				
#if DO_PRELOADING
			case 'p':
				_nopreload = TRUE;
				break;
#endif
				
			case 'r':
				strncpy(_root, optarg, PATH_MAX);
				_root_set = TRUE;
				break;
				
			case 'a':
				_analyze = TRUE;
				break;
				
			case 'f':
				_force = TRUE;
				break;
				
			case 'L':
				print_title();
				printf(COPYRIGHT);
				exit(0);

			case 'h': case '?': case ':':
				print_title();
				printf(
					"\nGenerate component description files.\n"
					"\n    gbi" GAMBAS_VERSION_STRING " [options] [components]\n"
					"\nOptions:"
					#if HAVE_GETOPT_LONG
					"\n\n"
					"  -f  --force                force generation of all description files\n"
					"  -h  --help                 display this help\n"
					"  -L  --license              display license\n"
					#if DO_PRELOADING
					"  -p                         disable preloading\n"
					#endif
					"  -r  --root <directory>     gives the gambas installation directory\n"
					"  -v                         verbose output\n"
					"  -V  --version              display version\n"
					#else
					" (no long options on this system)\n\n"
					"  -h                         display this help\n"
					"  -L                         display license\n"
					#if DO_PRELOADING
					"  -p                         disable preloading\n"
					#endif
					"  -r <directory>             gives the gambas installation directory\n"
					"  -v                         verbose output\n"
					"  -V                         display version\n"
					#endif
					"\n"
					);

				exit(opt != 'h');
		}
	}

	init();
	
	if (_analyze)
	{
		if (_verbose)
		{
			char *env = getenv("LD_PRELOAD");
			if (env)
				fprintf(stderr, "LD_PRELOAD=%s\n", env);
		}

		analyze(argv[optind], FALSE);
	}
	else
	{
		if (optind == argc
		#ifdef OS_SOLARIS  /* solaris bug ? */
				|| optind == 0
		#endif
				)
		{
			if (_verbose)
			{
				fprintf(stderr, "Browsing component directory: %s\n", _lib_path);
				fprintf(stderr, "*.info files are stored in: %s\n\n", _info_path);
			}
			
			make_component_list();
			
			for (i = 0; i < ARRAY_count(_components); i++)
			{
				name = _components[i];
				run_myself(argv[0], name);
				STR_free(name);
			}
			
			ARRAY_delete(&_components);
		}
		else
		{
			for (ind = optind; ind < argc; ind++)
			{
				name = argv[ind];
				run_myself(argv[0], name);
			}
		}
	}

	exit(0);
}

