/* Resizer was written by and is Copyright (C) Richard A. Rost  July 2,2021.
 * 
 * This program launches a program and then resizes and moves its
 * window based on the arguments passed in.
 * 
 * My Copyright terms are simple. You may copy, modify, and use this
 * program as you see fit provided you meet the following terms:
 * 
 * 1. You leave my name as the author and do not take credit for my work.
 * 2. While I hope this program will be useful it is offered WITHOUT ANY
 *    WARRANTY and you agree not to hold me liable for any damages.
 * 3. You leave this header in it's entirety at the top of all source
 *    files for this program and any forks. You may append notes, list
 *    changes, and add the authors of any changes to the end of this header.
 * 4. You do not collect any fee for this program as is or modified.
 * 5. You do not collect any fee for any changes you make to this program.
 * 6. You do not include or package it with any software for which any fee
 *    is collected.
 * 7. You do not include it on media (CD, DVD, etc.) which is sold or any
 *    fee is collected for the cost of the media, shipping, handling, etc.
 * 8. Items 4, 5, 6, and 7 apply to the source code as well as the executable.
 * 9. If you modify this program, you must make the full source code with a
 *    functioning compile script available in one of the following ways.
 *   A. Packaged with the executable when you distribute it.
 *   B. As a separate package available from where you distribute the
 *      executable. If distributed on a CD a web link to the source package
 *      is acceptable.
 * 
 * 
 * This program was written using the Geany 1.23 fast and lightweight IDE.
 * Tabs are set to 4.
 * -----------------------End of original header----------------------------
 */


#include <stdlib.h>
#include <stdio.h>		// printf
#include <unistd.h>		// sleep usleep getopt close
#include <errno.h>
#include <string.h>		// memset strerror strstr
#include <ctype.h>		// isdigit

#include <X11/Xlib.h>


/******************************************************************/
//	Defines

#define COPYRIGHT "Copyright Richard A. Rost July 2,2021"
#define PROGRAM "Resizer"
#define VERSION "version 0.20"

// Flags for which options were selected.
#define OptX		1 << 0		// Override X position.
#define OptY		1 << 1		// Override Y position.
#define OptWidth	1 << 2		// Override Width.
#define OptHeight	1 << 3		// Override Height.
#define OptCommand	1 << 4		// Command to execute.
#define OptDelay	1 << 5		// Delay moving windo after program start.
#define OptBare		1 << 6		// No window decorations are present.
#define OptFull		1 << 7		// Make app fullscreen.
#define OptDisplay	1 << 8		// Display ID and geometry of window under mouse.
/******************************************************************/


/******************************************************************/
//	Globals

struct WindowParams
{
	int x;					//
	int y;					//
	int w;					//
	int h;					//
	int map_state;			// (0, 1, 2) IsUnmapped, IsUnviewable, IsViewable
};


struct ScreenParams
{
	Window rootwindow;		// The desktop window ID
	int rootwidth;			// The desktop width in pixels
	int rootheight;			// The desktop height in pixels
	int screen;
	Display *display;		//
};

static struct ScreenParams		crt;

// Used to override errno when calling Bail().
//static int NoSuchDevice=19;
static int InvalidArgument=22;

/******************************************************************/


/******************************************************************/
//	Prototypes

static void			Bail(int linenumber, int *ErrNum);
static int			CompareXIDs(const void * a, const void * b);
static int			FindSortedChildren(Window window, Window **children, int caller);
static void			FreeX(void **Pointer);
static void 		GetScreenParams(char *DisplayName);
static void			GetWindowParams(Window window, struct WindowParams *params);
static int			IsNumber(char *string);
static int			String2Long(char *string, int caller);
static void			Usage(void);
static void			Zfree(void **Pointer);
static void			Zmalloc(void **Pointer, int size, int caller);

/******************************************************************/


/******************************************************************/
/* Debugging aid to trace program execution.
 * Prints out line number and function name anywhere TRACE is placed
 * if TRACE_ENABLE is defined. Otherwise, TRACE is ignored.
 * Uncomment the TRACE_ENABLE line to enable the TRACE function.
 */
//#define TRACE_ENABLE

#ifdef TRACE_ENABLE
#define TRACE	printf("%4d %s\n", __LINE__, __FUNCTION__);
#else
#define TRACE	
#endif
/******************************************************************/


/******************************************************************/
/* Print an error message and the line number of the source file
 * that triggered it. *ErrNum can optionally be used to force a
 * specific error, or pass in NULL to leave its default value.
 */
static void Bail(int linenumber, int *ErrNum)
{
	if(ErrNum)
		errno=*ErrNum;
	printf("Error line: %d\n%s\n", linenumber, strerror(errno));
	exit(1);
}
/******************************************************************/

/******************************************************************/
/* Passed to qsort when sortin window IDs.
 */
static int CompareXIDs(const void * a, const void * b)
{
	return(*(int*)a - *(int*)b);
}
/******************************************************************/

/******************************************************************/
/* This finds the children of  window  and saves the list in  children.
 * The list is then sorted so it can easily be compared to other lists.
 * It returns the number of children found in  count.
 */
static int FindSortedChildren(Window window, Window **children, int caller)
{
	unsigned int count;

	Window root;			// 
	Window parent;			// 

	if(XQueryTree(crt.display, window, &root, &parent, children, &count) == 0)
		Bail(caller, NULL);

	if(*children)	// XQueryTree returns NULL if no children found.
	{
		qsort(*children, count, sizeof(Window), CompareXIDs);
	}
	else			// Set count to zero if no children found.
	{
		count=0;
	}

	return(count);
}
/******************************************************************/

/******************************************************************/
/* Used to free X11 pointers.
 * Checks to see the pointer is not NULL, frees memeory, and sets
 * pointer equal to NULL
 */
static void FreeX(void **Pointer)
{
	if(*Pointer == NULL)
		return;
	XFree(*Pointer);
	*Pointer=NULL;
	return;
}
/******************************************************************/

/******************************************************************/
/* Common parameters required by X11 functions */
static void GetScreenParams(char *DisplayName)
{
	if((crt.display=XOpenDisplay(DisplayName)))
	{
		crt.screen=DefaultScreen(crt.display);
		crt.rootwindow=RootWindow(crt.display,crt.screen);		// XID of the root window
		crt.rootwidth=DisplayWidth(crt.display,crt.screen);		// Screen width in pixels
		crt.rootheight=DisplayHeight(crt.display,crt.screen);	// Screen height in pixels
		return;
	}
	Bail(__LINE__, NULL);
}
/******************************************************************/

/******************************************************************/
/* This returns the position, size, and map state of a window.
 */
static void GetWindowParams(Window window, struct WindowParams *params)
{
	XWindowAttributes wa;

	XGetWindowAttributes(crt.display, window, &wa);

	params->x=wa.x;
	params->y=wa.y;
	params->w=wa.width;
	params->h=wa.height;
	params->map_state=wa.map_state;

	return;
}
/******************************************************************/

/******************************************************************/
/* Tests if a string forms a valid whole number.
 * A leading plus or minus sign is legal. Decimal points are not.
 * Returns 1 if string is valid, and 0 if not.
 */
static int IsNumber(char *string)
{
	char *num=string;

	// A leading sign character is legal, so skip it if present.
	if((*num == '+') || (*num == '-'))
		num++;

	while(*num)
	{
		if(isdigit(*num) == 0)
			return(0);
		num++;
	}

	return(1);	// Valid number.
}
/******************************************************************/

/******************************************************************/
/* Takes a whole number in the form of a string and returns an int.
 * A leading plus or minus sign is legal. Decimal points are not.
 */
static int String2Long(char *string, int caller)
{
	int i;

	if(IsNumber(string) == 0)
	{
		printf("%s is not a valid number.\n", string);
		Bail(caller, &InvalidArgument);
	}

	errno=0;
	i=strtol(string, NULL, 10);
	if(errno)
		Bail(caller, NULL);

	return(i);
}
/******************************************************************/

/******************************************************************/
/* Help message.
 */
static void Usage(void)
{

	printf("\n%s %s %s %s\n%s\n"
			"\n%s starts a program and then resizes and moves its\n"
			"window based on the options passed in.\n\n"
			"Usage:\n\n  %s -p \"program args\" [-x N] [-y N] [-w N] [-h N] [-f] [-d N] [-b] [-D]\n\n"
			"\t-p program plus arguments in quotes\n"
			"\t-x X position  -y Y position  -w Width  -h Height\n"
			"\t-f Fullscreen\n"
			"\t-d Delay N seconds, then move/resize window\n"
			"\t-b No window manager present\n"
			"\t-D Display ID and geometry of window under mouse, ^C exits\n"
			"\n"
			"\tUse -d when a program:\n"
			"\t\tPuts up a splash dialog before its window\n"
			"\t\tDisplays its window and later moves it\n"
			"\n\tYour screen size is %d x %d\n\n"
			, PROGRAM, VERSION, __DATE__, __TIME__, COPYRIGHT
			, PROGRAM, PROGRAM, crt.rootwidth, crt.rootheight);
	exit(1);
}
/******************************************************************/

/******************************************************************/
/* Checks to see the pointer is not NULL, frees memeory, and sets
 * pointer equal to NULL
 */
static void Zfree(void **Pointer)
{
	if(*Pointer == NULL)
		return;
	free(*Pointer);
	*Pointer=NULL;
	return;
}
/******************************************************************/

/******************************************************************/
/* Mallocs a block of memory and zeros it out. If the malloc request
 * fails, the program aborts and prints the line number the Zmalloc
 * call originated from. Otherwise a pointer to valid memory is
 * returned.
 */
static void Zmalloc(void **Pointer, int size, int caller)
{
	if(*Pointer != NULL) Bail(caller, NULL);

	if((*Pointer=malloc(size)) == NULL) Bail(caller, NULL);

	memset(*Pointer, 0, size);
	return;
}
/******************************************************************/



/******************************************************************/
int main(int argc, char *argv[])
{

	Window target;						// Window to be moved/resized.
	Window previous=0;					// Window frame if a window manager is present.
	Window *childwindows=NULL;			// 
	Window *childwindows2=NULL;			// 

	struct WindowParams targetparams={0};	// Window location and map state.
	struct WindowParams previousparams={0};	// Window location and map state.

	char *command=NULL;

	int childcount;
	int childcount2=0;
	int opt;
	int options=0;						// Bitfield record of which options were selected.
	int i, j;

	int X, Y;							// New X and/or Y position if specified.
	int Width, Height;					// New Width and/or Height if specified.
	int Delay=0;						// Optional delay for programs that misbehave.

	// Retrieve common parameters required by X11 functions.
	// This gets placed up here so Usage() can print display size
	// if called from  getopt  loop below.
	GetScreenParams(NULL);

	while ((opt=getopt(argc, argv, "-:p:x:y:w:h:d:bfD")) != -1) 
	{
		switch(opt)
		{
			case 'p':	// Program. Needs quotes if passing arguments.
			i=strlen(optarg) + 8;
			Zmalloc((void **)&command, sizeof(char) * i, __LINE__);
			strcpy(command, optarg);
			strcat(command, " &");
			options|=OptCommand;
			break;

			case 'x':	// X position
			X=String2Long(optarg, __LINE__);
			options|=OptX;
			break;

			case 'y':	// Y position
			Y=String2Long(optarg, __LINE__);
			options|=OptY;
			break;

			case 'w':	// Width
			Width=String2Long(optarg, __LINE__);
			if(Width <= 0) Bail(__LINE__, &InvalidArgument);
			options|=OptWidth;
			break;

			case 'h':	// Height
			Height=String2Long(optarg, __LINE__);
			if(Height <= 0) Bail(__LINE__, &InvalidArgument);
			options|=OptHeight;
			break;

			case 'd':	// Delay
			Delay=String2Long(optarg, __LINE__);
			if(Delay <= 0) Bail(__LINE__, &InvalidArgument);
			options|=OptDelay;
			break;

			case 'b':	// No window decorations are present.
			options|=OptBare;
			break;

			case 'f':	// Make app fullscreen.
			options|=OptFull;
			break;

			case 'D':	// Display ID and geometry of window under mouse.
			options|=OptDisplay;
			break;

			case '?':	// printf("Unknown option: %c\n", optopt);
			case ':':	// printf("Missing arg for %c\n", optopt);
			case 1:		// printf("Non-option arg: %s\n", optarg);
			Usage();	// Errors return help message.
			break;
		}
	}

	// This overrides all other options so it comes first.
	if(options & OptDisplay)
	{
		while(1)
		{
			usleep(50000);
			XQueryPointer(crt.display, crt.rootwindow, &previous, &target, &X, &Y, &j, &j, (unsigned int *)&j);
			// Always print mouse position.
			printf(" Mouse:X=%-6dY=%-6d", X, Y);
			// Only update ID and geometry if mouse is over a child of crt.rootwindow.
			if(target)
			{
				GetWindowParams(target, &targetparams);
				printf("Window: ID=%08lx  X=%-6dY=%-6dW=%-6dH=%-6d", target, targetparams.x, targetparams.y, targetparams.w, targetparams.h);
			}
			//  \e[K  deletes from cursor to end o line. Removes ID and geometry if mouse is not over a child of crt.rootwindow..
			//  \e[1A  moves the cursor up one line so we don't scroll.
			//   \n   newline flushes buffer to force screen update.
			printf("\e[K\e[1A\n");
		}
	}

	// Display help if run with no options or missing command option.
	// Remaining reasons for calling usage handled by  getopt  loop above.
	if(! command)
		Usage();

	// Make X run synchronously so we don't need to deal with race conditions.
	XSynchronize(crt.display, True);

	// Create a list of existing top level windows.
	childcount=FindSortedChildren(crt.rootwindow, &childwindows, __LINE__);

	// Launch the command.
	system(command);

	// Optional delay to allow some programs to settle before we find
	// their window and alter it.
	sleep(Delay);

	// Wait up to 10 seconds for a new window to appear.
	j=1000;
	while(j)
	{
		usleep(10000);
		// Create a second list of top level windows. If this list is longer, break out of the loop.
		if((childcount2=FindSortedChildren(crt.rootwindow, &childwindows2, __LINE__)) > childcount)
			break;
		// If memory was allocated by XQueryTree then free it.
		FreeX((void **)&childwindows2);
		j--;
	}

	// Abort. No new windows detected.
	if(childcount2 <= childcount) Bail(__LINE__, NULL);


	// Find the ID of the new window.
	j=0;
	// This loops until it detects  target == previous  10 times in a row.
	while(j < 10)
	{
		// Compare existing list to new list.
		for(i=0; i < childcount; i++)
		{
			if(childwindows[i] != childwindows2[i])
			{
				break;
			}
		}

		// Make sure the window has an ID assigned and isn't zero.
		if(childwindows2[i])
		{
			target=childwindows2[i];
			GetWindowParams(target, &targetparams);
			if((target == previous) && (targetparams.map_state == IsViewable))
			{
				j++;
			}
			else
			{
				previous=target;
				j=0;
			}
		}
		usleep(10000);
		FreeX((void **)&childwindows2);
		childcount2=FindSortedChildren(crt.rootwindow, &childwindows2, __LINE__);
	}

	// At this point, previous and target point to the same window.
	// If no window manager is involved, we have our window for
	// moving and/or resizing.

	// If a size or position option is not specified, assign the default value.
	if(!(options & OptX))
		X=targetparams.x;
	if(!(options & OptY))
		Y=targetparams.y;
	if(!(options & OptWidth))
		Width=targetparams.w;
	if(!(options & OptHeight))
		Height=targetparams.h;

	// If full screen is chosen, it overrides any size/position options selected.
	if(options & OptFull)
	{
		X=0;
		Y=0;
		Width=crt.rootwidth;
		Height=crt.rootheight;
	}

	// If a window manager is involved, we need to get the apps window
	// and adjust its geometry to account for window frame dimensions.
	if(!(options & OptBare))
	{
		// Since a window manager is involved, we only have the window
		// frame, so we have to fetch the frames child for resizing.
		FreeX((void **)&childwindows2);
		childcount2=FindSortedChildren(previous, &childwindows2, __LINE__);

		target=childwindows2[0];
		previousparams=targetparams;
		GetWindowParams(target, &targetparams);

		// We have to resize/move the applications top level window, not
		// its window frame parent. So we have to compensate for the
		// frames borders. The window manager adjusts the window frame
		// size in response to the application window changing size.

		// targetparams x and y are zero base offsets relative to the
		// to the parent window, so we can add it directly.
		X=X + targetparams.x;
		Y=Y + targetparams.y;

		// Subtracting the width of targetparams from previousparams
		// gives the total width occupied by the borders. That's how
		// much we need to reduce the final width of the apps window
		// so the parent frame is the correct width.
		Width=Width - (previousparams.w - targetparams.w);
		Height=Height - (previousparams.h - targetparams.h);
	}

	// Apply the position and size variables.
	XMoveResizeWindow(crt.display, target, X, Y, Width, Height);

	// Switch X back to asynchronous mode.
	XSynchronize(crt.display, False);

	// Clean up before we leave.
	FreeX((void **)&childwindows);
	FreeX((void **)&childwindows2);
	XCloseDisplay(crt.display);
	Zfree((void **)&command);

	return(0);

}

