/**
 * @file client.c
 * @author Joe Wingbermuehle
 * @date 2004-2006
 *
 * @brief Client window functions.
 *
 */

#include "jwm.h"
#include "client.h"
#include "clientlist.h"
#include "icon.h"
#include "group.h"
#include "tray.h"
#include "confirm.h"
#include "cursor.h"
#include "taskbar.h"
#include "screen.h"
#include "pager.h"
#include "color.h"
#include "place.h"
#include "event.h"
#include "settings.h"
#include "timing.h"
#include "grab.h"
#include "desktop.h"

static ClientNode *activeClient;

unsigned int clientCount;

static void LoadFocus(void);
static void RestackTransients(const ClientNode *np);
static void MinimizeTransients(ClientNode *np, char lower);
static void RestoreTransients(ClientNode *np, char raise);
static void KillClientHandler(ClientNode *np);
static void UnmapClient(ClientNode *np);

/** Load windows that are already mapped. */
void StartupClients(void)
{

   XWindowAttributes attr;
   Window rootReturn, parentReturn, *childrenReturn;
   unsigned int childrenCount;
   unsigned int x;

   clientCount = 0;
   activeClient = NULL;
   currentDesktop = 0;

   /* Clear out the client lists. */
   for(x = 0; x < LAYER_COUNT; x++) {
      nodes[x] = NULL;
      nodeTail[x] = NULL;
   }

   /* Query client windows. */
   JXQueryTree(display, rootWindow, &rootReturn, &parentReturn,
               &childrenReturn, &childrenCount);

   /* Add each client. */
   for(x = 0; x < childrenCount; x++) {
      if(JXGetWindowAttributes(display, childrenReturn[x], &attr)) {
         if(attr.override_redirect == False && attr.map_state == IsViewable) {
            AddClientWindow(childrenReturn[x], 1, 1);
         }
      }
   }

   JXFree(childrenReturn);

   LoadFocus();

   RequireTaskUpdate();
   RequirePagerUpdate();

}

/** Release client windows. */
void ShutdownClients(void)
{

   int x;

   for(x = 0; x < LAYER_COUNT; x++) {
      while(nodeTail[x]) {
         RemoveClient(nodeTail[x]);
      }
   }

}

/** Set the focus to the window currently under the mouse pointer. */
void LoadFocus(void)
{

   ClientNode *np;
   Window rootReturn, childReturn;
   int rootx, rooty;
   int winx, winy;
   unsigned int mask;

   JXQueryPointer(display, rootWindow, &rootReturn, &childReturn,
                  &rootx, &rooty, &winx, &winy, &mask);

   np = FindClient(childReturn);
   if(np) {
      FocusClient(np);
   }

}

/** Add a window to management. */
ClientNode *AddClientWindow(Window w, char alreadyMapped, char notOwner)
{

   XWindowAttributes attr;
   ClientNode *np;

   Assert(w != None);

   /* Get window attributes. */
   if(JXGetWindowAttributes(display, w, &attr) == 0) {
      return NULL;
   }

   /* Determine if we should care about this window. */
   if(attr.override_redirect == True) {
      return NULL;
   }
   if(attr.class == InputOnly) {
      return NULL;
   }

   /* Prepare a client node for this window. */
   np = Allocate(sizeof(ClientNode));
   memset(np, 0, sizeof(ClientNode));

   np->window = w;
   np->parent = None;
   np->owner = None;
   np->state.desktop = currentDesktop;

   np->x = attr.x;
   np->y = attr.y;
   np->width = attr.width;
   np->height = attr.height;
   np->cmap = attr.colormap;
   np->state.status = STAT_NONE;
   np->state.maxFlags = MAX_NONE;
   np->state.layer = LAYER_NORMAL;
   np->state.defaultLayer = LAYER_NORMAL;

   np->state.border = BORDER_DEFAULT;
   np->borderAction = BA_NONE;

   ReadClientInfo(np, alreadyMapped);

   if(!notOwner) {
      np->state.border = BORDER_OUTLINE | BORDER_TITLE | BORDER_MOVE;
      np->state.status |= STAT_WMDIALOG | STAT_STICKY;
      np->state.layer = LAYER_ABOVE;
      np->state.defaultLayer = LAYER_ABOVE;
   }

   ApplyGroups(np);
   if(np->icon == NULL) {
      LoadIcon(np);
   }

   /* We now know the layer, so insert */
   np->prev = NULL;
   np->next = nodes[np->state.layer];
   if(np->next) {
      np->next->prev = np;
   } else {
      nodeTail[np->state.layer] = np;
   }
   nodes[np->state.layer] = np;

   SetDefaultCursor(np->window);

   if(notOwner) {
      XSetWindowAttributes sattr;
      JXAddToSaveSet(display, np->window);
      sattr.event_mask
         = EnterWindowMask
         | ColormapChangeMask
         | PropertyChangeMask
         | KeyReleaseMask
         | StructureNotifyMask;
      sattr.do_not_propagate_mask = ButtonPressMask
                                  | ButtonReleaseMask
                                  | PointerMotionMask
                                  | KeyPressMask
                                  | KeyReleaseMask;
      JXChangeWindowAttributes(display, np->window,
                               CWEventMask | CWDontPropagate, &sattr);
   }
   JXGrabButton(display, AnyButton, AnyModifier, np->window, True,
                ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);

   PlaceClient(np, alreadyMapped);
   ReparentClient(np);
   XSaveContext(display, np->window, clientContext, (void*)np);

   if(np->state.status & STAT_MAPPED) {
      JXMapWindow(display, np->window);
   }

   clientCount += 1;

   if(!alreadyMapped) {
      RaiseClient(np);
   }

   if(np->state.status & STAT_OPACITY) {
      SetOpacity(np, np->state.opacity, 1);
   } else {
      SetOpacity(np, settings.inactiveClientOpacity, 1);
   }
   if(np->state.status & STAT_STICKY) {
      SetCardinalAtom(np->window, ATOM_NET_WM_DESKTOP, ~0UL);
   } else {
      SetCardinalAtom(np->window, ATOM_NET_WM_DESKTOP, np->state.desktop);
   }

   /* Shade the client if requested. */
   if(np->state.status & STAT_SHADED) {
      np->state.status &= ~STAT_SHADED;
      ShadeClient(np);
   }

   /* Minimize the client if requested. */
   if(np->state.status & STAT_MINIMIZED) {
      np->state.status &= ~STAT_MINIMIZED;
      MinimizeClient(np, 0);
   }

   /* Maximize the client if requested. */
   if(np->state.maxFlags) {
      const MaxFlags flags = np->state.maxFlags;
      np->state.maxFlags = MAX_NONE;
      MaximizeClient(np, flags);
   }

   if(np->state.status & STAT_URGENT) {
      RegisterCallback(URGENCY_DELAY, SignalUrgent, np);
   }

   /* Update task bars. */
   AddClientToTaskBar(np);

   /* Make sure we're still in sync */
   WriteState(np);
   SendConfigureEvent(np);

   /* Hide the client if we're not on the right desktop. */
   if(np->state.desktop != currentDesktop
      && !(np->state.status & STAT_STICKY)) {
      HideClient(np);
   }

   ReadClientStrut(np);

   /* Focus transients if their parent has focus. */
   if(np->owner != None) {
      if(activeClient && np->owner == activeClient->window) {
         FocusClient(np);
      }
   }

   /* Make the client fullscreen if requested. */
   if(np->state.status & STAT_FULLSCREEN) {
      np->state.status &= ~STAT_FULLSCREEN;
      SetClientFullScreen(np, 1);
   }
   ResetBorder(np);

   return np;
}

/** Minimize a client window and all of its transients. */
void MinimizeClient(ClientNode *np, char lower)
{
   Assert(np);
   MinimizeTransients(np, lower);
   RequireRestack();
   RequireTaskUpdate();
}

/** Minimize all transients as well as the specified client. */
void MinimizeTransients(ClientNode *np, char lower)
{

   ClientNode *tp;
   int x;

   Assert(np);

   /* Unmap the window and update its state. */
   if(np->state.status & (STAT_MAPPED | STAT_SHADED)) {
      UnmapClient(np);
      if(np->parent != None) {
         JXUnmapWindow(display, np->parent);
      }
   }
   np->state.status |= STAT_MINIMIZED;

   /* Minimize transient windows. */
   for(x = 0; x < LAYER_COUNT; x++) {
      tp = nodes[x];
      while(tp) {
         ClientNode *next = tp->next;
         if(tp->owner == np->window
            && (tp->state.status & (STAT_MAPPED | STAT_SHADED))
            && !(tp->state.status & STAT_MINIMIZED)) {
            MinimizeTransients(tp, lower);
         }
         tp = next;
      }
   }

   /* Focus the next window. */
   if(np->state.status & STAT_ACTIVE) {
      FocusNextStacked(np);
   }

   if(lower) {
      /* Move this client to the end of the layer list. */
      if(nodeTail[np->state.layer] != np) {
         if(np->prev) {
            np->prev->next = np->next;
         } else {
            nodes[np->state.layer] = np->next;
         }
         np->next->prev = np->prev;
         tp = nodeTail[np->state.layer];
         nodeTail[np->state.layer] = np;
         tp->next = np;
         np->prev = tp;
         np->next = NULL;
      }
   }

   WriteState(np);

}

/** Shade a client. */
void ShadeClient(ClientNode *np)
{

   Assert(np);

   if((np->state.status & (STAT_SHADED | STAT_FULLSCREEN)) ||
      !(np->state.border & BORDER_SHADE)) {
      return;
   }

   UnmapClient(np);
   np->state.status |= STAT_SHADED;

   WriteState(np);
   ResetBorder(np);
   RequirePagerUpdate();

}

/** Unshade a client. */
void UnshadeClient(ClientNode *np)
{

   Assert(np);

   if(!(np->state.status & STAT_SHADED)) {
      return;
   }

   if(!(np->state.status & (STAT_MINIMIZED | STAT_SDESKTOP))) {
      JXMapWindow(display, np->window);
      np->state.status |= STAT_MAPPED;
   }
   np->state.status &= ~STAT_SHADED;

   WriteState(np);
   ResetBorder(np);
   RefocusClient();
   RequirePagerUpdate();

}

/** Set a client's state to withdrawn. */
void SetClientWithdrawn(ClientNode *np)
{

   Assert(np);

   if(activeClient == np) {
      activeClient = NULL;
      np->state.status &= ~STAT_ACTIVE;
      FocusNextStacked(np);
   }

   if(np->state.status & STAT_MAPPED) {
      UnmapClient(np);
      if(np->parent != None) {
         JXUnmapWindow(display, np->parent);
      }
   } else if(np->state.status & STAT_SHADED) {
      if(!(np->state.status & STAT_MINIMIZED)) {
         if(np->parent != None) {
            JXUnmapWindow(display, np->parent);
         }
      }
   }

   np->state.status &= ~STAT_SHADED;
   np->state.status &= ~STAT_MINIMIZED;
   np->state.status &= ~STAT_SDESKTOP;

   WriteState(np);
   RequireTaskUpdate();
   RequirePagerUpdate();

}

/** Restore a window with its transients (helper method). */
void RestoreTransients(ClientNode *np, char raise)
{

   ClientNode *tp;
   int x;

   Assert(np);

   /* Make sure this window is on the current desktop. */
   SetClientDesktop(np, currentDesktop);

   /* Restore this window. */
   if(!(np->state.status & STAT_MAPPED)) {
      if(np->state.status & STAT_SHADED) {
         if(np->parent != None) {
            JXMapWindow(display, np->parent);
         }
      } else {
         JXMapWindow(display, np->window);
         if(np->parent != None) {
            JXMapWindow(display, np->parent);
         }
         np->state.status |= STAT_MAPPED;
      }
   }
   np->state.status &= ~STAT_MINIMIZED;
   np->state.status &= ~STAT_SDESKTOP;

   /* Restore transient windows. */
   for(x = 0; x < LAYER_COUNT; x++) {
      for(tp = nodes[x]; tp; tp = tp->next) {
         if(tp->owner == np->window && (tp->state.status & STAT_MINIMIZED)) {
            RestoreTransients(tp, raise);
         }
      }
   }

   if(raise) {
      FocusClient(np);
      RaiseClient(np);
   }
   WriteState(np);

}

/** Restore a client window and its transients. */
void RestoreClient(ClientNode *np, char raise)
{
   if((np->state.status & STAT_FIXED) && !(np->state.status & STAT_STICKY)) {
      ChangeDesktop(np->state.desktop);
   }
   RestoreTransients(np, raise);
   RequireRestack();
   RequireTaskUpdate();
}

/** Set the client layer. This will affect transients. */
void SetClientLayer(ClientNode *np, unsigned int layer)
{

   ClientNode *tp, *next;

   Assert(np);
   Assert(layer <= LAST_LAYER);

   if(np->state.layer != layer) {
      int x;

      /* Loop through all clients so we get transients. */
      for(x = FIRST_LAYER; x <= LAST_LAYER; x++) {
         tp = nodes[x];
         while(tp) {
            next = tp->next;
            if(tp == np || tp->owner == np->window) {

               /* Remove from the old node list */
               if(next) {
                  next->prev = tp->prev;
               } else {
                  nodeTail[tp->state.layer] = tp->prev;
               }
               if(tp->prev) {
                  tp->prev->next = next;
               } else {
                  nodes[tp->state.layer] = next;
               }

               /* Insert into the new node list */
               tp->prev = NULL;
               tp->next = nodes[layer];
               if(nodes[layer]) {
                  nodes[layer]->prev = tp;
               } else {
                  nodeTail[layer] = tp;
               }
               nodes[layer] = tp;

               /* Set the new layer */
               tp->state.layer = layer;
               WriteState(tp);

            }
            tp = next;
         }
      }

      RequireRestack();

   }

}

/** Set a client's sticky status. This will update transients. */
void SetClientSticky(ClientNode *np, char isSticky)
{

   ClientNode *tp;
   int x;
   char old;

   Assert(np);

   /* Get the old sticky status. */
   if(np->state.status & STAT_STICKY) {
      old = 1;
   } else {
      old = 0;
   }

   if(isSticky && !old) {

      /* Change from non-sticky to sticky. */

      for(x = 0; x < LAYER_COUNT; x++) {
         for(tp = nodes[x]; tp; tp = tp->next) {
            if(tp == np || tp->owner == np->window) {
               tp->state.status |= STAT_STICKY;
               SetCardinalAtom(tp->window, ATOM_NET_WM_DESKTOP, ~0UL);
               WriteState(tp);
            }
         }
      }

   } else if(!isSticky && old) {

      /* Change from sticky to non-sticky. */

      for(x = 0; x < LAYER_COUNT; x++) {
         for(tp = nodes[x]; tp; tp = tp->next) {
            if(tp == np || tp->owner == np->window) {
               tp->state.status &= ~STAT_STICKY;
               WriteState(tp);
            }
         }
      }

      /* Since this client is no longer sticky, we need to assign
       * a desktop. Here we use the current desktop.
       * Note that SetClientDesktop updates transients (which is good).
       */
      SetClientDesktop(np, currentDesktop);

   }

}

/** Set a client's desktop. This will update transients. */
void SetClientDesktop(ClientNode *np, unsigned int desktop)
{

   ClientNode *tp;

   Assert(np);

   if(JUNLIKELY(desktop >= settings.desktopCount)) {
      return;
   }

   if(!(np->state.status & STAT_STICKY)) {
      int x;
      for(x = 0; x < LAYER_COUNT; x++) {
         for(tp = nodes[x]; tp; tp = tp->next) {
            if(tp == np || tp->owner == np->window) {

               tp->state.desktop = desktop;

               if(desktop == currentDesktop) {
                  ShowClient(tp);
               } else {
                  HideClient(tp);
               }

               SetCardinalAtom(tp->window, ATOM_NET_WM_DESKTOP,
                               tp->state.desktop);
            }
         }
      }
      RequirePagerUpdate();
      RequireTaskUpdate();
   }

}

/** Hide a client. This will not update transients. */
void HideClient(ClientNode *np)
{
   if(!(np->state.status & STAT_HIDDEN)) {
      if(activeClient == np) {
         activeClient = NULL;
      }
      np->state.status |= STAT_HIDDEN;
      if(np->state.status & (STAT_MAPPED | STAT_SHADED)) {
         if(np->parent != None) {
            JXUnmapWindow(display, np->parent);
         } else {
            JXUnmapWindow(display, np->window);
         }
      }
   }
}

/** Show a hidden client. This will not update transients. */
void ShowClient(ClientNode *np)
{
   if(np->state.status & STAT_HIDDEN) {
      np->state.status &= ~STAT_HIDDEN;
      if(np->state.status & (STAT_MAPPED | STAT_SHADED)) {
         if(!(np->state.status & STAT_MINIMIZED)) {
            if(np->parent != None) {
               JXMapWindow(display, np->parent);
            } else {
               JXMapWindow(display, np->window);
            }
            if(np->state.status & STAT_ACTIVE) {
               FocusClient(np);
            }
         }
      }
   }
}

/** Maximize a client window. */
void MaximizeClient(ClientNode *np, MaxFlags flags)
{

    /* Return if we don't have a client. */
    if(np == NULL) {
        return;
    }

   /* Don't allow maximization of full-screen clients. */
   if(np->state.status & STAT_FULLSCREEN) {
      return;
   }
   if(!(np->state.border & BORDER_MAX)) {
      return;
   }

   if(np->state.status & STAT_SHADED) {
      UnshadeClient(np);
   }

   if(np->state.status & STAT_MINIMIZED) {
      RestoreClient(np, 1);
   }

   RaiseClient(np);
   FocusClient(np);
   if(np->state.maxFlags) {
      /* Undo existing maximization. */
      np->x = np->oldx;
      np->y = np->oldy;
      np->width = np->oldWidth;
      np->height = np->oldHeight;
      np->state.maxFlags = MAX_NONE;
   }
   if(flags != MAX_NONE) {
      /* Maximize if requested. */
      PlaceMaximizedClient(np, flags);
   }

   WriteState(np);
   ResetBorder(np);
   DrawBorder(np);
   SendConfigureEvent(np);
   RequirePagerUpdate();

}

/** Maximize a client using its default maximize settings. */
void MaximizeClientDefault(ClientNode *np)
{

   MaxFlags flags = MAX_NONE;

   Assert(np);

   if(np->state.maxFlags == MAX_NONE) {
      if(np->state.border & BORDER_MAX_H) {
         flags |= MAX_HORIZ;
      }
      if(np->state.border & BORDER_MAX_V) {
         flags |= MAX_VERT;
      }
   }

   MaximizeClient(np, flags);

}

/** Set a client's full screen state. */
void SetClientFullScreen(ClientNode *np, char fullScreen)
{

   XEvent event;
   int north, south, east, west;
   BoundingBox box;
   const ScreenType *sp;

   Assert(np);

   /* Make sure there's something to do. */
   if(!fullScreen == !(np->state.status & STAT_FULLSCREEN)) {
      return;
   }
   if(!(np->state.border & BORDER_FULLSCREEN)) {
      return;
   }

   if(np->state.status & STAT_SHADED) {
      UnshadeClient(np);
   }

   if(fullScreen) {

      np->state.status |= STAT_FULLSCREEN;

      if(!(np->state.maxFlags)) {
         np->oldx = np->x;
         np->oldy = np->y;
         np->oldWidth = np->width;
         np->oldHeight = np->height;
      }

      sp = GetCurrentScreen(np->x, np->y);
      GetScreenBounds(sp, &box);

      GetBorderSize(&np->state, &north, &south, &east, &west);
      box.x += west;
      box.y += north;
      box.width -= east + west;
      box.height -= north + south;

      np->x = box.x;
      np->y = box.y;
      np->width = box.width;
      np->height = box.height;
      ResetBorder(np);

   } else {

      np->state.status &= ~STAT_FULLSCREEN;

      np->x = np->oldx;
      np->y = np->oldy;
      np->width = np->oldWidth;   
      np->height = np->oldHeight;
      ConstrainSize(np);
      ConstrainPosition(np);

      if(np->state.maxFlags != MAX_NONE) {
         PlaceMaximizedClient(np, np->state.maxFlags);
      }

      ResetBorder(np);

      event.type = MapRequest;
      event.xmaprequest.send_event = True;
      event.xmaprequest.display = display;
      event.xmaprequest.parent = np->parent;
      event.xmaprequest.window = np->window;
      JXSendEvent(display, rootWindow, False,
                  SubstructureRedirectMask, &event);

   }

   WriteState(np);
   SendConfigureEvent(np);
   RequireRestack();

}

/** Set the active client. */
void FocusClient(ClientNode *np)
{
   if(np->state.status & STAT_HIDDEN) {
      return;
   }
   if(!(np->state.status & (STAT_CANFOCUS | STAT_TAKEFOCUS))) {
      return;
   }

   if(activeClient != np || !(np->state.status & STAT_ACTIVE)) {
      if(activeClient) {
         activeClient->state.status &= ~STAT_ACTIVE;
         if(!(activeClient->state.status & STAT_OPACITY)) {
            SetOpacity(activeClient, settings.inactiveClientOpacity, 0);
         }
         DrawBorder(activeClient);
         WriteNetState(activeClient);
      }
      np->state.status |= STAT_ACTIVE;
      activeClient = np;
      if(!(np->state.status & STAT_OPACITY)) {
         SetOpacity(np, settings.activeClientOpacity, 0);
      }

      DrawBorder(np);
      RequirePagerUpdate();
      RequireTaskUpdate();
   }

   if(np->state.status & STAT_MAPPED) {
      UpdateClientColormap(np);
      SetWindowAtom(rootWindow, ATOM_NET_ACTIVE_WINDOW, np->window);
      WriteNetState(np);
      if(np->state.status & STAT_CANFOCUS) {
         JXSetInputFocus(display, np->window, RevertToParent, eventTime);
      }
      if(np->state.status & STAT_TAKEFOCUS) {
         SendClientMessage(np->window, ATOM_WM_PROTOCOLS, ATOM_WM_TAKE_FOCUS);
      }
   } else {
      JXSetInputFocus(display, rootWindow, RevertToParent, eventTime);
   }

}


/** Refocus the active client (if there is one). */
void RefocusClient(void)
{
   if(activeClient) {
      FocusClient(activeClient);
   }
}

/** Send a delete message to a client. */
void DeleteClient(ClientNode *np)
{
   Assert(np);
   ReadWMProtocols(np->window, &np->state);
   if(np->state.status & STAT_DELETE) {
      SendClientMessage(np->window, ATOM_WM_PROTOCOLS, ATOM_WM_DELETE_WINDOW);
   } else {
      KillClient(np);
   }
}

/** Callback to kill a client after a confirm dialog. */
void KillClientHandler(ClientNode *np)
{
   if(np == activeClient) {
      FocusNextStacked(np);
   }

   JXKillClient(display, np->window);
}

/** Kill a client window. */
void KillClient(ClientNode *np)
{
   Assert(np);
   ShowConfirmDialog(np, KillClientHandler,
      _("Kill this window?"),
      _("This may cause data to be lost!"),
      NULL);
}

/** Place transients on top of the owner. */
void RestackTransients(const ClientNode *np)
{
   ClientNode *tp;
   unsigned int layer;

   /* Place any transient windows on top of the owner */
   for(layer = 0; layer < LAYER_COUNT; layer++) {
      for(tp = nodes[layer]; tp; tp = tp->next) {
         if(tp->owner == np->window && tp->prev) {

            ClientNode *next = tp->next;

            tp->prev->next = tp->next;
            if(tp->next) {
               tp->next->prev = tp->prev;
            } else {
               nodeTail[tp->state.layer] = tp->prev;
            }
            tp->next = nodes[tp->state.layer];
            nodes[tp->state.layer]->prev = tp;
            tp->prev = NULL;
            nodes[tp->state.layer] = tp;

            tp = next;

         }

         /* tp will be tp->next if the above code is executed. */
         /* Thus, if it is NULL, we are done with this layer. */
         if(!tp) {
            break;
         }
      }
   }
}

/** Raise the client. This will affect transients. */
void RaiseClient(ClientNode *np)
{

   Assert(np);

   if(nodes[np->state.layer] != np) {

      /* Raise the window */
      Assert(np->prev);
      np->prev->next = np->next;
      if(np->next) {
         np->next->prev = np->prev;
      } else {
         nodeTail[np->state.layer] = np->prev;
      }
      np->next = nodes[np->state.layer];
      nodes[np->state.layer]->prev = np;
      np->prev = NULL;
      nodes[np->state.layer] = np;

   }

   RestackTransients(np);
   RequireRestack();

}

/** Restack a client window. This will not affect transients. */
void RestackClient(ClientNode *np, Window above, int detail)
{

   ClientNode *tp;
   char inserted = 0;

   /* Remove from the window list. */
   if(np->prev) {
      np->prev->next = np->next;
   } else {
      nodes[np->state.layer] = np->next;
   }
   if(np->next) {
      np->next->prev = np->prev;
   } else {
      nodeTail[np->state.layer] = np->prev;
   }

   /* Insert back into the window list. */
   if(above != None && above != np->window) {

      /* Insert relative to some other window. */
      char found = 0;
      for(tp = nodes[np->state.layer]; tp; tp = tp->next) {
         if(tp == np) {
            found = 1;
         } else if(tp->window == above) {
            char insert_before = 0;
            inserted = 1;
            switch(detail) {
            case Above:
            case TopIf:
               insert_before = 1;
               break;
            case Below:
            case BottomIf:
               insert_before = 0;
               break;
            case Opposite:
               insert_before = !found;
               break;
            }
            if(insert_before) {

               /* Insert before this window. */
               np->prev = tp->prev;
               np->next = tp;
               if(tp->prev) {
                  tp->prev->next = np;
               } else {
                  nodes[np->state.layer] = np;
               }
               tp->prev = np;

            } else {

               /* Insert after this window. */
               np->prev = tp;
               np->next = tp->next;
               if(tp->next) {
                  tp->next->prev = np;
               } else {
                  nodeTail[np->state.layer] = np;
               }
               tp->next = np;

            }
            break;
         }
      }
   }
   if(!inserted) {

      /* Insert absolute for the layer. */
      if(detail == Below || detail == BottomIf) {

         /* Insert to the bottom of the stack. */
         np->next = NULL;
         np->prev = nodeTail[np->state.layer];
         if(nodeTail[np->state.layer]) {
            nodeTail[np->state.layer]->next = np;
         } else {
            nodes[np->state.layer] = np;
         }
         nodeTail[np->state.layer] = np;

      } else {

         /* Insert at the top of the stack. */
         np->next = nodes[np->state.layer];
         np->prev = NULL;
         if(nodes[np->state.layer]) {
            nodes[np->state.layer]->prev = np;
         } else {
            nodeTail[np->state.layer] = np;
         }
         nodes[np->state.layer] = np;

      }
   }

   RestackTransients(np);
   RequireRestack();

}

/** Restack the clients according the way we want them. */
void RestackClients(void)
{

   TrayType *tp;
   ClientNode *np;
   unsigned int layer, index;
   int trayCount;
   Window *stack;
   Window fw;

   if(JUNLIKELY(shouldExit)) {
      return;
   }

   /* Allocate memory for restacking. */
   trayCount = GetTrayCount();
   stack = AllocateStack((clientCount + trayCount) * sizeof(Window));

   /* Prepare the stacking array. */
   fw = None;
   index = 0;
   if(activeClient && (activeClient->state.status & STAT_FULLSCREEN)) {
      fw = activeClient->window;
      for(np = nodes[activeClient->state.layer]; np; np = np->next) {
         if(np->owner == fw) {
            if(np->parent != None) {
               stack[index] = np->parent;
            } else {
               stack[index] = np->window;
            }
            index += 1;
         }
      }
      if(activeClient->parent != None) {
         stack[index] = activeClient->parent;
      } else {
         stack[index] = activeClient->window;
      }
      index += 1;
   }
   layer = LAST_LAYER;
   for(;;) {

      for(np = nodes[layer]; np; np = np->next) {
         if(    (np->state.status & (STAT_MAPPED | STAT_SHADED))
            && !(np->state.status & STAT_HIDDEN)) {
            if(fw != None && (np->window == fw || np->owner == fw)) {
               continue;
            }
            if(np->parent != None) {
               stack[index] = np->parent;
            } else {
               stack[index] = np->window;
            }
            index += 1;
         }
      }

      for(tp = GetTrays(); tp; tp = tp->next) {
         if(layer == tp->layer) {
            stack[index] = tp->window;
            index += 1;
         }
      }

      if(layer == FIRST_LAYER) {
         break;
      }
      layer -= 1;

   }

   JXRestackWindows(display, stack, index);

   ReleaseStack(stack);
   UpdateNetClientList();
   RequirePagerUpdate();

}

/** Send a client message to a window. */
void SendClientMessage(Window w, AtomType type, AtomType message)
{

   XEvent event;
   int status;

   memset(&event, 0, sizeof(event));
   event.xclient.type = ClientMessage;
   event.xclient.window = w;
   event.xclient.message_type = atoms[type];
   event.xclient.format = 32;
   event.xclient.data.l[0] = atoms[message];
   event.xclient.data.l[1] = eventTime;

   status = JXSendEvent(display, w, False, 0, &event);
   if(JUNLIKELY(status == False)) {
      Debug("SendClientMessage failed");
   }

}

/** Remove a client window from management. */
void RemoveClient(ClientNode *np)
{

   ColormapNode *cp;

   Assert(np);
   Assert(np->window != None);

   /* Remove this client from the client list */
   if(np->next) {
      np->next->prev = np->prev;
   } else {
      nodeTail[np->state.layer] = np->prev;
   }
   if(np->prev) {
      np->prev->next = np->next;
   } else {
      nodes[np->state.layer] = np->next;
   }
   clientCount -= 1;
   XDeleteContext(display, np->window, clientContext);
   if(np->parent != None) {
      XDeleteContext(display, np->parent, frameContext);
   }

   if(np->state.status & STAT_URGENT) {
      UnregisterCallback(SignalUrgent, np);
   }

   /* Make sure this client isn't active */
   if(activeClient == np && !shouldExit) {
      FocusNextStacked(np);
   }
   if(activeClient == np) {

      /* Must be the last client. */
      SetWindowAtom(rootWindow, ATOM_NET_ACTIVE_WINDOW, None);
      activeClient = NULL;
      JXSetInputFocus(display, rootWindow, RevertToParent, eventTime);

   }

   /* If the window manager is exiting (ie, not the client), then
    * reparent etc. */
   if(shouldExit && !(np->state.status & STAT_WMDIALOG)) {
      if(np->state.maxFlags) {
         np->x = np->oldx;
         np->y = np->oldy;
         np->width = np->oldWidth;
         np->height = np->oldHeight;
         JXMoveResizeWindow(display, np->window,
                            np->x, np->y, np->width, np->height);
      }
      GravitateClient(np, 1);
      if((np->state.status & STAT_HIDDEN)
         || (!(np->state.status & STAT_MAPPED)
            && (np->state.status & (STAT_MINIMIZED | STAT_SHADED)))) {
         JXMapWindow(display, np->window);
      }
      JXUngrabButton(display, AnyButton, AnyModifier, np->window);
      JXReparentWindow(display, np->window, rootWindow, np->x, np->y);
      JXRemoveFromSaveSet(display, np->window);
   }

   /* Destroy the parent */
   if(np->parent) {
      JXDestroyWindow(display, np->parent);
   }

   if(np->name) {
      Release(np->name);
   }
   if(np->instanceName) {
      JXFree(np->instanceName);
   }
   if(np->className) {
      JXFree(np->className);
   }

   RemoveClientFromTaskBar(np);
   RemoveClientStrut(np);

   while(np->colormaps) {
      cp = np->colormaps->next;
      Release(np->colormaps);
      np->colormaps = cp;
   }

   DestroyIcon(np->icon);

   Release(np);

   RequireRestack();

}

/** Get the active client (possibly NULL). */
ClientNode *GetActiveClient(void)
{
   return activeClient;
}

/** Find a client by parent or window. */
ClientNode *FindClient(Window w)
{
   ClientNode *np;
   np = FindClientByWindow(w);
   if(!np) {
      np = FindClientByParent(w);
   }
   return np;
}

/** Find a client by window. */
ClientNode *FindClientByWindow(Window w)
{
   ClientNode *np;
   if(!XFindContext(display, w, clientContext, (void*)&np)) {
      return np;
   } else {
      return NULL;
   }
}

/** Find a client by its frame window. */
ClientNode *FindClientByParent(Window p)
{
   ClientNode *np;
   if(!XFindContext(display, p, frameContext, (void*)&np)) {
      return np;
   } else {
      return NULL;
   }
}

/** Reparent a client window. */
void ReparentClient(ClientNode *np)
{
   XSetWindowAttributes attr;
   XEvent event;
   int attrMask;
   int x, y, width, height;
   int north, south, east, west;

   if((np->state.border & (BORDER_TITLE | BORDER_OUTLINE)) == 0) {

      if(np->parent == None) {
         return;
      }

      JXReparentWindow(display, np->window, rootWindow, np->x, np->y);
      XDeleteContext(display, np->parent, frameContext);
      JXDestroyWindow(display, np->parent);
      np->parent = None;

   } else {

      if(np->parent != None) {
         return;
      }

      attrMask = 0;

      /* We can't use PointerMotionHint mask here since the exact location
       * of the mouse on the frame is important. */
      attrMask |= CWEventMask;
      attr.event_mask
         = ButtonPressMask
         | ButtonReleaseMask
         | ExposureMask
         | PointerMotionMask
         | SubstructureRedirectMask
         | SubstructureNotifyMask
         | EnterWindowMask
         | LeaveWindowMask
         | KeyPressMask
         | KeyReleaseMask;

      attrMask |= CWDontPropagate;
      attr.do_not_propagate_mask = ButtonPressMask | ButtonReleaseMask;

      attrMask |= CWBackPixel;
      attr.background_pixel = colors[COLOR_TITLE_BG2];

      attrMask |= CWBorderPixel;
      attr.border_pixel = 0;

      x = np->x;
      y = np->y;
      width = np->width;
      height = np->height;
      GetBorderSize(&np->state, &north, &south, &east, &west);
      x -= west;
      y -= north;
      width += east + west;
      height += north + south;

      /* Create the frame window. */
      np->parent = JXCreateWindow(display, rootWindow, x, y, width, height,
                                  0, rootDepth, InputOutput,
                                  rootVisual, attrMask, &attr);
      XSaveContext(display, np->parent, frameContext, (void*)np);

      JXSetWindowBorderWidth(display, np->window, 0);

      /* Reparent the client window. */
      JXReparentWindow(display, np->window, np->parent, west, north);

      if(np->state.status & STAT_MAPPED) {
         JXMapWindow(display, np->parent);
      }
   }

   JXSync(display, False);
   JXCheckTypedWindowEvent(display, np->window, UnmapNotify, &event);

}

/** Send a configure event to a client window. */
void SendConfigureEvent(ClientNode *np)
{

   XConfigureEvent event;
   const ScreenType *sp;

   Assert(np);

   memset(&event, 0, sizeof(event));
   event.display = display;
   event.type = ConfigureNotify;
   event.event = np->window;
   event.window = np->window;
   if(np->state.status & STAT_FULLSCREEN) {
      sp = GetCurrentScreen(np->x, np->y);
      event.x = sp->x;
      event.y = sp->y;
      event.width = sp->width;
      event.height = sp->height;
   } else {
      event.x = np->x;
      event.y = np->y;
      event.width = np->width;
      event.height = np->height;
   }

   JXSendEvent(display, np->window, False, StructureNotifyMask,
               (XEvent*)&event);

}

/** Update a window's colormap.
 * A call to this function indicates that the colormap(s) for the given
 * client changed. This will change the active colormap(s) if the given
 * client is active.
 */
void UpdateClientColormap(ClientNode *np)
{

   Assert(np);

   if(np == activeClient) {

      ColormapNode *cp = np->colormaps;
      char wasInstalled = 0;
      while(cp) {
         XWindowAttributes attr;
         if(JXGetWindowAttributes(display, cp->window, &attr)) {
            if(attr.colormap != None) {
               if(attr.colormap == np->cmap) {
                  wasInstalled = 1;
               }
               JXInstallColormap(display, attr.colormap);
            }
         }
         cp = cp->next;
      }

      if(!wasInstalled && np->cmap != None) {
         JXInstallColormap(display, np->cmap);
      }

   }

}

/** Update callback for clients with the urgency hint set. */
void SignalUrgent(const TimeType *now, int x, int y, Window w, void *data)
{

   ClientNode *np = (ClientNode*)data;

   /* Redraw borders. */
   if(np->state.status & STAT_FLASH) {
      np->state.status &= ~STAT_FLASH;
   } else if(!(np->state.status & STAT_NOTURGENT)) {
      np->state.status |= STAT_FLASH;
   }
   DrawBorder(np);
   RequireTaskUpdate();
   RequirePagerUpdate();

}

/** Unmap a client window and consume the UnmapNotify event. */
void UnmapClient(ClientNode *np)
{
   if(np->state.status & STAT_MAPPED) {
      np->state.status &= ~STAT_MAPPED;
      JXUnmapWindow(display, np->window);
   }
}

