 /*-
 * Copyright (c) 2001, 2002 Jordan DeLong
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the author nor the names of contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
#include "wm.h"

/* list of clients */
clientlist_t   client_list = LIST_HEAD_INITIALIZER(&client_list);

/* currently focused client */
client_t *client_focused;

/*
 * shutdown client stuff; unload all client_t's and reparent all clients
 * back to the root window.
 */
void client_shutdown()
{
   client_t *client;

   /* remove all clients */
   while (!LIST_EMPTY(&client_list))
   {
      client = LIST_FIRST(&client_list);
      if (!client->flags.internal)
      {
         XReparentWindow(display, client->window, client->screen->root,
                         client->x, client->y);
         /*
          * XXX: should save their initial border
          * instead of just setting it back to 1.
          */
         XSetWindowBorderWidth(display, client->window, 1);
      }

      client_rm(client);
   }
}

/*
 * XWMHints structure gets handled here; startup state and
 * if the client wants to be able to recieve input focus.
 */
static __inline void client_init_wmhints(client_t *client)
{
   Atom type;
   u_long items, bytes;
   long *state;
   int fmt;

   /*
    * start it up in the state it thinks it's in, or use the state hint
    * in the XWMHints structure.
    */
   XGetWindowProperty(display, client->window, WM_STATE, 0, 1, 0, WM_STATE,
                      &type, &fmt, &items, &bytes, (u_char **) &state);
   if (type == WM_STATE)
   {
      client->state = *state;
      XFree(state);
   }
   else if (client->wmhints && client->wmhints->flags & StateHint)
   {
      client->state = client->wmhints->initial_state;
   }
   else
   {
      client->state = WithdrawnState;
   }

   /* flag noinput windows as nofocus */
   if (client->wmhints && client->wmhints->flags & InputHint)
   {
      if (!client->wmhints->input)
      {
         client->flags.nofocus = 1;
      }
   }
}

#define CLIENT_EVENTS ( StructureNotifyMask | PropertyChangeMask | \
                        EnterWindowMask | LeaveWindowMask | ColormapChangeMask | \
                        FocusChangeMask | VisibilityChangeMask | \
                        ExposureMask    | ButtonReleaseMask | ButtonPressMask )

/* add decoration for a client during client_add */
static __inline void client_decorate(client_t *client, dgroup_t *dgroup)
{
   XSetWindowAttributes attr;

   /*
    * figure out what decoration group to use, unless
    * a hints handler (from a plugin) set it already
    */
   if( !client->dgroup )
   {
      if (dgroup)
         client->dgroup = dgroup;
      else if (client->flags.transient)
         client->dgroup = options.dgroup_trans;
      else if (client->flags.internal)
         client->dgroup = options.dgroup_internal;
      else
         client->dgroup = options.dgroup_default;
   }

   attr.override_redirect = 1;
   attr.background_pixel = BlackPixel(display, client->screen->num);

   client->frame = XCreateWindow( display, client->screen->root, client->x, client->y,
                                  FULLWIDTH(client), FULLHEIGHT(client), 0, CopyFromParent, CopyFromParent,
                                  CopyFromParent, CWOverrideRedirect | CWBackPixel, &attr);


   XSetWindowBorderWidth(display, client->window, 0);

   /* actual decor window creation (decor_decorate) posponed 'till later in add */
   decor_decorate(client);
   client_shape(client);
}

/* grab input on our decoration for the client */
static __inline void client_dograbs(client_t *client)
{
   XSetWindowAttributes attr;
   long mask;

   mask = SubstructureNotifyMask | SubstructureRedirectMask;
   switch (options.focus)
   {
      case FOCUS_CLICK:
         if (!client->flags.nofocus)
            XGrabButton(display, AnyButton, AnyModifier, client->frame,
                        1, ButtonPressMask, GrabModeSync, GrabModeAsync, None, None);
         else
            buttongrabhack(client->frame, AnyButton, options.mouse_modifier);
         break;
      case FOCUS_POINTER:
         mask |= LeaveWindowMask;
      case FOCUS_SLOPPY:
         mask |= EnterWindowMask;
         buttongrabhack(client->frame, AnyButton, options.mouse_modifier);
         break;
   }
   XSelectInput(display, client->frame, mask);
   XShapeSelectInput(display, client->window, ShapeNotifyMask);

   /* prevent us from recieving some unneccessary events */
   attr.do_not_propagate_mask = ButtonPressMask | ButtonReleaseMask;
   // bva
   attr.event_mask = CLIENT_EVENTS;
   // end bva

   mask = CWEventMask | CWDontPropagate;
   XChangeWindowAttributes(display, client->window, mask, &attr);
}


/*
 * add a client.
 * flags is the flags for the client, or NULL if we use defaults.
 * not all of the flags passed in will be honored: we decide here
 * if it's a transient, etc.  if plugin is not NULL and it's an internal
 * it means this should be associated with that plugin as an internal
 * window.  dgroup is the dgroup to use, otherwise we will try to figure
 * out for ourselves and use dgroups based on if it is a transient/internal,
 * and so forth.
 */
client_t *client_add( screen_t *screen,
                      Window w,
                      clientflags_t *flags,
                      dgroup_t *dgroup )
{
   XWindowAttributes win_attr;
   Window dumwin;
   client_t *client = NULL;
   long supplied;
#ifdef I18N
   XTextProperty text_prop;
   char **cl;
   int n;
#endif

   log_enter( " window: 0x%x", w );

   /*
    * we need to do a grab so that it's not possible for the window
    * we are about to manage to disappear halfway through the process
    * of us preparing to manage it.
    */
   XGrabServer(display);

   /* if we can't get window attributes, give up */
   if (!XGetWindowAttributes(display, w, &win_attr))
      goto done;

   /* get mem and set flags */
   client = calloc(1, sizeof(client_t));
   if (!client)
      goto done;
   if (flags)
      memcpy(&client->flags, flags, sizeof(clientflags_t));

   /* fill in client_t stuff */
   client->window = w;
   client->screen = screen;

   client->initial_attr.desktop = screen->desktop; // bva. current desktop by default.
   client->initial_attr.viewport_x = 0;
   client->initial_attr.viewport_y = 0;

   client->x = win_attr.x;
   client->y = win_attr.y;
   client->last_width = client->width = win_attr.width;
   client->last_height = client->height = win_attr.height;

   log( 2, "initial x: %d y: %d",
        client->x,
        client->y );
   log( 2, "initial viewx: %d viewy: %d",
        client->initial_attr.desktop->viewx,
        client->initial_attr.desktop->viewy );

   /* shading information */
   client->doubleclick.last_click_time = -1;
   client->doubleclick.last_click_button = 0;
   client->flags.shaded = 0;

   /* default stacking layer to normal */
   client->stacklayer = STACKLAYER_NORMAL;

   /* get PropertyChange information */
   if( !client->flags.internal )
   {
      XSelectInput(display, client->window, PropertyChangeMask );
   }

   /* get hints and do stuff that they require */
   client->wmhints = XGetWMHints(display, client->window);
   XGetWMNormalHints(display, client->window, &client->normalhints, &supplied);
   XGetClassHint(display, client->window, &client->classhint);
#ifdef I18N
   if (XGetWMName(display, client->window, &text_prop) != 0)
   {
      if (text_prop.encoding == XA_STRING)
      {
         client->store_name = (char *)text_prop.value;
      }
      else
      {
         XmbTextPropertyToTextList(display, &text_prop, &cl, &n);
         if (cl)
         {
            client->store_name = strdup(cl[0]);
            XFreeStringList(cl);
         }
         else
         {
            client->store_name = strdup("NoName");
         }
      }
   }
   else
   {
      client->store_name = strdup("NoName");
   }
#else
   XFetchName(display, client->window, &client->store_name);
#endif
   if (XGetTransientForHint(display, client->window, &dumwin))
      client->flags.transient = 1;

   /* do window manager hints, and decorate */
   client_init_wmhints(client);
   plugin_init_hints(client);

   log( 2, "initial viewport: %d %d\n",
        client->initial_attr.viewport_x,
        client->initial_attr.viewport_y );

   if( client->initial_attr.viewport_x > 0 )
   {
      client->x = ( client->x +
                    client->initial_attr.viewport_x * client->screen->width );
   }
   if( client->initial_attr.viewport_y > 0 )
   {
      client->y =
          (client->y + client->initial_attr.viewport_y * client->screen->height);
   }

   // bva. some of this stuff should be optional based on whether or not
   // we are still on the same workspace ....
   client_decorate(client, dgroup);

   /*
    * put this client_t into client_context for this
    * client's window and frame.
    */
   /* printf( "...... saving client 0x%x to window 0x%x and frame 0x%x\n",
      (int) client, (int) client->window, (int) client->frame  ); */

   XSaveContext(display, client->window, client_context, (XPointer) client);
   XSaveContext(display, client->frame, client_context, (XPointer) client);

   /* select input and then reparent the client to it's frame window */
   client_dograbs(client);

   // bva ....
   XSelectInput( display, client->window, CLIENT_EVENTS & ~StructureNotifyMask);
   // end bva

   XReparentWindow( display, client->window, client->frame,
                    client->dgroup->left_space,
                    client->dgroup->top_space );

   // bva ....
   XSelectInput( display, client->window, CLIENT_EVENTS );
   // end bva

   client_gravitate(client);

   /* lower the client in the frame window so decoration can cover it */
   XLowerWindow(display, client->window);

   /* link it up to the all-clients list */
   LIST_INSERT_HEAD(&client_list, client, c_list);

   /* save non internals */
   if (!client->flags.internal)
      XAddToSaveSet(display, client->window);

  done:
   /* ungrab the server */
   XUngrabServer(display);

   if( client )
   {
      client->speed = 1;
   }

   log_exit( "client 0x%x associated with window: 0x%x",
             (int) client, (int) client->window );

   return client;
}

/* remove a client */
void client_rm(client_t *client)
{
   log_enter( "client 0x%x", (int) client );


   /*
    * eliminate decoration windows and set dgroup to dgroup_empty
    * before removing from the workspace/desktop because if this
    * window is focused, removing it from the desktop will attempt
    * to unfocus all of it's decoration windows (change their pixmaps
    * to the nonfocus ones) which is a waste of time.
    */

   decor_undecor(client);

   client->dgroup = &dgroup_empty;

   /* free stuff allocated by Xlib for this client */
   if (client->wmhints)             XFree(client->wmhints);
   if (client->classhint.res_name)  XFree(client->classhint.res_name);
   if (client->classhint.res_class) XFree(client->classhint.res_class);
   if (client->store_name)          XFree(client->store_name);

   /* kill the frame window and remove entries in client_context */

   XDeleteContext(display, client->frame, client_context);
   XDeleteContext(display, client->window, client_context);
   XDestroyWindow(display, client->frame);

   /* remove from workspace and desktop */
   if (client->workspace)
   {
      log(5,"removing the client from the list");

      desktop_rm_client(client);
      workspace_rm_client(client);
   }

   LIST_REMOVE(client, c_list);

   // signal the plugins .. bva. moved from event.c()
   // bva. put it back in event.c ... see if it causes problems.
   //log(2,"calling plugin_window_death()");
   //plugin_window_death(client);

   /* free managed client memory */
   free(client);

   log_exit( "" );
}

/* remove a client */
void client_tentatively_rm(client_t *client)
{
   log_enter( "client 0x%x", (int) client );

   /* we dont want events any more */
   XSelectInput(display, client->window, NoEventMask);
   XSelectInput(display, client->frame, NoEventMask);

   /*
    * eliminate decoration windows and set dgroup to dgroup_empty
    * before removing from the workspace/desktop because if this
    * window is focused, removing it from the desktop will attempt
    * to unfocus< all of it's decoration windows (change their pixmaps
    * to the nonfocus ones) which is a waste of time.
    */

   decor_undecor(client);

   client->dgroup = &dgroup_empty;

   /* remove from workspace and desktop */
   if (client->workspace)
   {
      log(5,"removing the client from the list");

      desktop_rm_client(client);
      workspace_rm_client(client);
   }

   LIST_REMOVE(client, c_list);

   client->flags.tentatively_removed = 1;

   log_exit( "" );
}

client_t *client_readd( client_t *client )
{
#ifdef I18N
   XTextProperty text_prop;
   char **cl;
   int n;
#endif

   grab_server( 1 );

   XMapWindow( display, client->window );
//   client_map( client, 0 );


   grab_server( 0 );


   log_enter( " window: 0x%x", client->window );

   client->flags.tentatively_removed = 0;
   client->flags.unmap_from_reparent = 1;

   if (!client->flags.internal)
   {
      XAddToSaveSet(display, client->window);
   }

   client_dograbs(client);


   // bva. some of this stuff should be optional based on whether or not
   // we are still on the same workspace ....
   client_decorate(client, NULL);

   // bva ....
   XSelectInput( display, client->window, CLIENT_EVENTS & ~StructureNotifyMask);
   // end bva

   XReparentWindow( display, client->window, client->frame,
                    client->dgroup->left_space,
                    client->dgroup->top_space );

   // bva ....
   XSelectInput( display, client->window, CLIENT_EVENTS );
   // end bva


   client_gravitate(client);

   /* lower the client in the frame window so decoration can cover it */
   XLowerWindow(display, client->window);

   /* link it up to the all-clients list */
   LIST_INSERT_HEAD(&client_list, client, c_list);

   /* ungrab the server */
   XUngrabServer(display);

   return client;
}


/* called by map_request and by manage_existing_windows (screen.c) */
void client_map(client_t *client, int allowplace)
{
   /*
    * no matter what state, we map it's
    * top window
    */
   log_enter( "" );
   log( 2, "screen's current desk: %x",
        client->screen->desktop );
   log( 2, "mapping window: 0x%x to desk: 0x%x",
        client->window,
        client->initial_attr.desktop );

   XMapWindow(display, client->window );

   /*
    * to prevent losing clients if you restart after
    * changing desktop dims
    */
   if ( client->x + FULLWIDTH(client) < 0 ||
        client->y + FULLHEIGHT(client) < 0 ||
        client->x >= (client->screen->desktop->width
                      - client->screen->desktop->viewx) * client->screen->width ||
        client->y >= (client->screen->desktop->height
                      - client->screen->desktop->viewy) * client->screen->width )
   {
      // hmmm. need a flag to say that this window was "externally" placed that way
      // this check may work in the future, but who really cares ... eh ?
      // bva.


      // client->y = client->x = 0;


      ////// aaaaaaaaaaaa right here ...............
      // if we set the x/y of the client to a more "global" sense, this bitch
      // gets confused and moves the window off the screen.

      // if I take it out, we don't get anything drawn at all .... but why!
      // when we start over the same fucking x/y work ...

      //client_sizeframe(client);
   }

   /* put it into the state it wants to be in */
   switch (client->state)
   {
      case IconicState:
         /* client starts as an icon */
         client_setstate(client, IconicState);
         plugin_iconify_notify(client);
         break;
      case WithdrawnState:
      case NormalState:
      default:
         if (allowplace)
         {
            placement_place(client);
            client_sizeframe(client);
         }
         else
         {
            /*
             * if placement is not allowed, it means that we are
             * probably running on a window that existed before
             * the window manager was started; this means the
             * unmap_from_reparent flag will still be set.
             * If the flag isn't set, we still do a birth
             * animation.
             */
            if (!client->flags.unmap_from_reparent)
            {
               plugin_anim_birth(client);
            }
         }

         /*
          * add to the workspace appropriate for the client's
          * position on the screen.
          */
         log( 2, "mapping client to x: %d y: %d",
              client->x, client->y );

         if( client->initial_attr.desktop != client->screen->desktop )
         {
            log( 2, "adding to the remote workspace on remote desktop");

            /*  if( client->initial_viewportx != 0 || */
/*                    client->initial_viewporty != 0 ) */
/*                { */
/*                   workspace_add_by_index(client->desktop, client, */
/*                                          client->initial_viewportx, */
/*                                          client->initial_viewporty ); */
/*                } */
/*                else */
/*                { */
            workspace_add_bypos(client->initial_attr.desktop, client);

            /*  } */

            stacking_raise(client);
            client_setstate(client, NormalState);

            action_send_config(client);

            plugin_window_birth(client);
         }
         else
         {
            log( 2, "adding to the current workspace on current desktop");
            /* if( client->initial_viewportx != 0 || */
/*                    client->initial_viewporty != 0 ) */
/*                { */
/*                    workspace_add_by_index( client->desktop, client, */
/*                                            client->initial_viewportx, */
/*                                            client->initial_viewporty ); */
/*                } */
/*                else */
/*                { */
            workspace_add_bypos(client->screen->desktop, client);
            /*  } */

            if( client->x > client->screen->width ||
                client->y > client->screen->height )
            {
               // nothing we are out of sight!
               XMapWindow(display, client->frame);
            }
            else
            {
               /* the window gets focus in map_notify */
               XMapWindow(display, client->frame);
               stacking_raise(client);
               client_setstate(client, NormalState);
            }

            /*
             * send configure to the client to make sure it
             * knows where it is, and notify plugins.
             */

            action_send_config(client);

            plugin_window_birth(client);
         }
         break;

   }

   log_exit( "" );
}

/* shape a client */
void client_shape(client_t *client)
{
   XRectangle *rect = NULL;
   XRectangle r1;
   int n, order;

   rect = XShapeGetRectangles(display, client->window, ShapeBounding, &n, &order);
   if (n > 1)
   {
      r1.x = 0;
      r1.y = 0;
      r1.width = client->width;
      r1.height = client->height;
      XShapeCombineRectangles(display, client->frame, ShapeBounding,
                              client->dgroup->left_space, client->dgroup->top_space,
                              &r1, 1, ShapeSubtract, Unsorted);
      XShapeCombineShape(display, client->frame, ShapeBounding,
                         client->dgroup->left_space, client->dgroup->top_space,
                         client->window, ShapeBounding, ShapeUnion);
      client->flags.shaped = 1;
   }
   else if (client->flags.shaped)
   {
      r1.x = 0;
      r1.y = 0;
      r1.width = client->width;
      r1.height = client->height;
      XShapeCombineRectangles(display, client->frame, ShapeBounding,
                              client->dgroup->left_space, client->dgroup->top_space,
                              &r1, 1, ShapeUnion, Unsorted);
      client->flags.shaped = 0;
   }

   XFree(rect);
}

/*
 * adjust client geometry according to the gravity settings
 * specified in the client normalhints.
 */
void client_gravitate(client_t *client)
{
   static point_t gravoffs[] =
       {
          {  0,  0}, /* ForgetGravity */
          { -1, -1}, /* NorthWestGravity */
          {  0, -1}, /* NorthGravity */
          {  1, -1}, /* NorthEastGravity */
          { -1,  0}, /* WestGravity */
          {  0,  0}, /* CenterGravity */
          {  1,  0}, /* EastGravity */
          { -1,  1}, /* SouthWestGravity */
          {  0,  1}, /* SouthGravity */
          {  1,  1}, /* SouthEastGravity */
          {  0,  0}  /* StaticGravity */
       };
   point_t *offs;

   /* make sure we can adjust this for gravity */
   if (!(client->normalhints.flags & PWinGravity))
      return;
   if (client->normalhints.win_gravity < 0
       || client->normalhints.win_gravity > StaticGravity)
      return;

   /* adjust the geometry to correspond to gravity settings */
   offs = &gravoffs[client->normalhints.win_gravity];
   if (offs->x > 0)
   {
      client->x -= DWIDTH(client);
      log( 6, "^^^^^^^^ changing clientx to: %d", client->x );

   }
   else if (offs->x == 0)
   {
      client->x -= client->dgroup->left_space;
      log( 6, "^^^^^^^^ changing clientx to: %d", client->x );
   }
   if (offs->y > 0)
      client->y -= DHEIGHT(client);
   else if (offs->y == 0)
      client->y -= client->dgroup->top_space;
}

/* resize the decoration and such for a client: reshape, etc. */
void client_sizeframe(client_t *client)
{
   dgroup_t *dgroup;
   int x, y;

   x = client->x;
   y = client->y;

   if( client->width != client->last_width ||
       client->height != client->last_height )
   {
      dgroup = client->dgroup;

      XMoveResizeWindow( display,
                         client->frame,
                         x,
                         y,
                         FULLWIDTH(client),
                         FULLHEIGHT(client) );
      XResizeWindow(display, client->window, client->width, client->height);

      decor_shapesize(client);

      if (client->flags.shaped)
      {
         client_shape(client);
      }

      client->last_width = client->width;
      client->last_height = client->height;
   }
   else
   {
      XMoveWindow(display, client->frame, x, y);
   }
}

/* set the state for a window (our var and the WM_STATE) */
void client_setstate(client_t *client, int state)
{
   u_long data[2];

   data[0] = state;
   data[1] = None;

   XChangeProperty( display, client->window, WM_STATE, WM_STATE, 32,
                    PropModeReplace, (u_char *) data, 2 );

   client->state = state;

   XSync(display, 0);
}
