/* ccdv.c */

/* Ported to Win32 by Randy Heit
 * Maybe I got a little carried away. This is pure Win32 code without any CRT in sight.
 *
 * To build this, use one of these command lines:
 *
 * [MinGW] gcc -Os -s -nostdlib -o ccdv.exe ccdv-win32.c -lkernel32 -luser32
 * [MSC]   cl -O1 ccdv-win32.c -link -subsystem:console -opt:nowin98 kernel32.lib user32.lib 
 *
 * Rewriting this to not use any global variables can save 512 bytes when compiled with MSC
 * because it allows the .data section to be ommitted, which means the header can occupy
 * 512 bytes rather than 1024. With GCC, it doesn't help the size any, since GCC still has
 * the separate .idata and .rdata sections. Since MSC really doesn't need this tool,
 * I'm not bothering with that size optimization.
 */

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#define COLOR_SUCCESS	(FOREGROUND_GREEN|FOREGROUND_INTENSITY)                 /* green */
#define COLOR_FAILURE	(FOREGROUND_RED|FOREGROUND_INTENSITY)                   /* red */
#define COLOR_WARNING	(FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_INTENSITY)  /* yellow */
#define COLOR_COMMAND	(FOREGROUND_GREEN|FOREGROUND_BLUE|FOREGROUND_INTENSITY) /* cyan */

#define TEXT_BLOCK_SIZE 8192
#define INDENT 2

PROCESS_INFORMATION gCCP;
size_t gNBufUsed, gNBufAllocated;
char *gBuf;
char *gAction;
char *gTarget;
char *gAr;
char *gArLibraryTarget;
BOOL  gDumpCmdArgs;
char *gArgsStr;
int gColumns;
int gExitStatus;
HANDLE gHeap;

HANDLE gStdOut, gStdErr;

#ifdef __GNUC__
#define REGPARM(x) __attribute((regparm(x)))
#else
#define REGPARM(x)
#endif

void REGPARM(1) perror(const char *string)
{
	char *buffer;
	DWORD error = GetLastError();
	DWORD len = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER|FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL, error, 0, (LPTSTR)&buffer, 0, NULL);
	DWORD wrote;
	WriteFile (gStdErr, string, lstrlen(string), &wrote, NULL);
	if(len == 0)
	{
		char errcode[9];
		wsprintf(errcode, "%08x", error);
		WriteFile (gStdErr, ": Error ", 8, &wrote, NULL);
		WriteFile (gStdErr, errcode, 8, &wrote, NULL);
	}
	else
	{
		WriteFile (gStdErr, ": ", 2, &wrote, NULL);
		WriteFile (gStdErr, buffer, len, &wrote, NULL);
		LocalFree(buffer);
	}
	WriteFile (gStdErr, "\n", 1, &wrote, NULL);
}

static void DumpFormattedOutput()
{
	CONSOLE_SCREEN_BUFFER_INFO info;
	DWORD out;
	WORD color;
	char *cp;
	char spaces[8 + 1];
	char *saved;
	int curcol;
	int i;

	if(!GetConsoleScreenBufferInfo(gStdOut, &info))
	{
		WriteFile(gStdOut, gBuf, lstrlen(gBuf), &out, NULL);
		WriteFile(gStdOut, "\n", 1, &out, NULL);
		return;
	}

	for(i = 0; i < 8; ++i)
	{
		spaces[i] = ' ';
	}
	spaces[i] = '\0';

	color = info.wAttributes & ~(FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE|FOREGROUND_INTENSITY);
	curcol = 0;
	saved = NULL;
	if(gDumpCmdArgs)
	{
		SetConsoleTextAttribute(gStdOut, color | COLOR_COMMAND); 
		WriteConsole(gStdOut, gBuf, lstrlen(gArgsStr)+1, &out, NULL);
		SetConsoleTextAttribute(gStdOut, info.wAttributes);
	}
	for(cp = gBuf + lstrlen(gArgsStr) + 1;; cp++)
	{
		if(*cp == '\0')
		{
			if(saved != NULL)
			{
				cp = saved;
				saved = NULL;
			}
			else
				break;
		}
		if(*cp == '\r')
			continue;
		if(*cp == '\t')
		{
			saved = cp + 1;
			cp = spaces + 8 - (8 - ((curcol - INDENT - 1) % 8));
		}
		if(curcol == 0)
		{
			for(i = INDENT; --i >= 0;)
				WriteConsole(gStdOut, " " ,1, &out, NULL);
			curcol = INDENT;
		}
		WriteConsole(gStdOut, cp, 1, &out, NULL);
		if(++curcol == (gColumns - 1))
		{
			WriteConsole(gStdOut, "\n", 1, &out, NULL);
			curcol = 0;
		}
		else if(*cp == '\n')
			curcol = 0;
	}
	HeapFree(gHeap, 0, gBuf);
}	/* DumpFormattedOutput */

static void Wait(void)
{
	DWORD exitcode;
	WaitForSingleObject(gCCP.hProcess, INFINITE);
	GetExitCodeProcess(gCCP.hProcess, &exitcode);
	gExitStatus = (int)exitcode;
}	/* Wait */

static DWORD WINAPI SlurpThread(LPVOID foo)
{
	HANDLE fd = (HANDLE)foo;
	char *newbuf;
	DWORD ntoread;
	DWORD nread;

	for(;;)
	{
		if(gNBufUsed == (gNBufAllocated - 1))
		{
			if((newbuf =
				(char *) HeapReAlloc(gHeap, 0, gBuf,
								 gNBufAllocated + TEXT_BLOCK_SIZE)) == NULL)
			{
				return 1;
			}
			gNBufAllocated += TEXT_BLOCK_SIZE;
			gBuf = newbuf;
		}
		ntoread = (gNBufAllocated - gNBufUsed - 1);
		if(!ReadFile(fd, gBuf + gNBufUsed, ntoread, &nread, NULL))
		{
			return 2;
		}
		else if(nread > 0)
		{
			gNBufUsed += nread;
			gBuf[gNBufUsed] = '\0';
		}
	}
	return 0;
}

static void REGPARM(2) WriteAction(HANDLE hStdOut, const char *suffix)
{
	DWORD out;

	WriteFile(hStdOut, gAction, lstrlen(gAction), &out, NULL);
	if(gTarget != NULL)
	{
		WriteFile(hStdOut, " ", 1, &out, NULL);
		WriteFile(hStdOut, gTarget, lstrlen(gTarget), &out, NULL);
	}
	WriteFile(hStdOut, suffix, 3, &out, NULL);
}

static int REGPARM(2) Slurp(HANDLE fd, HANDLE hStdOut)
{
	HANDLE handles[2];
	DWORD waitstate;
	DWORD exitcode;
	DWORD out;
	const char *trail = "/-\\|", *trailcp;
	CONSOLE_SCREEN_BUFFER_INFO info;
	CONSOLE_CURSOR_INFO cursorInfo;
	WORD color, colors[5];
	int ncells, i;

	trailcp = trail;
	if(hStdOut != NULL)
	{
		WriteAction(hStdOut, "...");
	}

	handles[0] = gCCP.hProcess;
	handles[1] = CreateThread(NULL, 0, SlurpThread, (LPVOID)fd, 0, NULL);

	if(handles[1] == 0)
	{
		perror("ccdv: CreateThread");
		return -1;
	}

	if(hStdOut != NULL)
	{
		GetConsoleScreenBufferInfo(hStdOut, &info);
		info.dwCursorPosition.X = info.dwSize.X - 9;
		if(GetConsoleCursorInfo(hStdOut, &cursorInfo))
		{
			cursorInfo.bVisible = FALSE;
			SetConsoleCursorInfo(hStdOut, &cursorInfo);
		}
		else
		{
			cursorInfo.bVisible = TRUE;
		}
	}

	gExitStatus = 0xabadcafe;
	while(gExitStatus == 0xabadcafe)
	{
		waitstate = WaitForMultipleObjects(2, handles, FALSE, 1000);
		switch(waitstate)
		{
		case WAIT_TIMEOUT:
			if(hStdOut != NULL)
			{
				SetConsoleCursorPosition(hStdOut, info.dwCursorPosition);
				WriteConsoleA(hStdOut, trailcp, 1, &out, NULL);
				if(*++trailcp == '\0')
					trailcp = trail;
			}
			break;

		case WAIT_FAILED:
			perror("ccdv: WaitForMultipleObjects");
			CloseHandle(handles[1]);
			return -1;

		case WAIT_OBJECT_0:
			GetExitCodeProcess(gCCP.hProcess, &exitcode);
			CloseHandle(handles[1]);
			gExitStatus = (int)exitcode;
			break;

		case WAIT_OBJECT_0+1:
			GetExitCodeThread(handles[1], &exitcode);
			CloseHandle(handles[1]);
			if(exitcode == 1)
			{
				perror("ccdv: HeapReAlloc");
				return -1;
			}
			else if(exitcode == 2)
			{
				perror("ccdv: ReadFile");
				return -1;
			}
			Wait();
			break;
		}
	}
	if(hStdOut != NULL)
	{
		info.dwCursorPosition.X = 0;
		SetConsoleCursorPosition(hStdOut, info.dwCursorPosition);
		WriteAction(hStdOut, ":  ");
		info.dwCursorPosition.X = info.dwSize.X - 10;
		if(gExitStatus == 0)
		{
			SetConsoleCursorPosition(hStdOut, info.dwCursorPosition);
			WriteConsoleA(hStdOut, "[OK]     ", 9, &out, NULL);
			color = ((gNBufUsed - lstrlen(gArgsStr)) < 4) ? COLOR_SUCCESS : COLOR_WARNING;
			ncells = 2;
		}
		else
		{
			SetConsoleCursorPosition(hStdOut, info.dwCursorPosition);
			WriteConsoleA(hStdOut, "[ERROR]  ", 9, &out, NULL);
			color = COLOR_FAILURE;
			ncells = 5;
			gDumpCmdArgs = 1;	/* print cmd when there are errors */
		}
		color |= info.wAttributes & ~(FOREGROUND_RED|FOREGROUND_GREEN|FOREGROUND_BLUE|FOREGROUND_INTENSITY);
		for(i = 0; i < ncells; ++i)
		{
			colors[i] = color;
		}
		info.dwCursorPosition.X++;
		WriteConsoleOutputAttribute(hStdOut, colors, ncells, info.dwCursorPosition, &out);
		if(!cursorInfo.bVisible)
		{
			cursorInfo.bVisible = TRUE;
			SetConsoleCursorInfo(hStdOut, &cursorInfo);
		}
		WriteConsole(hStdOut, "\n", 1, &out, NULL);
	}
	else
	{
		gDumpCmdArgs = (gExitStatus != 0);	/* print cmd when there are errors */
	}
	return (0);
}	/* SlurpProgress */

static const char *REGPARM(2) Basename(const char *path, int len)
{
	while(len > 0)
	{
		len--;
		if(path[len] == '/' || path[len] == '\\')
		{
			return path + len + 1;
		}
	}
	return path;
}	/* Basename */

static const char *REGPARM(2) Extension(const char *path, int len)
{
	while(len > 0)
	{
		len--;
		if(path[len] == '.')
		{
			return path + len;
		}
	}
	return "";
}	/* Extension */

static void Usage(void)
{
	static const char sUsage[] = "\
Usage: ccdv /path/to/cc CFLAGS...\n\
\n\
I wrote this to reduce the deluge Make output to make finding actual problems\n\
easier.  It is intended to be invoked from Makefiles, like this.  Instead of:\n\
\n\
	.c.o:\n\
		$(CC) $(CFLAGS) $(DEFS) $(CPPFLAGS) $< -c\n\
\n\
Rewrite your rule so it looks like:\n\
\n\
	.c.o:\n\
		@ccdv $(CC) $(CFLAGS) $(DEFS) $(CPPFLAGS) $< -c\n\
	.cpp.o:\n\
		@ccdv $(CXX) $(CFLAGS) $(DEFS) $(CPPFLAGS) $< -c\n\
\n\
ccdv 1.1.0 is Free under the GNU Public License.  Enjoy!\n\
  -- Mike Gleason, NcFTP Software <http://www.ncftp.com>\n\
  -- John F Meinel Jr, <http://ccdv.sourceforge.net>\n\
  ";
	DWORD out;
	WriteFile (gStdErr, sUsage, sizeof(sUsage)-1, &out, NULL);
	ExitProcess(96);
}	/* Usage */

static BOOL REGPARM(3) StepCmdLine(char **cmdline, char **arg, int *arglen)
{
	char *gArgsStr = *cmdline;

	// Skip whitespace
	while(*gArgsStr == ' ' || *gArgsStr == '\t' || *gArgsStr == '\n' || *gArgsStr == '\r')
	{
		gArgsStr++;
	}
	if(*gArgsStr == 0)
	{
		*cmdline = gArgsStr;
		return FALSE;
	}
	if(*gArgsStr == '\"')
	{ // It's a quoted string
		*arg = ++gArgsStr;
		while(*gArgsStr && *gArgsStr != '\"')
		{
			gArgsStr++;
		}
		*arglen = gArgsStr - *arg;
		if(*gArgsStr)
		{ // Ends with a quote
			gArgsStr++;
		}
		*cmdline = gArgsStr;
		return TRUE;
	}
	// It's a whitespace-separated string
	*arg = gArgsStr;
	while(*gArgsStr && *gArgsStr != ' ' && *gArgsStr != '\t' && *gArgsStr != '\n' && *gArgsStr != '\r')
	{
		gArgsStr++;
	}
	*arglen = gArgsStr - *arg;
	*cmdline = gArgsStr;
	return TRUE;
}

static void REGPARM(3) SetTarget(char **target, const char *arg, int arglen)
{
	const char *base;

	if (*target) HeapFree(gHeap, 0, *target);
	base = Basename(arg, arglen);
	arglen = arglen - (base - arg) + 1;
	*target = HeapAlloc(gHeap, 0, arglen);
	lstrcpyn(*target, base, arglen);
}

void mainCRTStartup(void)
{
	const char *ext;
	char *cmdline, *arg;
	int arglen, extlen;
	SECURITY_ATTRIBUTES security;
	STARTUPINFO siStartInfo;
	HANDLE pipeRd, pipeWr, pipeRdDup;
	char emerg[256];
	DWORD nread;
	int cc = 0;
	int yy = 0;
	int gcc = 0;
	int lemon = 0;
	CONSOLE_SCREEN_BUFFER_INFO bufferInfo;

	gExitStatus = 95;
	gHeap = GetProcessHeap();
	gArgsStr = cmdline = GetCommandLine();
	gStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
	gStdErr = GetStdHandle(STD_ERROR_HANDLE);

	if (!StepCmdLine(&cmdline, &arg, &arglen) ||	// Skip ccdv.exe
		!StepCmdLine(&cmdline, &arg, &arglen))		// Read name of process to run
	{
		Usage();
	}

	// "Running *argv[1]*"
	ext = Basename(arg, arglen);
	extlen = arglen - (ext - arg);
	gAction = HeapAlloc(gHeap, 0, 64+extlen);
	lstrcpy(gAction, "Running ");
	lstrcpyn(gAction+8, ext, extlen+1);

	if (extlen == 2 && lstrcmpi(gAction+8, "ar") == 0)
	{
		SetTarget(&gAr, arg, arglen);
	}
	else if((extlen == 2 &&
		(lstrcmpi(gAction+8, "cc") == 0 ||
		 lstrcmpi(gAction+8, "cl") == 0 ||
		 lstrcmpi(gAction+8, "ld") == 0)) ||
		(extlen == 3 && 
		 (lstrcmpi(gAction+8, "gcc") == 0 ||
		  lstrcmpi(gAction+8, "g++") == 0)) ||
		(extlen == 7 && lstrcmpi(gAction+8, "windres") == 0))
	{
		gcc = 1;
	}
	else if(extlen == 5 && lstrcmpi(gAction+8, "lemon") == 0)
	{
		lemon = 1;
	}

	while(StepCmdLine(&cmdline, &arg, &arglen))
	{
		if(gcc && arglen == 2 && arg[0] == '-' && arg[1] == 'o')
		{
			if(StepCmdLine(&cmdline, &arg, &arglen))
			{
				ext = Extension(arg, arglen);
				if(ext[0] != '.' || (ext[1] != 'o' && ext[1] != 'O'))
				{
					lstrcpy(gAction, "Linking");
					SetTarget(&gTarget, arg, arglen);
				}
			}
			continue;
		}
		else if(arg[0] == '-' || arg[0] == '+' || arg[0] == '/')
		{
			continue;
		}
		ext = Extension(arg, arglen);
		if(ext[0] == '.' && (ext[1] == 'C' || ext[1] == 'c'))
		{
			cc++;
			SetTarget(&gTarget, arg, arglen);
		}
		else if(ext[0] == '.' && ext[1] == 'y')
		{
			if(lemon)
			{
				lstrcpy(gAction, "Generating");
				SetTarget(&gTarget, arg, arglen);
				gTarget[arglen-1] = 'c';
			}
			yy++;
		}
		else if(ext[0] == '.' && ext[1] == 'r' && ext[2] == 'e')
		{
			yy++;
		}
		else if((ext[0] == '.' && ext[1] == 'n' && ext[2] == 'a' && ext[3] == 's') ||
				(ext[0] == '.' && ext[1] == 'a' && ext[2] == 's' && ext[3] == 'm') ||
				(ext[0] == '.' && ext[1] == 's'))
		{
			lstrcpy(gAction, "Assembling");
			SetTarget(&gTarget, arg, arglen);
		}
		else if(gArLibraryTarget == NULL && ext[0] == '.' && ext[1] == 'a')
		{
			SetTarget(&gArLibraryTarget, arg, arglen);
		}
	}
	if((gAr != NULL) && (gArLibraryTarget != NULL))
	{
		lstrcpy(gAction, "Creating library");
		if(gTarget != NULL) HeapFree(gHeap, 0, gTarget);
		gTarget = gArLibraryTarget;
		gArLibraryTarget = NULL;
	}
	else if(cc > 0)
	{
		lstrcpy(gAction, yy == 0 ? "Compiling" : "Generating");
	}

	/* Initialize security attributes for the pipe */
	security.nLength = sizeof security;
	security.lpSecurityDescriptor = NULL;
	security.bInheritHandle = TRUE;

	if(!CreatePipe(&pipeRd, &pipeWr, &security, 0))
	{
		perror("ccdv: pipe");
		ExitProcess(97);
	}

	if (!DuplicateHandle(GetCurrentProcess(), pipeRd,
			GetCurrentProcess(), &pipeRdDup , 0,
			FALSE,
			DUPLICATE_SAME_ACCESS))
	{
		perror("ccdv: DuplicateHandle");
	}
    CloseHandle(pipeRd);

	/* Initialize startup info for the child process */
	siStartInfo.cb = sizeof siStartInfo;
	siStartInfo.lpReserved = NULL;
	siStartInfo.lpDesktop = NULL;
	siStartInfo.lpTitle = NULL;
	siStartInfo.dwX = 0;
	siStartInfo.dwY = 0;
	siStartInfo.dwXSize = 0;
	siStartInfo.dwYSize = 0;
	siStartInfo.dwXCountChars = 0;
	siStartInfo.dwYCountChars = 0;
	siStartInfo.dwFillAttribute = 0;
	siStartInfo.dwFlags = STARTF_USESTDHANDLES;
	siStartInfo.wShowWindow = 0;
	siStartInfo.cbReserved2 = 0;
	siStartInfo.lpReserved2 = 0;
	siStartInfo.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
	siStartInfo.hStdOutput = pipeWr;
	siStartInfo.hStdError = pipeWr;

	StepCmdLine(&gArgsStr, &arg, &arglen);	// Skip "ccdv"
	while(*gArgsStr == ' ' || *gArgsStr == '\t' || *gArgsStr == '\n' || *gArgsStr == '\r')
		gArgsStr++;

	extlen = lstrlen(gArgsStr);
	gNBufAllocated = extlen + TEXT_BLOCK_SIZE;
	gBuf = (char *) HeapAlloc(gHeap, 0, gNBufAllocated);
	if(gBuf == NULL)
		goto panic;
	gNBufUsed = extlen + 1;
	lstrcpy(gBuf, gArgsStr);
	gBuf[extlen] = '\n';
	gBuf[extlen+1] = 0;

	if(!CreateProcessA(NULL, 
		gArgsStr,      /* command gArgsStr */
		NULL,          /* process security attributes */
		NULL,          /* primary thread security attributes */
		TRUE,          /* handles are inherited */
		0,             /* creation flags */
		NULL,          /* use parent's environment */
		NULL,          /* use parent's current directory */
		&siStartInfo,  /* STARTUPINFO pointer */
		&gCCP))        /* receives PROCESS_INFORMATION */
	{
		CloseHandle(pipeRdDup);
		CloseHandle(pipeWr);
		perror("ccdv: CreateProcess");
		gExitStatus = 98;
		goto panic;
	}
	CloseHandle(gCCP.hThread);

	if(GetConsoleScreenBufferInfo(gStdOut, &bufferInfo))
	{
		gColumns = bufferInfo.dwSize.X;
		if(Slurp(pipeRdDup, gStdOut) < 0)
			goto panic;
	}
	else
	{
		if(Slurp(pipeRdDup, NULL) < 0)
			goto panic;
	}
	DumpFormattedOutput();
	ExitProcess(gExitStatus);

  panic:
	gDumpCmdArgs = 1;	/* print cmd when there are errors */
	DumpFormattedOutput();
	while(ReadFile(pipeRdDup, emerg, sizeof emerg, &nread, NULL) && nread > 0)
		WriteFile(gStdErr, emerg, nread, &nread, NULL);
	Wait();
	ExitProcess(gExitStatus);
}	/* main */

/* eof ccdv.c */
