/*
 *  
 * 
 *  xclip.c - command line interface to X server selections 
 *  Copyright (C) 2001 Kim Saunders
 *  Copyright (C) 2007-2008 Peter Åstrand
 *
 *  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 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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#ifdef HAVE_ICONV
#include <errno.h>
#include <iconv.h>
#endif
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xmu/Atoms.h>
#include "xcdef.h"
#include "xcprint.h"
#include "xclib.h"

/* command line option table for XrmParseCommand() */
XrmOptionDescRec opt_tab[14];

/* Options that get set on the command line */
int sloop = 0;			/* number of loops */
char *sdisp = NULL;		/* X display to connect to */
Atom sseln = XA_PRIMARY;	/* X selection to work with */
Atom target = XA_STRING;

/* Flags for command line options */
static int fverb = OSILENT;	/* output level */
static int fdiri = T;		/* direction is in */
static int ffilt = F;		/* filter mode */
static int frmnl = F;		/* remove (single) newline character at the very end if present */

Display *dpy;			/* connection to X11 display */
XrmDatabase opt_db = NULL;	/* database for options */

char **fil_names;		/* names of files to read */
int fil_number = 0;		/* number of files to read */
int fil_current = 0;
FILE *fil_handle = NULL;

/* variables to hold Xrm database record and type */
XrmValue rec_val;
char *rec_typ;

int tempi = 0;

/* Use XrmParseCommand to parse command line options to option variable */
static void
doOptMain(int argc, char *argv[])
{
    /* Initialise resource manager and parse options into database */
    XrmInitialize();
    XrmParseCommand(&opt_db, opt_tab, sizeof(opt_tab) / sizeof(opt_tab[0]), PACKAGE_NAME, &argc,
		    argv);

    /* set output level */
    if (XrmGetResource(opt_db, "xclip.olevel", "Xclip.Olevel", &rec_typ, &rec_val)
	) {
	/* set verbose flag according to option */
	if (strcmp(rec_val.addr, "S") == 0)
	    fverb = OSILENT;
	if (strcmp(rec_val.addr, "Q") == 0)
	    fverb = OQUIET;
	if (strcmp(rec_val.addr, "V") == 0)
	    fverb = OVERBOSE;
    }

    /* set direction flag (in or out) */
    if (XrmGetResource(opt_db, "xclip.direction", "Xclip.Direction", &rec_typ, &rec_val)
	) {
	if (strcmp(rec_val.addr, "I") == 0)
	    fdiri = T;
	if (strcmp(rec_val.addr, "O") == 0)
	    fdiri = F;
    }

    /* set filter mode */
    if (XrmGetResource(opt_db, "xclip.filter", "Xclip.Filter", &rec_typ, &rec_val)
	) {
	/* filter mode only allowed in silent mode */
	if (fverb == OSILENT)
	    ffilt = T;
    }

    /* set "remove last newline character if present" mode */
    if (XrmGetResource(opt_db, "xclip.rmlastnl", "Xclip.RmLastNl", &rec_typ, &rec_val)
	) {
	frmnl = T;
    }

    /* check for -help and -version */
    if (XrmGetResource(opt_db, "xclip.print", "Xclip.Print", &rec_typ, &rec_val)
	) {
	if (strcmp(rec_val.addr, "H") == 0)
	    prhelp(argv[0]);
	if (strcmp(rec_val.addr, "V") == 0)
	    prversion();
    }

    /* check for -display */
    if (XrmGetResource(opt_db, "xclip.display", "Xclip.Display", &rec_typ, &rec_val)
	) {
	sdisp = rec_val.addr;
	if (fverb == OVERBOSE)	/* print in verbose mode only */
	    fprintf(stderr, "Display: %s\n", sdisp);
    }

    /* check for -loops */
    if (XrmGetResource(opt_db, "xclip.loops", "Xclip.Loops", &rec_typ, &rec_val)
	) {
	sloop = atoi(rec_val.addr);
	if (fverb == OVERBOSE)	/* print in verbose mode only */
	    fprintf(stderr, "Loops: %i\n", sloop);
    }

    /* Read remaining options (filenames) */
    while ((fil_number + 1) < argc) {
	if (fil_number > 0) {
	    fil_names = xcrealloc(fil_names, (fil_number + 1) * sizeof(char *)
		);
	}
	else {
	    fil_names = xcmalloc(sizeof(char *));
	}
	fil_names[fil_number] = argv[fil_number + 1];
	fil_number++;
    }
}

/* process selection command line option */
static void
doOptSel(void)
{
    /* set selection to work with */
    if (XrmGetResource(opt_db, "xclip.selection", "Xclip.Selection", &rec_typ, &rec_val)
	) {
	switch (tolower(rec_val.addr[0])) {
	case 'p':
	    sseln = XA_PRIMARY;
	    break;
	case 's':
	    sseln = XA_SECONDARY;
	    break;
	case 'c':
	    sseln = XA_CLIPBOARD(dpy);
	    break;
	case 'b':
	    sseln = XA_STRING;
	    break;
	}

	if (fverb == OVERBOSE) {
	    fprintf(stderr, "Using selection: ");

	    if (sseln == XA_PRIMARY)
		fprintf(stderr, "XA_PRIMARY");
	    if (sseln == XA_SECONDARY)
		fprintf(stderr, "XA_SECONDARY");
	    if (sseln == XA_CLIPBOARD(dpy))
		fprintf(stderr, "XA_CLIPBOARD");
	    if (sseln == XA_STRING)
		fprintf(stderr, "XA_STRING");

	    fprintf(stderr, "\n");
	}
    }
}

/* process noutf8 and target command line options */
static void
doOptTarget(void)
{
    /* check for -noutf8 */
    if (XrmGetResource(opt_db, "xclip.noutf8", "Xclip.noutf8", &rec_typ, &rec_val)
	) {
	if (fverb == OVERBOSE)	/* print in verbose mode only */
	    fprintf(stderr, "Using old UNICODE instead of UTF8.\n");
    }
    else if (XrmGetResource(opt_db, "xclip.target", "Xclip.Target", &rec_typ, &rec_val)
	) {
	target = XInternAtom(dpy, rec_val.addr, False);
	if (fverb == OVERBOSE)	/* print in verbose mode only */
	    fprintf(stderr, "Using %s.\n", rec_val.addr);
    }
    else {
	target = XA_UTF8_STRING(dpy);
	if (fverb == OVERBOSE)	/* print in verbose mode only */
	    fprintf(stderr, "Using UTF8_STRING.\n");
    }
}

static int
doIn(Window win, const char *progname)
{
    unsigned char *sel_buf;	/* buffer for selection data */
    unsigned long sel_len = 0;	/* length of sel_buf */
    unsigned long sel_all = 0;	/* allocated size of sel_buf */
    XEvent evt;			/* X Event Structures */
    int dloop = 0;		/* done loops counter */

    /* in mode */
    sel_all = 16;		/* Reasonable ballpark figure */
    sel_buf = xcmalloc(sel_all * sizeof(char));

    /* Put chars into inc from stdin or files until we hit EOF */
    do {
	if (fil_number == 0) {
	    /* read from stdin if no files specified */
	    fil_handle = stdin;
	}
	else {
	    if ((fil_handle = fopen(fil_names[fil_current], "r")) == NULL) {
		errperror(3, progname, ": ", fil_names[fil_current]
		    );
		return EXIT_FAILURE;
	    }
	    else {
		/* file opened successfully. Print
		 * message (verbose mode only).
		 */
		if (fverb == OVERBOSE)
		    fprintf(stderr, "Reading %s...\n", fil_names[fil_current]
			);
	    }
	}

	fil_current++;
	while (!feof(fil_handle)) {
	    /* If sel_buf is full (used elems =
	     * allocated elems)
	     */
	    if (sel_len == sel_all) {
		/* double the number of
		 * allocated elements
		 */
		sel_all *= 2;
		sel_buf = (unsigned char *) xcrealloc(sel_buf, sel_all * sizeof(char)
		    );
	    }
	    sel_len += fread(sel_buf + sel_len, sizeof(char), sel_all - sel_len, fil_handle);
	}
    } while (fil_current < fil_number);

    /* remove the last newline character if necessary */
    if (frmnl && sel_len && sel_buf[sel_len - 1] == '\n') {
	sel_len--;
    }

    /* if there are no files being read from (i.e., input
     * is from stdin not files, and we are in filter mode,
     * spit all the input back out to stdout
     */
    if ((fil_number == 0) && ffilt) {
	fwrite(sel_buf, sizeof(char), sel_len, stdout);
	fclose(stdout);
    }

    /* Handle cut buffer if needed */
    if (sseln == XA_STRING) {
	XStoreBuffer(dpy, (char *) sel_buf, (int) sel_len, 0);
	return EXIT_SUCCESS;
    }

    /* take control of the selection so that we receive
     * SelectionRequest events from other windows
     */
    /* FIXME: Should not use CurrentTime, according to ICCCM section 2.1 */
    XSetSelectionOwner(dpy, sseln, win, CurrentTime);

    /* fork into the background, exit parent process if we
     * are in silent mode
     */
    if (fverb == OSILENT) {
	pid_t pid;

	pid = fork();
	/* exit the parent process; */
	if (pid)
	    exit(EXIT_SUCCESS);
    }

    /* print a message saying what we're waiting for */
    if (fverb > OSILENT) {
	if (sloop == 1)
	    fprintf(stderr, "Waiting for one selection request.\n");

	if (sloop < 1)
	    fprintf(stderr, "Waiting for selection requests, Control-C to quit\n");

	if (sloop > 1)
	    fprintf(stderr, "Waiting for %i selection requests, Control-C to quit\n", sloop);
    }

    /* Avoid making the current directory in use, in case it will need to be umounted */
    if (chdir("/") == -1) {
	errperror(3, progname, ": ", "chdir to \"/\"");
	return EXIT_FAILURE;
    }

    /* loop and wait for the expected number of
     * SelectionRequest events
     */
    while (dloop < sloop || sloop < 1) {
	/* print messages about what we're waiting for
	 * if not in silent mode
	 */
	if (fverb > OSILENT) {
	    if (sloop > 1)
		fprintf(stderr, "  Waiting for selection request %i of %i.\n", dloop + 1, sloop);

	    if (sloop == 1)
		fprintf(stderr, "  Waiting for a selection request.\n");

	    if (sloop < 1)
		fprintf(stderr, "  Waiting for selection request number %i\n", dloop + 1);
	}

	/* wait for a SelectionRequest event */
	while (1) {
	    static unsigned int clear = 0;
	    static unsigned int context = XCLIB_XCIN_NONE;
	    static unsigned long sel_pos = 0;
	    static Window cwin;
	    static Atom pty;
	    int finished;

	    XNextEvent(dpy, &evt);

	    finished = xcin(dpy, &cwin, evt, &pty, target, sel_buf, sel_len, &sel_pos, &context);

	    if (evt.type == SelectionClear)
		clear = 1;

	    if ((context == XCLIB_XCIN_NONE) && clear)
		return EXIT_SUCCESS;

	    if (finished)
		break;
	}

	dloop++;		/* increment loop counter */
    }

    return EXIT_SUCCESS;
}

static void
printSelBuf(FILE * fout, Atom sel_type, unsigned char *sel_buf, size_t sel_len)
{
#ifdef HAVE_ICONV
    Atom html = XInternAtom(dpy, "text/html", True);
#endif

    if (fverb == OVERBOSE) {	/* print in verbose mode only */
	char *atom_name = XGetAtomName(dpy, sel_type);
	fprintf(stderr, "Type is %s.\n", atom_name);
	XFree(atom_name);
    }

    if (sel_type == XA_INTEGER) {
	/* if the buffer contains integers, print them */
	long *long_buf = (long *) sel_buf;
	size_t long_len = sel_len / sizeof(long);
	while (long_len--)
	    fprintf(fout, "%ld\n", *long_buf++);
	return;
    }

    if (sel_type == XA_ATOM) {
	/* if the buffer contains atoms, print their names */
	Atom *atom_buf = (Atom *) sel_buf;
	size_t atom_len = sel_len / sizeof(Atom);
	while (atom_len--) {
	    char *atom_name = XGetAtomName(dpy, *atom_buf++);
	    fprintf(fout, "%s\n", atom_name);
	    XFree(atom_name);
	}
	return;
    }

#ifdef HAVE_ICONV
    if (html != None && sel_type == html) {
	/* if the buffer contains UCS-2 (UTF-16), convert to
	 * UTF-8.  Mozilla-based browsers do this for the
	 * text/html target.
	 */
	iconv_t cd;
	char *sel_charset = NULL;
	if (sel_buf[0] == 0xFF && sel_buf[1] == 0xFE)
	    sel_charset = "UTF-16LE";
	else if (sel_buf[0] == 0xFE && sel_buf[1] == 0xFF)
	    sel_charset = "UTF-16BE";

	if (sel_charset != NULL && (cd = iconv_open("UTF-8", sel_charset)) != (iconv_t) - 1) {
	    char *out_buf_start = malloc(sel_len), *in_buf = (char *) sel_buf + 2,
		*out_buf = out_buf_start;
	    size_t in_bytesleft = sel_len - 2, out_bytesleft = sel_len;

	    while (iconv(cd, &in_buf, &in_bytesleft, &out_buf, &out_bytesleft) == -1
		   && errno == E2BIG) {
		fwrite(out_buf_start, sizeof(char), sel_len - out_bytesleft, fout);
		out_buf = out_buf_start;
		out_bytesleft = sel_len;
	    }
	    if (out_buf != out_buf_start)
		fwrite(out_buf_start, sizeof(char), sel_len - out_bytesleft, fout);

	    free(out_buf_start);
	    iconv_close(cd);
	    return;
	}
    }
#endif

    /* otherwise, print the raw buffer out */
    fwrite(sel_buf, sizeof(char), sel_len, fout);
}

static int
doOut(Window win)
{
    Atom sel_type = None;
    unsigned char *sel_buf;	/* buffer for selection data */
    unsigned long sel_len = 0;	/* length of sel_buf */
    XEvent evt;			/* X Event Structures */
    unsigned int context = XCLIB_XCOUT_NONE;

    if (sseln == XA_STRING)
	sel_buf = (unsigned char *) XFetchBuffer(dpy, (int *) &sel_len, 0);
    else {
	while (1) {
	    /* only get an event if xcout() is doing something */
	    if (context != XCLIB_XCOUT_NONE)
		XNextEvent(dpy, &evt);

	    /* fetch the selection, or part of it */
	    xcout(dpy, win, evt, sseln, target, &sel_type, &sel_buf, &sel_len, &context);

	    if (context == XCLIB_XCOUT_BAD_TARGET) {
		if (target == XA_UTF8_STRING(dpy)) {
		    /* fallback is needed. set XA_STRING to target and restart the loop. */
		    context = XCLIB_XCOUT_NONE;
		    target = XA_STRING;
		    continue;
		}
		else {
		    /* no fallback available, exit with failure */
		    char *atom_name = XGetAtomName(dpy, target);
		    fprintf(stderr, "Error: target %s not available\n", atom_name);
		    XFree(atom_name);
		    return EXIT_FAILURE;
		}
	    }

	    /* only continue if xcout() is doing something */
	    if (context == XCLIB_XCOUT_NONE)
		break;
	}
    }

    /* remove the last newline character if necessary */
    if (frmnl && sel_len && sel_buf[sel_len - 1] == '\n') {
	sel_len--;
    }

    if (sel_len) {
	/* only print the buffer out, and free it, if it's not
	 * empty
	 */
	printSelBuf(stdout, sel_type, sel_buf, sel_len);
	if (sseln == XA_STRING)
	    XFree(sel_buf);
	else
	    free(sel_buf);
    }

    return EXIT_SUCCESS;
}

int
main(int argc, char *argv[])
{
    /* Declare variables */
    Window win;			/* Window */
    int exit_code;

    /* set up option table. I can't figure out a better way than this to
     * do it while sticking to pure ANSI C. The option and specifier
     * members have a type of volatile char *, so they need to be allocated
     * by strdup or malloc, you can't set them to a string constant at
     * declare time, this is not pure ANSI C apparently, although it does
     * work with gcc
     */

    /* loop option entry */
    opt_tab[0].option = xcstrdup("-loops");
    opt_tab[0].specifier = xcstrdup(".loops");
    opt_tab[0].argKind = XrmoptionSepArg;
    opt_tab[0].value = (XPointer) NULL;

    /* display option entry */
    opt_tab[1].option = xcstrdup("-display");
    opt_tab[1].specifier = xcstrdup(".display");
    opt_tab[1].argKind = XrmoptionSepArg;
    opt_tab[1].value = (XPointer) NULL;

    /* selection option entry */
    opt_tab[2].option = xcstrdup("-selection");
    opt_tab[2].specifier = xcstrdup(".selection");
    opt_tab[2].argKind = XrmoptionSepArg;
    opt_tab[2].value = (XPointer) NULL;

    /* filter option entry */
    opt_tab[3].option = xcstrdup("-filter");
    opt_tab[3].specifier = xcstrdup(".filter");
    opt_tab[3].argKind = XrmoptionNoArg;
    opt_tab[3].value = (XPointer) xcstrdup(ST);

    /* in option entry */
    opt_tab[4].option = xcstrdup("-in");
    opt_tab[4].specifier = xcstrdup(".direction");
    opt_tab[4].argKind = XrmoptionNoArg;
    opt_tab[4].value = (XPointer) xcstrdup("I");

    /* out option entry */
    opt_tab[5].option = xcstrdup("-out");
    opt_tab[5].specifier = xcstrdup(".direction");
    opt_tab[5].argKind = XrmoptionNoArg;
    opt_tab[5].value = (XPointer) xcstrdup("O");

    /* version option entry */
    opt_tab[6].option = xcstrdup("-version");
    opt_tab[6].specifier = xcstrdup(".print");
    opt_tab[6].argKind = XrmoptionNoArg;
    opt_tab[6].value = (XPointer) xcstrdup("V");

    /* help option entry */
    opt_tab[7].option = xcstrdup("-help");
    opt_tab[7].specifier = xcstrdup(".print");
    opt_tab[7].argKind = XrmoptionNoArg;
    opt_tab[7].value = (XPointer) xcstrdup("H");

    /* silent option entry */
    opt_tab[8].option = xcstrdup("-silent");
    opt_tab[8].specifier = xcstrdup(".olevel");
    opt_tab[8].argKind = XrmoptionNoArg;
    opt_tab[8].value = (XPointer) xcstrdup("S");

    /* quiet option entry */
    opt_tab[9].option = xcstrdup("-quiet");
    opt_tab[9].specifier = xcstrdup(".olevel");
    opt_tab[9].argKind = XrmoptionNoArg;
    opt_tab[9].value = (XPointer) xcstrdup("Q");

    /* verbose option entry */
    opt_tab[10].option = xcstrdup("-verbose");
    opt_tab[10].specifier = xcstrdup(".olevel");
    opt_tab[10].argKind = XrmoptionNoArg;
    opt_tab[10].value = (XPointer) xcstrdup("V");

    /* utf8 option entry */
    opt_tab[11].option = xcstrdup("-noutf8");
    opt_tab[11].specifier = xcstrdup(".noutf8");
    opt_tab[11].argKind = XrmoptionNoArg;
    opt_tab[11].value = (XPointer) xcstrdup("N");

    /* target option entry */
    opt_tab[12].option = xcstrdup("-target");
    opt_tab[12].specifier = xcstrdup(".target");
    opt_tab[12].argKind = XrmoptionSepArg;
    opt_tab[12].value = (XPointer) NULL;

    /* "remove newline if it is the last character" entry */
    opt_tab[13].option = xcstrdup("-rmlastnl");
    opt_tab[13].specifier = xcstrdup(".rmlastnl");
    opt_tab[13].argKind = XrmoptionNoArg;
    opt_tab[13].value = (XPointer) xcstrdup(ST);

    /* parse command line options */
    doOptMain(argc, argv);

    /* Connect to the X server. */
    if ((dpy = XOpenDisplay(sdisp))) {
	/* successful */
	if (fverb == OVERBOSE)
	    fprintf(stderr, "Connected to X server.\n");
    }
    else {
	/* couldn't connect to X server. Print error and exit */
	errxdisplay(sdisp);
    }

    /* parse selection command line option */
    doOptSel();

    /* parse noutf8 and target command line options */
    doOptTarget();

    /* Create a window to trap events */
    win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 1, 1, 0, 0, 0);

    /* get events about property changes */
    XSelectInput(dpy, win, PropertyChangeMask);

    if (fdiri)
	exit_code = doIn(win, argv[0]);
    else
	exit_code = doOut(win);

    /* Disconnect from the X server */
    XCloseDisplay(dpy);

    /* exit */
    return exit_code;
}
