/* peksystray
 * Copyright (c) 2003, 2005 Mohammed Sameer.
 * Copyright (c) 2005, 2007 Eric Piel.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /* HAVE_CONFIG_H */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
/* Sun OpenWindows */
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#define SYSTEM_TRAY_REQUEST_DOCK    0
#define SYSTEM_TRAY_BEGIN_MESSAGE   1
#define SYSTEM_TRAY_CANCEL_MESSAGE  2

#define _NET_SYSTEM_TRAY_ORIENTATION_HORZ 0
#define _NET_SYSTEM_TRAY_ORIENTATION_VERT 1

/*
 * XEMBED messages
 */
#define XEMBED_EMBEDDED_NOTIFY		0
#define XEMBED_WINDOW_ACTIVATE		1
#define XEMBED_WINDOW_DEACTIVATE	2
#define XEMBED_REQUEST_FOCUS		3
#define XEMBED_FOCUS_IN			4
#define XEMBED_FOCUS_OUT		5
#define XEMBED_FOCUS_NEXT		6
#define XEMBED_FOCUS_PREV		7
/* 8-9 were used for XEMBED_GRAB_KEY/XEMBED_UNGRAB_KEY */
#define XEMBED_MODALITY_ON		10
#define XEMBED_MODALITY_OFF		11
#define XEMBED_REGISTER_ACCELERATOR	12
#define XEMBED_UNREGISTER_ACCELERATOR	13
#define XEMBED_ACTIVATE_ACCELERATOR	14


/* Flags for _XEMBED_INFO */
#define XEMBED_MAPPED                   (1 << 0)

//#define DEBUG
#ifdef DEBUG
#define dbg_printf(fmt,arg...) fprintf(stderr,fmt,##arg)
#else
#define dbg_printf(fmt,arg...) do { } while (0)
#endif

Atom selection_atom, opcode_atom, data_atom;

struct dock {
	Window main;		/* window representing the dock */
	Window container;	/* window containing the slots (has the same size as main)*/	
	Window *slots;		/* an array of the slots = displayed icons */
};

struct dock *docks;	/* the docks which are displayed */
// XXX should it be a chained list?
int num_docks = 0;		/* number of docks created */
int slots_per_dock;		/* number of slots that fit in one dock (depends on their size) */

int no_multiple = 0;
int icon_size = 16;
int border_width = 0;
int dock_width = 64; // with WindowMaker it might be possible to retrieve it dynamically?
int dock_height = -1; // -1 means it is equal to icon_size

Display *display = NULL;
char *display_name = NULL;
Window Root;

void
quit (char *err)
{
  fprintf (stderr, err);
  exit (1);
}

void
display_help ()
{
  fprintf (stdout, ""
	   "%s - version %s\n"
	   "Copyright 2003-2005, Mohammed Sameer <msameer (at) foolab.org>\n"
	   "Copyright 2005-2007, Eric Piel <eric.piel (at) tremplin-utc.net>\n"
	   "\n"
	   "Usage: %s [OPTIONS]\n"
	   "\n"
	   "Options:\n"
	   " --help\t\t\t Display this help.\n"
	   " --version\t\t Display version number and exit.\n"
	   " --display DISPLAY\t The X display to connect to.\n"
	   " --icon-size SIZE\t Icon size. Default is 16.\n"
	   " --width SIZE\t\t Dock width. Default is 64.\n"
	   " --height SIZE\t\t Dock height. Default is to be equal to the icon size.\n"
	   " --border SIZE\t\t Border width. Default is 0.\n"
	   " --no-multiple\t\t Do not automatically create a new dock when one is full.\n"
	   "\n"
	   "", PACKAGE, VERSION, PACKAGE);
  exit (0);
}

void
display_version ()
{
  fprintf (stdout, "%s\n", VERSION);
  exit (0);
}

void
parse_cmd_line (int argc, char *argv[])
{
  int x;

  for (x = 1; x < argc; x++)
    {
      dbg_printf ("%s\n", argv[x]);
      if (!strcmp (argv[x], "--help"))
	  display_help ();
      
      else if (!strcmp (argv[x], "--version"))
	  display_version ();
      
      else if (!strcmp (argv[x], "--vertical"))
	  printf("WARNING: --vertical option has been ignored. It is now deprecated, please use the --width and --height options.\n");

      else if (!strcmp (argv[x], "--square"))
	  printf("WARNING: --square option has been ignored. It is now deprecated, please use the --width and --height options.\n");
      
      else if (!strcmp (argv[x], "--multiple"))
	  printf("WARNING: --multiple option is now deprecated and activated by default.\n");
      
      else if (!strcmp (argv[x], "--no-multiple"))
	  no_multiple = 1;
      
      else if (!strcmp (argv[x], "--display")) {
	  if (x + 1 == argc)
	      quit ("--display requires an argument");
	  else
	    {
	      display_name = argv[x + 1];
	      ++x;
	      continue;
	    }
	}
      else if (!strcmp (argv[x], "--width")) {
	  if (x + 1 == argc)
	      quit ("--width requires an argument");
	  else {
	      dock_width = atoi (argv[x + 1]);
	      if (dock_width < 1)
		  quit ("The width must be at least 1 pixel!");
	      ++x;
	      continue;
	    }
	}
      else if (!strcmp (argv[x], "--height")) {
	  if (x + 1 == argc)
	      quit ("--height requires an argument");
	  else {
	      dock_height = atoi (argv[x + 1]);
	      if (dock_height < 1)
		  quit ("The height must be at least 1 pixel!");
	      ++x;
	      continue;
	    }
	}
      else if (!strcmp (argv[x], "--icon-size")) {
	  if (x + 1 == argc)
	      quit ("--icon-size requires an argument");
	  else
	    {
	      icon_size = atoi (argv[x + 1]);
	      if (icon_size < 1)
		{
		  quit ("The icon size must be at least 1 pixel!");
		}
	      ++x;
	      continue;
	    }
	}
      else if (!strcmp (argv[x], "--border")) {
	  if (x + 1 == argc)
	      quit ("--border requires an argument");
	  else
	    {
	      border_width = atoi (argv[x + 1]);
	      if (border_width < 0)
		  quit ("The border width cannot be negative!");
	      ++x;
	      continue;
	    }
	}
      else
	  fprintf (stderr, "Unknown command line argument: %s\n", argv[x]);
    }
}

/*
 * Delete the dock passed in argument.
 * It will silently not delete the first dock as it's necessary to keep 
 * the systray active wrt the X server.
 */
void delete_dock(int dock_nb)
{
	/* never delete the systray window */
	if (dock_nb == 0)
		return;

	XDestroyWindow (display, docks[dock_nb].main);
	XDestroyWindow (display, docks[dock_nb].container);

	/* move the next docks to overwrite this one */
	memmove(&docks[dock_nb],
		&docks[dock_nb + 1],
		(num_docks - dock_nb - 1) * sizeof(struct dock));
	docks = realloc(docks, (num_docks - 1) * sizeof(struct dock));

	num_docks--;
}

/*
 * Manages the creation of one new dock. It includes updating the data structures
 * as well as displaying it's window.
 * Returns a non null value if an error occured.
 */
int create_dock()
{
	XClassHint *class_hint;
	XWMHints *hints;
	struct dock *new_docks, *new_dock;

	/* update the data stuctures */
	new_docks = realloc(docks, (num_docks + 1) * sizeof(struct dock));
	if (new_docks == NULL)
		return -1;
	
	docks = new_docks;
	new_dock = &docks[num_docks];
	new_dock->slots = malloc(slots_per_dock * sizeof(Window));
	memset(new_dock->slots, 0, slots_per_dock * sizeof(Window));
	if (new_dock->slots == NULL)
		return -2;

	/* create the windows */
	new_dock->main =
	XCreateSimpleWindow (display, DefaultRootWindow (display), 0, 0,
			     dock_width, dock_height, border_width, border_width, 0);
	new_dock->container =
	XCreateSimpleWindow (display, new_dock->main, 0, 0,
			     dock_width, dock_height, border_width, border_width, 0);

	if ((class_hint = XAllocClassHint ()) == NULL) {
		fprintf(stderr, "Failed to allocate class hint\n");
		return -2;
	}

	class_hint->res_class = PACKAGE;
	class_hint->res_name = PACKAGE;
	XSetClassHint (display, new_dock->main, class_hint);
	XFree (class_hint);

	if ((hints = XAllocWMHints ()) == NULL) {
		fprintf(stderr, "Failed to allocate hints\n");
		return -2;
	}

	hints->flags = StateHint | WindowGroupHint | IconWindowHint;
	hints->window_group = new_dock->main;
	hints->icon_window = new_dock->container;
	hints->initial_state = WithdrawnState;

	XSetWMHints (display, new_dock->main, hints);
	XFree (hints);

	XSetWindowBackgroundPixmap (display, new_dock->container, ParentRelative);

	/* Add the systray info in case it's the first dock */
	if (num_docks == 0) {
		selection_atom = XInternAtom (display, "_NET_SYSTEM_TRAY_S0", False);
		XSetSelectionOwner (display, selection_atom, new_dock->main, CurrentTime);

		if (XGetSelectionOwner (display, selection_atom) == new_dock->main) {
			XClientMessageEvent xev;
			int scr = DefaultScreen (display);
			Root = RootWindow (display, scr);
			xev.type = ClientMessage;
			xev.window = Root;
			xev.message_type = XInternAtom (display, "MANAGER", False);

			xev.format = 32;
			xev.data.l[0] = CurrentTime;
			xev.data.l[1] = selection_atom;
			xev.data.l[2] = new_dock->main;
			xev.data.l[3] = 0;	/* manager specific data */
			xev.data.l[4] = 0;	/* manager specific data */

			XSendEvent (display, Root, False, StructureNotifyMask, (XEvent *) & xev);

			opcode_atom = XInternAtom (display, "_NET_SYSTEM_TRAY_OPCODE", False);
			data_atom = XInternAtom (display, "_NET_SYSTEM_TRAY_MESSAGE_DATA", False);
		}
		// XXX no else ? shouldn't we quit?
	}

	XSelectInput (display, new_dock->main, SubstructureNotifyMask);
	XMapRaised (display, new_dock->main);
	XFlush (display);

	num_docks++;
	return 0;
}

/*
 * Removes an icon from a given dock. It will automatically delete the whole dock
 * if it's empty (and not the first one).
 */
void remove_icon(int dock_nb, int slot_nb)
{
	int i;

	docks[dock_nb].slots[slot_nb] = 0;

	/* see if the dock is completely empty */
	for (i = 0; i < slots_per_dock; i++) {
		if (docks[dock_nb].slots[i] != 0)
			return;
	}

	/* delete the dock */
	delete_dock(dock_nb);
}

/**
 * Compute the coordinates of a slot from its number in the dock
 */
void x_y_from_slot(int slot_nb, int *x, int *y)
{
	int icons_per_row;

	/* We must have at least one icon per row! */
	icons_per_row = dock_width / icon_size;
	if (icons_per_row == 0)
		icons_per_row = 1;

	*x = (slot_nb % icons_per_row) * icon_size;
	*y = (slot_nb / icons_per_row) * icon_size;

	*x += border_width;
	*y += border_width;

	dbg_printf ("x = %d, y = %d\n", x, y);
}


/**
 * Put a given icon (embed window) on the right place on the right dock.
 */
void add_icon(Window embed, int dock_nb, int slot_nb)
{
	int x, y;

	x_y_from_slot(slot_nb, &x, &y);
	XReparentWindow (display, embed, docks[dock_nb].container, x, y);
	docks[dock_nb].slots[slot_nb] = embed;
}

/* 
 * Returns the slot corresponding to a given icon.
 * It will return an integer as the number of the slot and a second one (by reference)
 * as the number of the dock.
 * In case of error, an negative value is returned.
 */
int find_slot(Window embed, int *dock_nb)
{
	int i, j;

	/* find first slot which corresponds to the window */
	for (i = 0; i < num_docks; i++) {
		for (j = 0; j < slots_per_dock; j++) {
			if (docks[i].slots[j] == embed) {
				*dock_nb = i;
				return j;
			}
		}
	}

	return -1;
}


/* 
 * Returns the first free slot usable for a new icon.
 * It automatically handles the creation of a new dock if there aren't any free
 * slot left.
 * The returned int is the slot number, while the dock number is set via reference.
 * If no free slots are available it will return a negative value.
 */
int find_first_free_slot(int *dock_nb)
{
	int slot_nb;

	slot_nb = find_slot(0, dock_nb);
	if (slot_nb >= 0)
		return slot_nb;

	/* nothing available?*/
	if (no_multiple)
		return -1;

	/* Create more! */
	if (create_dock() != 0)
		return -2;

	*dock_nb = num_docks - 1;
	return 0;
}

/*
 * Adds a new icon to the systray.
 * The function will return a non null value in case of error.
 */
int add_tray_icon(Window embed)
{
	XEvent xevent;
	int this_dock, this_slot;
	dbg_printf ("%s: %i\n", __FUNCTION__, (int) embed);

	this_slot = find_first_free_slot(&this_dock);
	dbg_printf ("slot = %d, dock = %d\n", this_slot, this_dock);
	if (this_slot < 0)
		return this_slot;

	XSelectInput (display, embed, StructureNotifyMask | PropertyChangeMask);
	XWithdrawWindow (display, embed, 0);

	add_icon(embed, this_dock, this_slot);

	XSync (display, False);
	XMapRaised (display, embed);
	xevent.xclient.window = embed;
	xevent.xclient.type = ClientMessage;
	xevent.xclient.message_type = XInternAtom (display, "_XEMBED", False);
	xevent.xclient.format = 32;
	xevent.xclient.data.l[0] = CurrentTime;
	xevent.xclient.data.l[1] = XEMBED_EMBEDDED_NOTIFY;
	xevent.xclient.data.l[2] = 0;
	xevent.xclient.data.l[3] = docks[this_dock].container;
	xevent.xclient.data.l[4] = 0;
	XSendEvent (display, Root, False, NoEventMask, &xevent);

	return 0;
}

void rm_tray_icon(Window embed)
{
	int dock, slot;
	dbg_printf ("%s: %i\n", __FUNCTION__, (int) embed);

	slot = find_slot(embed, &dock);
	if (slot < 0)
		return;

	remove_icon(dock, slot);
}

/**
 * Set up the correct size and place of the window for a slot
 */
void configure_tray_icon_attrib(Window embed)
{
	XWindowAttributes attrib;
	int dock_nb, slot_nb;
	int x, y;

	dbg_printf ("%s: %i\n", __FUNCTION__, (int) embed);

	XGetWindowAttributes (display, embed, &attrib);
	dbg_printf ("X: %i\t Y: %i\t Width: %i\t Height:%i\tBorder: %i\n",
				attrib.x,
				attrib.y,
				attrib.width,
				attrib.height,
				attrib.border_width);

	/* Find the slot and its attributes */
	slot_nb = find_slot(embed, &dock_nb);
	if (slot_nb < 0)
		return;
	x_y_from_slot(slot_nb, &x, &y);

	/* Force the size and position specified by the user */
	if ((attrib.width != icon_size) || (attrib.height != icon_size) ||
	    (attrib.x != x) || (attrib.y != y))
		XMoveResizeWindow (display, embed, x, y, icon_size, icon_size);
}

void handle_event (XEvent ev)
{
	XClientMessageEvent *event = (XClientMessageEvent *) &ev;
	XWindowAttributes tray_icon_attr;
	long opcode = event->data.l[1];
	Window win = event->data.l[2];

	dbg_printf ("%s\n", __FUNCTION__);

	switch (opcode) {
	case SYSTEM_TRAY_REQUEST_DOCK:
		XGetWindowAttributes (display, win, &tray_icon_attr);
		dbg_printf ("X: %i\t Y: %i\t Width: %i\t Height:%i\tBorder: %i\n",
				tray_icon_attr.x,
				tray_icon_attr.y,
				tray_icon_attr.width,
				tray_icon_attr.height,
				tray_icon_attr.border_width);
		XResizeWindow (display, win, icon_size, icon_size);

		if (add_tray_icon(win)) {
			// Not sure if it's necessary
			// XReparentWindow (display, win, DefaultRootWindow(display), 0, 0);
			dbg_printf ("Error when adding another icon.\n");
			return;
		}
		break;
	case SYSTEM_TRAY_BEGIN_MESSAGE:
		// for ballon messages, could be nice to handle them too
		break;
	case SYSTEM_TRAY_CANCEL_MESSAGE:
		break;
	default:
		break;
	}
}

void handle_message_data (XEvent ev)
{
	dbg_printf ("%s\n", __FUNCTION__);
}

void eventLoop(void)
{
  XEvent ev;

  while (1) {
    XNextEvent (display, &ev);
    switch (ev.type) {
    case ConfigureNotify:
	dbg_printf ("ConfigureNotify\n");
	if (ev.xany.window != docks[0].main)
		configure_tray_icon_attrib (ev.xproperty.window);
	break;
    case DestroyNotify:
      {
	XDestroyWindowEvent *xev = (XDestroyWindowEvent *) &ev;
	dbg_printf ("DestroyNotify\n");
	if (xev->window == docks[0].main)
	  return;
	else
	  rm_tray_icon (xev->window);
	break;
      }
    case ReparentNotify:
      dbg_printf ("ReparentNotify\n");
      break;
    case UnmapNotify:
      {
	XUnmapEvent *xev = (XUnmapEvent *) &ev;
	dbg_printf ("UnmapNotify\n");
	if (xev->window != docks[0].main)
	  rm_tray_icon (xev->window);
	break;
      }
    case ClientMessage:
      if (ev.xclient.message_type == opcode_atom)
	handle_event (ev);
      else if (ev.xclient.message_type == data_atom)
	handle_message_data (ev);
      break;
    case SelectionClear:
      dbg_printf ("SelectionClear\n");
      if (XGetSelectionOwner (display, selection_atom) == docks[0].main)
	XSetSelectionOwner (display, selection_atom, None, CurrentTime);
      return;
    default:
      dbg_printf ("message=%i\n", ev.type);
      break;
    }
  }
}

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

	/* make dock_height same as icon_size if it was not specified */
	if (dock_height == -1)
		dock_height = icon_size;

	if ((display = XOpenDisplay (display_name)) == NULL)
		quit ("Cannot open display.\n");

	/* ?:1 is there to consider one slot instead of zero if their size is too big */
	slots_per_dock = (dock_height / icon_size ?:1) * (dock_width / icon_size ?:1);
	dbg_printf ("%d slots_per_dock\n", slots_per_dock);
	if (create_dock())
		quit("Error while creating the dock.\n");

	eventLoop ();

	XCloseDisplay (display);
	return EXIT_SUCCESS;
}
