/***************************************************************************
  
  gbx_archive.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 __GBX_ARCHIVE_C

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#include "gb_common.h"
#include "gb_common_string.h"
#include "gb_error.h"
#include "gb_alloc.h"

#include "gbx_string.h"
#include "gbx_stream.h"
#include "gbx_component.h"
#include "gbx_regexp.h"
#include "gbx_exec.h"
#include "gbx_class.h"
#include "gbx_project.h"
#include "gbx_jit.h"

#include "gbx_archive.h"

#include "gb_arch_temp.h"

//#define DEBUG_COMP 1

// main archive project (used only if gbx is run with -x flag)
ARCHIVE *ARCHIVE_main = NULL;

// Additional library search directory used in debug mode, normally '~/.config/share/gambas3/lib'
char *ARCHIVE_path = NULL;

ARCHIVE *ARCHIVE_list = NULL;

static char *_local_dir = NULL;
static char *arch_pattern = NULL;
static int arch_index = 0;
static ARCH *arch_dir = NULL;
static char *arch_prefix = NULL;

ARCHIVE *ARCHIVE_create(const char *name, const char *path)
{
	ARCHIVE *arch;

	ALLOC_ZERO(&arch, sizeof(ARCHIVE));

	arch->name = name;
	arch->path = STRING_new_zero(path);

	arch->domain = STRING_new_zero(name ? name : "gb");

	arch->translation_loaded = FALSE;

	TABLE_create(&arch->classes, sizeof(CLASS_SYMBOL), TF_IGNORE_CASE);

	LIST_insert(&ARCHIVE_list, arch, &arch->list);

	return arch;
}

static void error_ARCHIVE_load_exported_class(COMPONENT *current)
{
	COMPONENT_current = current;
}

void ARCHIVE_load_exported_class(ARCHIVE *arch, int pass)
{
	char *buffer;
	int len;
	char *name;
	char *file_name;
	CLASS *class;
	int i;
	COMPONENT *current;
	bool optional;
	char c;

	if (arch->exported_classes_loaded)
		return;

	current = COMPONENT_current;
	COMPONENT_current = (COMPONENT *)arch->current_component;

	ON_ERROR_1(error_ARCHIVE_load_exported_class, current)
	{
		/* COMPONENT_current is set => it will look in the archive */

		#if DEBUG_COMP
			fprintf(stderr, "load_exported_class: %s (component: %s)\n", arch->name, COMPONENT_current ? COMPONENT_current->name : "?");
		#endif

		if (FILE_exist(".list"))
		{
			if (pass & AR_FIND_ONLY)
			{
				#if DEBUG_COMP
					fprintf(stderr, "<Find pass>\n");
				#endif

				STREAM_load(".list", &buffer, &len);
				/* The file must end with a newline !*/
				buffer[len - 1] = 0;

				#if DEBUG_COMP
					fprintf(stderr, "-----------\n%s\n----------\n\n", buffer);
				#endif

				ARRAY_create(&arch->exported);

				for (name = strtok(buffer, "\n"); name; name = strtok(NULL, "\n"))
				{
					#if DEBUG_COMP
						fprintf(stderr, "Check %s global\n", name);
					#endif
					
					if (*name == '~')
						continue;
						
					file_name = strchr(name, ' ');
					if (file_name)
						*file_name++ = 0;
						
					len = strlen(name);
					optional = FALSE;

					for(;;)
					{
						c = name[len - 1];
						if (c == '?')
						{
							optional = TRUE;
							len--;
						}
						else if (c == '!')
							len--;
						else
							break;
					}

					name[len] = 0;

					if (!optional || CLASS_look_global(name, len) == NULL)
					{
						class = CLASS_find_export(file_name, name);
						
						if (file_name)
						{
							ALLOC(&class->data, len + 1);
							strcpy(class->data, file_name);
							class->data[len] = 0;
						}
						
						class = CLASS_check_global(class);

						#if DEBUG_COMP
							fprintf(stderr, "Add to load: %p %s\n", class, name);
						#endif
						class->component = COMPONENT_current;
						*((CLASS **)ARRAY_add(&arch->exported)) = class;
					}
				}

				FREE(&buffer);
			}

			if (pass & AR_FIND_ONLY) // That way the 'pass' flag is always ignored.
			{
				#if DEBUG_COMP
					fprintf(stderr, "<Load pass>\n");
				#endif

				ERROR_no_subst++;
				for (i = 0; i < ARRAY_count(arch->exported); i++)
				{
					TRY {
						#if DEBUG_COMP
							fprintf(stderr, "load %p %s\n", arch->exported[i], arch->exported[i]->name);
						#endif
						CLASS_load(arch->exported[i]);
					}
					CATCH
					{
						ERROR_print_prefix("warning");
						ERROR_print_at(stderr, TRUE, TRUE);
					}
					END_TRY
				}
				ERROR_no_subst--;

				ARRAY_delete(&arch->exported);
				arch->exported_classes_loaded = TRUE;
			}
		}
	}
	END_ERROR

	COMPONENT_current = current;
}

#if 0
// <dir> is the directory to browse.
// <name> is the name of the library <vendor>/<file> without the version.
// <version> is the requested version as a six character string "XXXYYY".

static char *exist_library(const char *dir, const char *name, const char *version)
{
	char *path;
	char *n;
	char *p;

	n = STRING_new_zero(name);

	path = (char *)FILE_cat(dir, n, NULL);
	if (FILE_exist(path))
		goto __RETURN_PATH;

	n = STRING_add(n, ".gambas", -1);
	path = (char *)FILE_cat(dir, n, NULL);
	if (FILE_exist(path))
		goto __RETURN_PATH;

	p = rindex(n, ':');
	if (p)
	{
		strcpy(p, ".gambas");
		path = (char *)FILE_cat(dir, n, NULL);
		if (FILE_exist(path))
			goto __RETURN_PATH;
	}

	path = NULL;

__RETURN_PATH:

	STRING_free(&n);
	return path;
}
#endif

static const char *read_version(const char *str, int *version)
{
	bool read_major = FALSE;
	int major = 0;
	int minor = 0;
	char c;
	
	*version = 0;
	
	for(;; str++)
	{
		c = *str;
		if (c >= '0' && c <= '9')
		{
			minor = minor * 10 + (c - '0');
			if (minor > 9999)
				return str;
		}
		else
		{
			if (!read_major)
			{
				major = minor;
				minor = 0;
				read_major = TRUE;
				
				if (c == '.')
					continue;
			}
			
			break;
		}
	}
	
	*version = major * 10000 + minor;
	return str;
}


static bool read_version_min_max(const char *str, int *ver_min, int *ver_max)
{
	if (!(*str >= '0' && *str <= '9'))
		return TRUE;
	
	str = read_version(str, ver_min);
		
	if (*str == '-')
	{
		str++;
		read_version(str, ver_max);
	}
	else
		*ver_max = *ver_min;
	
	return FALSE;
}


static char *search_library(const char *dir, const char *vendor, const char *name, int len_name, int version_min, int version_max)
{
	char *path;
	int len_path;
	char *chosen_path = NULL;
	int chosen_ver = 0;
	int ver_min, ver_max;
	
	if (*dir == '/')
		dir = FILE_cat(dir, vendor, NULL);
	else
		dir = FILE_cat(PROJECT_path, dir, vendor, NULL);
	
	if (!FILE_is_dir(dir))
		return NULL;
	
	FILE_dir_first(dir, NULL, GB_STAT_FILE);
	
	while (!FILE_dir_next(&path, &len_path))
	{
		if (len_path < (len_name + 11)) // "<name>@<X.Y>-<X.Y>.gambas"
			continue;
		
		if (!STRING_equal_same(&path[len_path - 7], ".gambas", 7))
			continue;
		
		if (!STRING_equal_same(path, name, len_name))
			continue;
		
		if (path[len_name] != ':')
			continue;
		
		if (read_version_min_max(&path[len_name + 1], &ver_min, &ver_max))
			continue;
		
		if (version_max < ver_min)
			continue;
		
		if (version_min > ver_max)
			continue;
		
		if (chosen_ver == 0 || chosen_ver < ver_min)
		{
			STRING_free(&chosen_path);
			chosen_path = STRING_new_zero(path);
			chosen_ver = ver_min;
		}
	}
	
	if (chosen_path)
	{
		path = (char *)FILE_cat(dir, chosen_path, NULL);
		STRING_free(&chosen_path);
		chosen_path = STRING_new(path, FILE_buffer_length());
	}
		
	return chosen_path;
}


char *ARCHIVE_search_path(const char *name)
{
	char *env;
	char *p;
	char *vendor;
	char *path;
	int len;
	int version_min, version_max;
	
	if (!_local_dir)
	{
		env = getenv("XDG_DATA_HOME");
		if (env && *env)
			_local_dir = STRING_new_zero(FILE_cat(env, "gambas3/lib", NULL));
		else
			_local_dir = STRING_new_zero(FILE_cat(FILE_get_home(), ".local/share/gambas3/lib", NULL));
	}
	
	p = index(name, '/');
	vendor = STRING_new_temp(name, p - name);
	name = p + 1;
	
	p = rindex(name, ':');
	if (!p)
		return NULL;
	
	if (read_version_min_max(&p[1], &version_min, &version_max))
		return NULL;
	
	len = p - name;
	
	path = search_library(_local_dir, vendor, name, len, version_min, version_max);
	if (path) return path;
	
	if (ARCHIVE_path)
	{
		path = search_library(ARCHIVE_path, vendor, name, len, version_min, version_max);
		if (path) return path;
	}
	
	path = search_library(COMPONENT_path, vendor, name, len, version_min, version_max);
	if (path) return path;
	
	return NULL;
}


void ARCHIVE_load(ARCHIVE *arch, bool load_exp)
{
	char *path;

	if (arch->path)
	{
		path = (char *)arch->path;
#if 0
		if (*arch->path == '/' && FILE_exist(arch->path))
		{
			path = (char *)arch->path;
		}
		else
		{
			if (*arch->path == ':')
			{
				if (!_local_dir)
				{
					env = getenv("XDG_DATA_HOME");
					if (env && *env)
						_local_dir = STRING_new_zero(FILE_cat(env, "gambas3/lib", NULL));
					else
						_local_dir = STRING_new_zero(FILE_cat(FILE_get_home(), ".local/share/gambas3/lib", NULL));
				}

				name = &arch->path[1];
			}
			else
				name = FILE_get_name(arch->path);

			// For backward-compatibility, library name is searched in the current folder too, without the vendor.

			if (!(path = exist_library(PROJECT_path, FILE_get_name(name))))
			{
				if (!_local_dir || !(path = exist_library(_local_dir, name)))
				{
					if (!ARCHIVE_path || !(path = exist_library(ARCHIVE_path, name)))
					{
						if (!(path = exist_library(COMPONENT_path, name)))
						{
							dir = STRING_new_zero(FILE_cat(PROJECT_exec_path, "bin", NULL));
							path = exist_library(dir, name);
							STRING_free(&dir);
						}
					}
				}
			}

			if (!path || !FILE_exist(path))
				THROW(E_LIBRARY, arch->name, "cannot find library");
		}
#endif
	}
	else
	{
		path = FILE_buffer();
		sprintf(path, ARCH_PATTERN, COMPONENT_path, arch->name);
	}

	arch->arch = ARCH_open(path);
	arch->current_component = COMPONENT_current;

	if (load_exp)
		ARCHIVE_load_exported_class(arch, AR_FIND_AND_LOAD); //, dep);
}


void ARCHIVE_create_main(const char *path)
{
	ARCHIVE_main = ARCHIVE_create(NULL, NULL);

	if (path)
		ARCHIVE_main->arch = ARCH_open(path);
	else
		ARCHIVE_main->arch = NULL;
}


void ARCHIVE_load_main()
{
	ARCHIVE_load_exported_class(ARCHIVE_main, AR_FIND_AND_LOAD);
}


void ARCHIVE_delete(ARCHIVE *arch)
{
	int i;
	LIST_remove(&ARCHIVE_list, arch, &arch->list);

	if (arch->arch)
		ARCH_close(arch->arch);

	for (i = 0; i < TABLE_count(arch->classes); i++)
		IFREE(TABLE_get_symbol(arch->classes, i)->name);
	
	TABLE_delete(&arch->classes);
	
	STRING_free(&arch->domain);
	STRING_free(&arch->version);
	STRING_free(&arch->path);

	FREE(&arch);
}


void ARCHIVE_init(void)
{
}


void ARCHIVE_exit(void)
{
	if (ARCHIVE_main)
		ARCHIVE_delete(ARCHIVE_main);

	STRING_free(&arch_pattern);
	STRING_free(&arch_prefix);
	STRING_free(&ARCHIVE_path);
	STRING_free(&_local_dir);
}


/* ### *parch must be initialized to NULL or a valid archive */
/* Returns a true archive, never the main archive if we are not running an executable */
bool ARCHIVE_find_from_path(ARCHIVE **parch, const char **ppath)
{
	int i;
	CLASS *class;
	const char *path = *ppath;
	const char *p;

	if (*parch)
		return FALSE;

	if (COMPONENT_current && COMPONENT_current->archive)
		*parch = COMPONENT_current->archive;
	else if (CP && CP->component && CP->component->archive)
		*parch = CP->component->archive;
	else
		*parch = NULL;

	//fprintf(stderr, "ARCHIVE_find_from_path: %s (%s)\n", *ppath, *parch ? (*parch)->name : "NULL");

	if (strncmp(path, "./", 2) == 0 && path[2])
	{
		path += 2;
		p = index(path, '/');
		if (p)
		{
			int len = p - path;
			char name[len + 1];
			COMPONENT *comp;
			
			strncpy(name, path, len);
			name[len] = 0;
			
			comp = COMPONENT_find(name);
			if (comp && comp->archive)
			{
				*parch = comp->archive;
				*ppath = p + 1;
				return FALSE;
			}
		}
	}
	
	if (strncmp(path, ".../", 4) == 0)
	{
		path += 4;
		*parch = NULL;
	}
	else
	{
		i = 0;
		while (strncmp(path, "../", 3) == 0)
		{
			path += 3;
			/*if (*parch == NULL || *parch == ARCHIVE_main)
				continue;*/

			while (i < STACK_frame_count)
			{
				class = STACK_frame[i].cp;
				//fprintf(stderr, "%s [%d] %s / %s\n", path, i, class ? class->name : "NULL", class && class->component && class->component->archive ? class->component->archive->name : "NULL");
				if (class)
				{
					if (!class->component && *parch)
					{
						*parch = NULL;
						break;
					}
					if (class->component && class->component->archive != *parch)
					{
						*parch = class->component->archive;
						break;
					}
				}

				i++;
			}

			if (i == STACK_frame_count)
				*parch = NULL;
		}
	}

	if (*parch == NULL && FLAG.arch)
		*parch = ARCHIVE_main;

	*ppath = path;
	
	//fprintf(stderr, "--> '%s' / %s\n", *parch ? (*parch)->name : "(null)", *ppath);
	
	return *parch == NULL;
}

/* Can return the main archive even if we are not running an executable */
bool ARCHIVE_get_current(ARCHIVE **parch)
{
	if (COMPONENT_current && COMPONENT_current->archive)
		*parch = COMPONENT_current->archive;
	else if (CP && CP->component && CP->component->archive)
		*parch = CP->component->archive;
	else
		*parch = ARCHIVE_main;

	return *parch == NULL;
}


bool ARCHIVE_get(ARCHIVE *arch, const char **ppath, ARCHIVE_FIND *find)
{
	ARCH_FIND f;
	struct stat buf;

	if (!*ppath || **ppath == 0)
		return TRUE;

	if (ARCHIVE_find_from_path(&arch, ppath))
	{
		// no archive found, we try a lstat
		if (!PROJECT_path)
			return TRUE;
		FILE_chdir(PROJECT_path);
		if (stat(*ppath, &buf))
			return TRUE;

		find->pos = S_ISDIR(buf.st_mode) ? -1 : 0;
		find->sym = NULL;
		find->len = (int)buf.st_size;
		find->index = -1;
		find->arch = NULL;
		return FALSE;
	}
	
	if (!arch->arch) // archive has been created but loading has failed
		return TRUE;

	if (ARCH_find(arch->arch, *ppath, 0, &f))
		return TRUE;

	find->sym = f.sym;
	find->pos = f.pos;
	find->len = f.len;
	find->index = f.index;
	find->arch = arch;

	return FALSE;
}


bool ARCHIVE_exist(ARCHIVE *arch, const char *path)
{
	ARCHIVE_FIND find;

	//if (get_current(&arch, &path))
	//  return FALSE;

	return (!ARCHIVE_get(arch, &path, &find));
}


bool ARCHIVE_is_dir(ARCHIVE *arch, const char *path)
{
	ARCHIVE_FIND find;

	if (ARCHIVE_get(arch, &path, &find))
		return FALSE;

	return (find.pos < 0);
}


bool ARCHIVE_stat(ARCHIVE *arch, const char *path, FILE_STAT *info)
{
	ARCHIVE_FIND find = { 0 };
	struct stat buf;
	int ret;

	//if (get_current(&arch))
	//  THROW_SYSTEM(ENOENT, path);

	if (ARCHIVE_get(arch, &path, &find))
		return TRUE;
		//THROW_SYSTEM(ENOENT, path);

	if (find.arch)
		ret = fstat(find.arch->arch->fd, &buf);
	else
		ret = stat(PROJECT_path, &buf);

	if (ret)
		return TRUE;

	info->type = find.pos < 0 ? GB_STAT_DIRECTORY : GB_STAT_FILE;
	info->mode = 0400;

	info->size = find.len;
	info->atime = (int)buf.st_mtime;
	info->mtime = (int)buf.st_mtime;
	info->ctime = (int)buf.st_mtime;
	info->hidden = (*FILE_get_name(path) == '.');
	info->uid = (int)buf.st_uid;
	info->gid = (int)buf.st_gid;

	return FALSE;
}


void ARCHIVE_read(ARCHIVE *arch, int pos, void *buffer, int len)
{
	ARCH_read(arch->arch, pos, buffer, len);
}


bool ARCHIVE_dir_first(ARCHIVE *arch, const char *path, const char *pattern, int attr)
{
	ARCHIVE_FIND find;
	char abs_path[16];
	int abs_len;

	/*if (get_current(&arch, &path))
	{
		arch_dir = NULL;
		return;
	}*/

	if (ARCHIVE_get(arch, &path, &find))
	{
		arch_dir = NULL;
		return FALSE;
	}

	// ?? "." means that we want to browse the archive.
	if (!find.sym && !find.arch) // && !(path[0] == '.' && path[1] == 0))
	{
		// By calling FILE_dir_first() again with an absolute path, we are sure that next calls to
		// FILE_dir_next() will never call ARCHIVE_dir_next().
		FILE_dir_first(FILE_cat(PROJECT_path, path, NULL), pattern, attr);
		return TRUE;
	}

	if (pattern == NULL || !*pattern)
		pattern = "*";

	arch = find.arch;
	arch_dir = arch->arch;
	arch_index = 0;

	STRING_free(&arch_pattern);
	arch_pattern = STRING_new_zero(pattern);

	//if (arch_dir->header.version == 2)
	//{
		if (find.index >= 0)
			abs_len = sprintf(abs_path, "/%d:", find.index);
		else
			abs_len = 0;
	/*}
	else
	{
		ARCH_get_absolute_path(path, strlen(path), abs_path, &abs_len);

		if (abs_len && abs_path[abs_len - 1] != '/')
		{
			abs_path[abs_len] = '/';
			abs_len++;
		}
	}*/

	STRING_free(&arch_prefix);
	if (abs_len)
		arch_prefix = STRING_new(abs_path, abs_len);
	
	return FALSE;
}


bool ARCHIVE_dir_next(char **name, int *len, int attr)
{
	ARCH_SYMBOL *asym;
	SYMBOL *sym;
	char *s = NULL;
	int l = 0;
	int lenp;

	/*if (arch_fd < 0)
		return FILE_dir_next(name, len);*/

	if (!arch_dir)
		return TRUE;

	lenp = STRING_length(arch_prefix);

	for(;;)
	{
		if (arch_index >= arch_dir->header.n_symbol)
			return TRUE;

		//sym = &arch_dir->symbol[arch_index].sym;
		asym = &arch_dir->symbol[arch_dir->sort[arch_index]];
		sym = &asym->sym;

		if (arch_pattern == NULL)
			break;

		arch_index++;

		if (attr == GB_STAT_DIRECTORY && (asym->pos >= 0))
			continue;

		if (sym->len < lenp)
			continue;

		if (lenp)
		{
			if (strncmp(sym->name, arch_prefix, lenp))
				continue;
		}
		else // special case: root directory when header.version == 2
		{
			if (!strncmp(sym->name, "/", 1))
				continue;
		}

		s = sym->name + lenp;
		l = sym->len - lenp;

		if (l < 0)
			continue;

		if (memchr(s, '/', l))
			continue;

		if (!REGEXP_match(arch_pattern, STRING_length(arch_pattern), s, l))
			continue;

		break;
	}

	*name = s;
	*len = l;
	return FALSE;
}


bool ARCHIVE_check_addr(const char *addr)
{
	ARCHIVE *arch;
	ARCH *a;

	LIST_for_each(arch, ARCHIVE_list)
	{
		a = arch->arch;
		if (a && addr >= a->addr && addr < &a->addr[a->length])
			return FALSE;
	}

	return TRUE;
}

void ARCHIVE_browse(ARCHIVE *arch, void (*found)(const char *path, int64_t size))
{
	int i;
	ARCH_SYMBOL *asym;
	SYMBOL *sym;
	ARCH *a = arch->arch;
	char *path;
	char *temp;
	int size;
	int ip;

	for (i = 0; i < a->header.n_symbol; i++)
	{
		asym = &a->symbol[i];
		sym = &asym->sym;

		size = asym->len;

		path = STRING_new(sym->name, sym->len);
		for(;;)
		{
			if (*path != '/')
				break;

			ip = atoi(&path[1]);
			sym = &a->symbol[ip].sym;

			temp = path;
			path = STRING_new(sym->name, sym->len);
			if (path[sym->len - 1] != '/')
				path = STRING_add(path, "/", 1);
			path = STRING_add(path, strchr(temp, ':') + 1, 0);
			STRING_free(&temp);
		}

		(*found)(path, size);
		STRING_free(&path);
	}
}

char *ARCHIVE_get_version(ARCHIVE *arch)
{
	COMPONENT *current;
	char *buffer;
	int len;
	int n;
	char *line;
	
	if (!arch->version_loaded)
	{
		current = COMPONENT_current;
		COMPONENT_current = (COMPONENT *)arch->current_component;

		ON_ERROR_1(error_ARCHIVE_load_exported_class, current)
		{
			STREAM_load(".startup", &buffer, &len);
			
			n = 0;
			line = strtok(buffer, "\n");
			while (line)
			{
				n++;
				if (n == 4)
				{
					arch->version = STRING_new_zero(line);
					break;
				}
				line = strtok(NULL, "\n");
			}

			FREE(&buffer);
		}
		END_ERROR

		COMPONENT_current = current;
		arch->version_loaded = TRUE;
	}

	return arch->version;
}


void ARCHIVE_fork(ARCHIVE *arch, bool after)
{
	if (arch->jit_state == JIT_COMPILING && !after)
		JIT_wait_for_compilation(arch);
}
