/*
 * Copyright (c) 2005 Tama Communications Corporation
 *
 * This file is part of GNU GLOBAL.
 *
 * 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/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_LIMITS_H
#include <limits.h>
#endif

#include "char.h"
#include "checkalloc.h"
#include "die.h"
#include "env.h"
#include "find.h"
#include "gpathop.h"
#include "locatestring.h"
#include "strbuf.h"
#include "test.h"
#include "xargs.h"

#if !defined(ARG_MAX) && defined(_SC_ARG_MAX)
/* FIXME: in theory sysconf() can return -1L for unlimited */
#define ARG_MAX         sysconf(_SC_ARG_MAX)
#endif

static int exec_line_limit(int);
static char *repeat_find_read(void);
static void repeat_find_next(void);
static FILE *execute_command(XARGS *);
static XARGS *xargs_open_generic(const char *, int);

/*
 *
 * usage: a piece of code to achieve the following command line.
 *
 * [command line]
 *
 *	find . -type f -print | xargs grep pattern
 *
 * [code]
 *
 * char *p;
 * FILE *ip = popen("find . -type f -print", "r");
 * XARGS *xp = xargs_open_with_file("grep pattern", ip)
 * xp->ignore_error = 1;
 * while ((p = xargs_read(xp)) != NULL)
 *	puts(p);
 * xargs_close(xp);
 */
/**
 * exec_line_limit: upper limit of bytes of exec line.
 *
 *	@param[in]	length	command line length
 *	@return	0: unknown or cannot afford long line.
 *		> 0: upper limit of exec line
 */
static int
exec_line_limit(int length)
{
	long limit = 0;

#ifdef ARG_MAX
	/*
	 * POSIX.2 limits the exec(2) line length to ARG_MAX - 2048.
	 */
	limit = ARG_MAX - 2048;
	/*
	 * The reason is unknown but the xargs(1) in GNU findutils
	 * use this limit.
	 */
	if (limit > 128 * 1024)
		limit = 128 * 1024;
	/*
	 * Add the command line length.
	 * We estimates additional 80 bytes for popen(3).
	 *
	 * for "/bin/sh -c "				11bytes
	 * reserve					69bytes
	 * ----------------------------------------------------
	 * Total					80 bytes
	 */
	limit -= length + 80;

	limit -= env_size();
#endif
#if !defined(ARG_MAX) && defined(_WIN32)
	/*
	 * The limit lenght of the command line of cmd.exe is 2047
	 * characters on Windows 2000 or 8191 characters on Windows XP
	 * and later. The 80 below is for safety.
	 */
	limit = 8191 - length - 80;
#endif
	if (limit < 0)
               die("Negative exec line limit = %ld", limit);

	return limit;
}
/**
 * static char *repeat_find_read(void)
 *
 * repeatable find_read
 */
static char *repeat_lastpath;
static char *
repeat_find_read(void)
{
	if (repeat_lastpath)
		return repeat_lastpath;
	return repeat_lastpath = find_read();
}
static void
repeat_find_next(void)
{
	repeat_lastpath = NULL;
}

/**
 * Specified limitation by user. (e.g. gtags --max-args)
 */
#define LT_MAX ((xp->max_args == 0 || count < xp->max_args))

/**
 * Common processing for each XARGS_XXXX type.
 */
#define APPEND_ARGUMENT(p) {\
	char *path = (p);\
	length = strlen(path);\
	if (*path == ' ') {\
		if (xp->put_gpath && !test("b", ++path))\
			gpath_put(path, GPATH_OTHER);\
		continue;\
	}\
	if (strbuf_getlen(comline) + length + 2 > limit)\
		break;\
	xp->seqno++;\
	if (xp->put_gpath)\
		gpath_put(path, GPATH_SOURCE);\
	if (xp->skip_assembly && locatestring(path, ".s", MATCH_AT_LAST|IGNORE_CASE) != NULL) {\
		if (xp->verbose)\
			xp->verbose(path + 2, xp->seqno, 1);\
	} else {\
		if (xp->verbose)\
			xp->verbose(path + 2, xp->seqno, 0);\
		strbuf_putc(comline, ' ');\
		strbuf_puts(comline, quote_shell(path));\
		count++;\
	}\
}

/**
 * execute_command
 *
 *	@param[in]	xp	xargs structure
 *	@return		!=NULL: file pointer
 *			==NULL: end of argument
 *
 * This function constructs command line from the following,
 *	command: xp->command
 *	argument: each argument provider
 * execute it on a pipe line, and return the file pointer.
 */
static FILE *
execute_command(XARGS *xp)
{
	int limit;
	STRBUF *comline = strbuf_open(0);
	int count = 0;
	int length;
	FILE *pipe = NULL;
	char *p, *meta_p;

#if defined(_WIN32) && !defined(__CYGWIN__)
	/*
	 * If the command starts with a quote, CMD.EXE requires the entire
	 * command line to be quoted.
	 */
	if (*xp->command == '"')
		strbuf_putc(comline, '"');
#endif
	/*
	 * Copy the part before '%s' of the command skeleton.
	 * The '%s' in the skeleton is replaced with given arguments.
	 */
	meta_p = locatestring(xp->command, "%s", MATCH_FIRST);
	if (meta_p) {
		strbuf_nputs(comline, xp->command, meta_p - xp->command);
		limit = exec_line_limit(strlen(meta_p + 2));
	} else {
		strbuf_puts(comline, xp->command);
		limit = exec_line_limit(0);
	}
	/*
	 * Append arguments as many as possible.
	 */
	switch (xp->type) {
	case XARGS_FILE:
		for (
			/* initial */
			fseek(xp->ip, xp->fptr, SEEK_SET)
			;
			/* continuation condition */
			(LT_MAX &&
				((p = (strbuf_getlen(xp->path) > 0 ?
				strbuf_value(xp->path) :
				strbuf_fgets(xp->path, xp->ip, STRBUF_NOCRLF))) != NULL))
			;
			/* preparation */
			strbuf_reset(xp->path)
		)
			APPEND_ARGUMENT(p);
		xp->fptr = ftell(xp->ip);
		break;
	case XARGS_ARGV:
		for (; LT_MAX && xp->argc > 0; xp->argc--, xp->argv++)
			APPEND_ARGUMENT(xp->argv[0])
		break;
	case XARGS_STRBUF:
		for (; LT_MAX && xp->curp < xp->endp; xp->curp += length + 1)
			APPEND_ARGUMENT(xp->curp)
		break;
	case XARGS_FIND:
		for (; LT_MAX && (p = repeat_find_read()) != NULL; repeat_find_next())
			APPEND_ARGUMENT(p)
		break;
	}
	/*
	 * Copy the left part of the command skeleton.
	 */
	if (meta_p) {
		strbuf_putc(comline, ' ');
		strbuf_puts(comline, meta_p + 2);
	}
#if defined(_WIN32) && !defined(__CYGWIN__)
	if (*xp->command == '"')
		strbuf_putc(comline, '"');
#endif
	if (count > 0) {
		pipe = popen(strbuf_value(comline), "r");
		if (pipe == NULL)
			die("cannot execute command '%s'.", strbuf_value(comline));
	}
	strbuf_close(comline);
	return pipe;
}
/**
 * xargs_open_generic: allocate generic part of xargs structure.
 *
 *	@param[in]	command	command line except for the arguments.
 *	@param[in]	max_args 0: no limit, >0: max argument
 *	@return		xargs structure	
 */
static XARGS *
xargs_open_generic(const char *command, int max_args)
{
	XARGS *xp;

	xp  = check_calloc(sizeof(XARGS), 1);
	xp->command = check_strdup(command);
	xp->type = 0;
	xp->pipe = NULL;
	xp->result = strbuf_open(0);
	xp->end_of_arg = 0;
	xp->unread = 0;
	xp->max_args = max_args;
	xp->seqno = 0;
	xp->skip_assembly = 0;
	/*
	 * Procedure to display verbose message.
	 * proc(path, seqno, skip)
	 */
	xp->verbose = NULL;
	/*
	 * By default, the error status returned by pclose() is not ignored,
	 * because global(1) doesn't return error code when the target
	 * was not found.
	 * Some commands like grep(1) return error code when the target
	 * was not found. If you would like to use such command, set the
	 * flag to 1.
	 */
	xp->ignore_error = 0;
	/*
	 * By default, we doesn't put the path to GPATH.
	 * This option is prepared for createtags() and updatetags().
	 */
	xp->put_gpath = 0;
	/*
	 * By default, we don't cut off the blanks at the tail of the line.
	 */
	xp->trim_line = 0;

	return xp;
}
/*
 * xargs_open_with_file: open xargs stream using file
 *
 *	@param[in]	command	command skeleton.
 *	@param[in]	max_args 0: no limit, >0: max argument
 *	@param[in]	ip	file pointer
 *	@return		xargs structure
 *
 * The '%s' in the command skeleton is replaced with given arguments.
 * If '%s' doesn't exist, the arguments is appended to the tail of the
 * skeleton.
 */
XARGS *
xargs_open_with_file(const char *command, int max_args, FILE *ip)
{
	XARGS *xp = xargs_open_generic(command, max_args);

	xp->type = XARGS_FILE;
	xp->ip = ip;
	xp->path = strbuf_open(0);
	xp->fptr = 0;
	return xp;
}
/*
 * xargs_open_with_argv: open xargs stream using argv
 *
 *	@param[in]	command	command skeleton.
 *	@param[in]	max_args 0: no limit, >0: max argument
 *	@param[in]	argc	argument number
 *	@param[in]	argv	argument array
 *	@return		xargs structure
 *
 * The '%s' in the command skeleton is replaced with given arguments.
 * If '%s' doesn't exist, the arguments is appended to the tail of the
 * skeleton.
 */
XARGS *
xargs_open_with_argv(const char *command, int max_args, int argc, char *const *argv)
{
	XARGS *xp = xargs_open_generic(command, max_args);

	xp->type = XARGS_ARGV;
	xp->argc = argc;
	xp->argv = argv;
	return xp;
}
/*
 * xargs_open_with_strbuf: open xargs stream using string buffer
 *
 *	@param[in]	command	command skeleton.
 *	@param[in]	max_args 0: no limit, >0: max argument
 *	@param[in]	sb	string buffer
 *	@return		xargs structure
 *
 * The '%s' in the command skeleton is replaced with given arguments.
 * If '%s' doesn't exist, the arguments is appended to the tail of the
 * skeleton.
 */
XARGS *
xargs_open_with_strbuf(const char *command, int max_args, STRBUF *sb)
{
	XARGS *xp = xargs_open_generic(command, max_args);

	xp->type = XARGS_STRBUF;
	xp->curp = strbuf_value(sb);
	xp->endp = xp->curp + strbuf_getlen(sb);
	return xp;
}
/*
 * xargs_open_with_find: open xargs stream using find().
 *
 *	@param[in]	command	command skeleton.
 *	@param[in]	max_args 0: no limit, >0: max argument
 *	@return		xargs structure
 *
 * The '%s' in the command skeleton is replaced with given arguments.
 * If '%s' doesn't exist, the arguments is appended to the tail of the
 * skeleton.
 */
XARGS *
xargs_open_with_find(const char *command, int max_args)
{
	XARGS *xp = xargs_open_generic(command, max_args);

	xp->type = XARGS_FIND;
	return xp;
}
/**
 * xargs_read: read a record from xargs stream
 *
 *	@param[in]	xp	xargs structure
 *	@return		result line
 */
char *
xargs_read(XARGS *xp)
{
	assert(xp != NULL);
	if (xp->end_of_arg)
		return NULL;
	if (xp->unread) {
		xp->unread = 0;
		return strbuf_value(xp->result);
	}
	if (xp->pipe && strbuf_fgets(xp->result, xp->pipe, STRBUF_NOCRLF) != NULL) {
		if (xp->trim_line)
			strbuf_trim(xp->result);
		return strbuf_value(xp->result);
	}
	if (xp->pipe)
		if (pclose(xp->pipe) != 0 && !xp->ignore_error)
			die("command failed in xargs_read().");
	/*
	 * Switch to the next segment.
	 */
	do {
		xp->pipe = execute_command(xp);
		if (xp->pipe && strbuf_fgets(xp->result, xp->pipe, STRBUF_NOCRLF) != NULL) {
			if (xp->trim_line)
				strbuf_trim(xp->result);
			return strbuf_value(xp->result);
		}
		if (xp->pipe) {
			if (pclose(xp->pipe) != 0 && !xp->ignore_error)
				die("command failed in xargs_read().");
		} else {
			xp->end_of_arg = 1;
		}
	} while (!xp->end_of_arg);

	return NULL;
}
/**
 * xargs_unread: push back a record to xargs stream
 *
 *	@param[in]	xp	xargs structure
 */
void
xargs_unread(XARGS *xp)
{
	assert(xp != NULL);
	xp->unread = 1;
}
/**
 * xargs_close(xp)
 *
 *	@param[in]	xp	xargs structure
 */
int
xargs_close(XARGS *xp)
{
	int count;

	assert(xp != NULL);
	count = xp->seqno;
	assert(xp->pipe == NULL);
	free(xp->command);
	strbuf_close(xp->result);

	switch (xp->type) {
	case XARGS_FILE:
		strbuf_close(xp->path);
		break;
	case XARGS_ARGV:
	case XARGS_STRBUF:
		/* Nothing to do */
		break;
	}
	free(xp);
	return count;
}
