/*
    scconfig - detect features of the system or the host/target computer
    Copyright (C) 2009..2012  Tibor Palinkas

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 31 Milk Street, # 960789 Boston, MA 02196 USA

		Project page: http://repo.hu/projects/scconfig
		Contact (email and IRC): http://igor2.repo.hu/contact.html
*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include "libs.h"
#include "log.h"
#include "db.h"
#include "dep.h"

int find_sys_ptrwidth(const char *name, int logdepth, int fatal)
{
	char *end, W[32];
	char *out = NULL;
	int w;

	char *test_c =
		NL "#include <stdio.h>"
		NL "int main() {"
		NL "	void *ptr;"
		NL "	printf(\"%d\\n\", sizeof(ptr));"
		NL "	return 0;"
		NL "}"
		NL;

	require("cc/cc", logdepth, fatal);

	report("Checking for pointer width... ");
	logprintf(logdepth, "find_sys_ptrwidth: trying to find pointer width...\n");
	logdepth++;

	if (compile_run(logdepth, test_c, NULL, NULL, NULL, &out) == 0) {
		w = strtol(out, &end, 10);
		if ((*end != '\0') && (*end != '\n') && (*end != '\r')) {
			report("FAILED (test code failed)\n");
			logprintf(logdepth+1, "FAILED: returned '%s' which is not a valid decimal number (at '%s')\n", out, end);
			return 1;
		}
		sprintf(W, "%d", w * 8);
		report("OK (%s bits)\n", W);
		put("sys/ptrwidth", W);
		logprintf(logdepth+1, "OK (%s bits)\n", W);
	}
	return 0;
}

int find_sys_byte_order(const char *name, int logdepth, int fatal)
{
	const char *test_c =
		NL "#include <stdio.h>"
		NL "int main() {"
		NL "	long int i = 8;"
		NL "	char *s = (char *)&i;"
		NL "	printf(\"%d\\n\", s[0]);"
		NL "	return 0;"
		NL "}"
		NL;
	const char* test_c_blind_template =
		NL "#include <stdio.h>"
		NL "#include <endian.h>"
		NL "#ifndef __BYTE_ORDER"
		NL "#error \"ERROR 1\""
		NL "void void *;"
		NL "#endif"
		NL "#ifndef __%s_ENDIAN"
		NL "#error \"ERROR 2\""
		NL "char char *;"
		NL "#endif"
		NL "#if __BYTE_ORDER != __%s_ENDIAN"
		NL "#error \"ERROR 3\""
		NL "int int *;"
		NL "#endif"
		NL "int main() {"
		NL "	puts(\"OK\");"
		NL "	return 0;"
		NL "}"
		NL;
	const char *key = "sys/byte_order";
	const char *endians1[] = { "LITTLE", "BIG", NULL };
	const char *endians2[] = { "LSB",    "MSB", NULL };
	int index;
	char test_c_blind[1024];
	char *end;
	const char *W;
	char *out = NULL;
	int w;

	require("cc/cc", logdepth, fatal);

	report("Checking for byte order... ");
	logprintf(logdepth, "find_sys_byte_order: trying to find byte order...\n");
	logdepth++;

	if ((!isblind(db_cwd)) && compile_run(logdepth, test_c, NULL, NULL, NULL, &out) == 0) {
		w = strtol(out, &end, 10);
		if (((*end != '\0') && (*end != '\n') && (*end != '\r')) || ((w != 0) && (w != 8))) {
			report("FAILED (test code failed)\n");
			logprintf(logdepth+1, "FAILED: returned '%s' which is not a valid decimal number (at '%s')\n", out, end);
			return 1;
		}
		if (w == 0)
			W = "MSB";
		else
			W = "LSB";

		report("OK (%s first)\n", W);
		put("sys/byte_order", W);
		logprintf(logdepth+1, "OK (%s first)\n", W);
		return 0;
	}

	for (index=0; endians1[index]!=NULL; ++index)
	{
		sprintf(test_c_blind, test_c_blind_template, endians1[index], endians1[index]);

		if (compile_run(logdepth, test_c_blind, NULL, NULL, NULL, NULL) == 0)
		{
			W = endians2[index];
			report("OK (%s first)\n", W);
			put(key, W);
			return 0;
		}
	}

	report("FAILED (cannot determine byte order, you must supply it)\n");
	return try_fail(logdepth, key);
}

static int test_shell_eats_backslash(int logdepth)
{
	char *test = "echo c:\\n";
	char *out;


	logprintf(logdepth, "testing if shell eats \\...\n");
	run_shell(logdepth+1, test, &out);
	if (out == NULL) {
		logprintf(logdepth+1, "oops, couldn't run shell?! (returned NULL)\n");
		report("ERROR: shell fails.");
		abort();
	}
	if (out[2] == '\\') {
		logprintf(logdepth, "shell does NOT eat \\...\n");
		put("sys/shell_eats_backslash", sfalse);
		free(out);
		return 0;
	}

	free(out);
	logprintf(logdepth, "shell eats \\...\n");
	put("sys/shell_eats_backslash", strue);
	return 1;
}


static int try_get_cwd(int logdepth, const char *cmd)
{
	char *cwd, *end;
	run_shell(logdepth+1, cmd, &cwd);
	if (cwd != NULL) {
		end = strpbrk(cwd, "\r\n");
		if (end != NULL)
			*end = '\0';
		if (*cwd != '\0') {
			end = cwd + strlen(cwd) - 1;
			while((*end == ' ') || (*end == '\t')) { *end = '\0'; end--; }
			put("sys/tmp", cwd);
			/* ugly hack for win32: paths there start as c:\ */
			if ((cwd[1] == ':') && (cwd[2] == '\\'))
				append("sys/tmp", "\\");
			else
				append("sys/tmp", "/");
			logprintf(logdepth, "cwd is '%s'\n", get("sys/tmp"));
			free(cwd);
			return 1;
		}
		else
			free(cwd);
	}
	return 0;
}

static int try_tmp(int logdepth)
{
	char *fn;

	logprintf(logdepth, "validating temp dir '%s'\n", get("sys/tmp"));
	fn = tempfile_new_noabort("");
	if (fn != NULL) {
		unlink(fn);
		free(fn);
		logprintf(logdepth, "temp dir works!\n");
		return 1;
	}

	logprintf(logdepth, "temp dir fails\n");
	return 0;
}

/* test temp dir with all sort of workarounds */
static int try_tmp_all(int logdepth)
{
	const char *tmp, *si;
	char c;
	char *t, *so, *old_tmp;
	int eats, n;

	tmp = get("sys/tmp");

	/* path must end in path separator */
	c = tmp[strlen(tmp)-1];
	if ((c != '/') && (c != '\\')) {
		append("sys/tmp", "/");
		tmp = get("sys/tmp");
	}

	logprintf(logdepth, "trying detected temp dir '%s'\n", tmp);
	if (try_tmp(logdepth+1)) return 1;

	/* try msys-on-windows hack: if path starts with /d/something, try d:/ instead */
	if ((tmp[0] == '/') && (isalpha(tmp[1])) && (tmp[2] == '/')) {
		/* for the next test we shouldn't use our half-detected tmp path but go with . */
		old_tmp = strclone(tmp);
		put("sys/tmp", "");
		eats = istrue(get("sys/shell_eats_backslash"));
		tmp = old_tmp;
		logprintf(logdepth, "tmp2='%s' eats=%d\n", tmp, eats);
		t = malloc(strlen(tmp) * 2);
		t[0] = tmp[1];
		t[1] = ':';
		for(si = tmp + 2, so = t + 2; *si != '\0'; si++, so++) {
			if (*si == '/') {
				*so = '\\';
				if (eats) {
					for(n = 0; n < 3; n++) {
						so++;
						*so = '\\';
					}
				}
			}
			else
				*so = *si;
		}
		*so = '\0';
		free(old_tmp);

		logprintf(logdepth, "trying windows fix: '%s'\n", t);
		put("sys/tmp", t);
		free(t);
		if (try_tmp(logdepth+1)) {
			if (eats)
				put("sys/path_sep", "\\\\\\\\");
			else
				put("sys/path_sep", "\\");
			put("sys/path_sep_escaped", "\\\\");
			return 1;
		}
		tmp = get("sys/tmp");
	}

	/* fail. Set back tmp to empty so next command has a chance to run */
	put("sys/tmp", "");
	return 0;
}

int find_tmp(const char *name, int logdepth, int fatal)
{
	const char *usertmp;

	if (in_cross_target) {
		report("Temp dir for cross compilation target is the same as for host...");
		logprintf(logdepth, "Copying temp dir from host to target\n");
		require("/host/sys/tmp", logdepth, fatal);
		usertmp = get("/host/sys/tmp");
		if (usertmp == NULL) {
			report("Host temp dir not found.\n");
			logprintf(logdepth, "Host temp dir not found.\n");
			return 1;
		}
		put("sys/tmp", usertmp);
		return 0;
	}

	/* we need shell for later tests; do this detection in . */
	put("sys/tmp", "");
	require("sys/shell", logdepth, fatal);

	put("sys/path_sep", "/");
	put("sys/path_sep_escaped", "/");

	report("Detecting temp dir...");
	logprintf(logdepth, "Finding temp dir (current working directory)...\n");

	usertmp = get("/arg/sys/tmp");

	/* . as tmp would fail for commands including a "cd" - this would cause
	   temporary files left in the target dir. We start out with empty
	   string (which is ., but on windows ./ would fail), and run
	   pwd (without cd) to find out the current directory (as getcwd() is not
	   portable). If pwd fails, we stay with ./ */
	put("sys/tmp", "");

	/* we need to know about shell backslash problem regardless of how
	   we end up with tmp - currently tmp is ., where the test could run
	   safely */
	test_shell_eats_backslash(logdepth+1);

	/* Special case: cross-compilation with emulator; we can not assume
	   the emulator uses the same paths as the host system, while we mix
	   accessing files from host and emu. If we stay in ., both emulator
	   and host system should be (more or less) happy. */
	if (istarget(db_cwd) && iscross) {
		if (usertmp == NULL) {
			report("using temp dir . for cross-compilation\n");
			logprintf(logdepth, "staying with . for cross-compilation\n");
		}
		else {
			put("sys/tmp", usertmp);
			report("using user supplied temp dir '%s' for cross-compilation\n", usertmp);
			logprintf(logdepth, "using user supplied temp dir '%s' for cross-compilation\n", usertmp);
			logprintf(logdepth, "Path sep: '%s'\n", get("sys/path_sep"));
		}
		return 0;
	}

	if ((usertmp != NULL))
			put("sys/tmp", usertmp);

	if (
	   ((usertmp != NULL) && (try_tmp_all(logdepth+2))) ||                      /* try user supplied temp dir */
	   ((try_get_cwd(logdepth+1, "pwd")) && (try_tmp_all(logdepth+2))) ||       /* try pwd for finding out cwd */
	   ((try_get_cwd(logdepth+1, "echo %cd%") && (try_tmp_all(logdepth+2))))) { /* try windows-specific way for finding out cwd */

		report(" validated %s\n", get("sys/tmp"));
		logprintf(logdepth, "Detected temp dir '%s'\n", get("sys/tmp"));
		logprintf(logdepth, "Path sep: '%s'\n", get("sys/path_sep"));
		return 0;
	}

	put("sys/tmp", "");
	report("using temp dir fallback .\n");
	logprintf(logdepth, "all temp directories failed, using . as tmp\n");
	logprintf(logdepth, "Path sep: '%s'\n", get("sys/path_sep"));
	return 0;
}

int test_shell(const char *shell, int logdepth, int quote)
{
	char *test = "echo hello";
	char *cmd;
	char *out;
	char *q;

	if (quote)
		q = "\"";
	else
		q = "";

	logprintf(logdepth, "testing '%s' as shell\n", shell);
	cmd = malloc(strlen(test) + strlen(shell) + 8);
	sprintf(cmd, "%s %s%s%s", shell, q, test, q);

	run(logdepth+1, cmd, &out);

	free(cmd);

	if ((out != NULL) && (strncmp(out, "hello", 5) == 0)) {
		put("sys/shell", shell);
		if (quote)
			put("sys/shell_needs_quote", strue);
		else
			put("sys/shell_needs_quote", sfalse);
		logprintf(logdepth, "accepted.\n");
		free(out);
		return 1;
	}

	logprintf(logdepth, "refused.\n");
	free(out);
	return 0;
}

static int find_shell_escape(const char *name, int logdepth, int fatal, const char *shell)
{
	char cmdline[256];
	char **t;
	char *tests[] = {
		"\\", "\\ {}&;|",
		"^",  "^ &",
		NULL, NULL
	};
	(void) fatal;  /* not used */
	(void) shell;  /* not used */

	report("Looking for a shell escape character... ");
	logprintf(logdepth, "finding shell escape character...\n");

	for(t = tests; *t != NULL; t += 2) {
		char *s, *end, *out, *start;
		strcpy(cmdline, "echo ");
		end = cmdline+5;
		for(s = t[1]; *s != '\0'; s++) {
			*end++ = *t[0];
			*end++ = *s;
		}
		*end = '\0';
		run(logdepth+1, cmdline, &out);
		if (out != NULL) {
			int res;
			if (*out == '\"') /* wine likes to wrap the output in quotes for some reason */
				start = out+1;
			else
				start = out;

			res = strncmp(start, t[1], strlen(t[1]));
			free(out);
			if (res == 0) {
				report("found: '%s'\n", t[0]);
				logprintf(logdepth, "found shell escape char '%s'\n", t[0]);
				put("sys/shell_escape_char", t[0]);
				return 0;
			}
		}
	}
	report("NOT FOUND\n");
	logprintf(logdepth, "shell escape character not found\n");

	return 1;
}

int find_shell(const char *name, int logdepth, int fatal)
{
	const char *shells[] = {
		"/bin/sh -c",
		"/bin/bash -c",
		"bash -c",
		"cmd.exe /c",
		"sh -c",
		"/bin/dash -c",
		"dash -c",
		"/bin/ksh -c",
		"ksh -c",
		NULL
	};
	const char **s;

	if (cross_blind) {
		const char *shell = get("/arg/sys/target-shell");
		if (shell == NULL) {
			report("Need to specify sys/target-shell in blind cross compiling mode, because the shell cannot be detected (note: scconfig will not attempt to run the target shell)\n");
			exit(1);
		}

		put("sys/shell", shell);
		report("Blind cross compiling: accepting '%s' as shell\n", shell);
		logprintf(logdepth, "Blind cross compiling: accepting '%s' as shell\n", shell);
		return 0;
	}

	report("Looking for a shell... ");
	logprintf(logdepth, "finding a shell\n");

	for(s = shells; *s != NULL; s++) {
		if ((test_shell(*s, logdepth+1, 0)) || (test_shell(*s, logdepth+1, 1))) {
			report("%s\n", *s);
			logprintf(logdepth, "found a shell '%s', need quote: %s\n", *s, get("sys/shell_needs_quote"));
			return find_shell_escape(name, logdepth, fatal, *s);
		}
	}

	report("NOT FOUND\n");
	logprintf(logdepth, "shell not found\n");
	return 1;
}
