#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "kernel.h"
#include "oslib/filter.h"
#include "module.h"
#include "ftask.h"

/*
static FILE *logfp = NULL;
#define OPEN_LOG() logfp = fopen("<WinSnap$Dir>.log", "w");
#define L(s, ...) if (logfp) fprintf(logfp, s, __VA_ARGS__)
static const os_error *error_check(int line, const os_error *e)
{
  if (e)
    L("Error at line %d: %s\n", line, e->errmess);
  return e;
}
#define E(e) error_check(__LINE__, (e))
*/
#define OPEN_LOG()
#define L(s, ...)
#define E(e) e

static os_error rmload_syntax_error = {0,
	"WinSnap load parameter syntax: [-m margin] [-i] [-p] [-o] [-w]"};

static os_error no_filer_error = {0,
	"WinSnap: Task Manager's handle not found"};

static os_error nomem_error = {0,
	"WinSnap: Not enough memory"};

/*static const char ModuleName[] = "WinSnap";*/
static const char OpenFilterName[] = "WinSnapOpen";
static const char TaskInitFilterName[] = "WinSnapInit";

static osbool open_filter_set = FALSE;
#define OPEN_FILTER_MASK (~(1<<wimp_OPEN_WINDOW_REQUEST))

static wimp_t task_manager = 0;
static osbool taskinit_filter_set = FALSE;
#define TASKINIT_FILTER_MASK \
        (~((1<<wimp_USER_MESSAGE) + (1<<wimp_USER_MESSAGE_RECORDED)))

#define OVERLAP_OFFSET 14

static struct {
  int margin;
  /*
  osbool snap_off_only;
  osbool sticky_only;
  osbool snap_panes;
  */
  osbool snap_to_iconbar;
  osbool overlap_iconbar;
  //osbool snap_to_windows;
} winsnap_options;


static struct {
  int width, height;     /* Converted from pixels to OS units */
  int xeig, yeig;        /* Can be ignored */
  int iconbar_height;
} screen_size;

static void read_screen_size(void)
{
  static const os_vdu_var vars[] = { os_MODEVAR_XWIND_LIMIT,
                                     os_MODEVAR_YWIND_LIMIT,
                                     os_MODEVAR_XEIG_FACTOR,
                                     os_MODEVAR_YEIG_FACTOR,
                                     os_VDUVAR_END_LIST };

  xos_read_vdu_variables((os_vdu_var_list const *) vars, (int *) &screen_size);
  screen_size.width = (screen_size.width + 1) << screen_size.xeig;
  screen_size.height = (screen_size.height + 1) << screen_size.xeig;
  screen_size.iconbar_height = 0;
}

static void deregister_open_filter(void *pw)
{
  if (open_filter_set)
  {
    xfilter_de_register_post_filter(OpenFilterName,
    	(void const *) (int) open_filter_veneer,	/* Yeuch */
    	pw, 0, OPEN_FILTER_MASK);
    open_filter_set = FALSE;
  }
}

static void deregister_taskinit_filter(void *pw)
{
  if (taskinit_filter_set)
  {
    xfilter_de_register_post_filter(TaskInitFilterName,
    	(void const *) (int) taskinit_filter_veneer,	/* Yeuch */
    	pw, task_manager, TASKINIT_FILTER_MASK);
    taskinit_filter_set = FALSE;
  }
}

os_error const *finalise(int fatal, int podule, void *pw)
{
  (void) fatal; (void) podule;

  deregister_open_filter(pw);
  deregister_taskinit_filter(pw);

  return 0;
}

static os_error const *register_open_filter(void *pw)
{
  os_error const *e = xfilter_register_post_filter(OpenFilterName,
  	(void const *) (int) open_filter_veneer,	/* Yeuch */
  	pw, 0, OPEN_FILTER_MASK);
  if (e)
    return e;
  open_filter_set = TRUE;
  return 0;
}

static os_error const *register_taskinit_filter(void *pw)
{
  /* Need to register a filter to catch TaskInitialised events,
     so use Task Manager because that's virtually guaranteed to be running
     at all times and will receive message_TASK_INITIALISE.
  */
  os_error const *e = find_named_task("Task Manager", &task_manager);
  if (e)
    return e;
  if (!task_manager)
    return &no_filer_error;
  e = xfilter_register_post_filter(TaskInitFilterName,
  	(void const *) (int) taskinit_filter_veneer,	/* Yeuch */
  	pw, task_manager, TASKINIT_FILTER_MASK);
  if (e)
    return e;
  taskinit_filter_set = TRUE;
  return 0;
}

/* Checks 'new_outline' against 'edge', and if within winsnap_options.margin
   (which side depends on 'top_right'), and 'new_edge' is further than
   'old_edge', it alters vis0 and vis1 so that 'outline' snaps to 'edge',
   and returns TRUE; returns FALSE if it doesn't snap.
   'top_right' indicates whether a top or right edge is being checked.
*/
static osbool snap_check(int old_outline, int new_outline, int edge,
                         int *vis0, int *vis1, osbool top_right)
{
  int old_dist, new_dist;

  old_dist = old_outline - edge;
  new_dist = new_outline - edge;
  /* Work out whether outline is off screen within margin, but further
  off screen than previous position ie moving off screen */
  if ((top_right && new_dist > 0 && new_dist < winsnap_options.margin &&
                                    old_dist < new_dist) ||
      (!top_right && new_dist < 0 && -new_dist < winsnap_options.margin &&
                                    old_dist > new_dist))
  {
    *vis0 += edge - new_outline;
    *vis1 += edge - new_outline;
    return TRUE;
  }
  return FALSE;
}

/* old_outline and new_outline are the old and new outlines of the window
   being moved, snap_outline is the outline it may want snapping to,
   visible is the window's visible area to be updated. Returns true if
   a horizontal edge was snapped (so snap_to_screen knows whether iconbar
   may also need snapping) */
static osbool snap_to_outline(const os_box *old_outline,
                              const os_box *new_outline,
                              const os_box *snap_outline,
                              os_box *visible)
{
  L("Called snap_to_outline with new_outline %d %d %d %d\n"
    "                           snap_outline %d %d %d %d\n",
    new_outline->x0, new_outline->y0, new_outline->x1, new_outline->y1,
    snap_outline->x0, snap_outline->y0, snap_outline->x1, snap_outline->y1);
  if (!snap_check(old_outline->x0, new_outline->x0, snap_outline->x0,
                  &visible->x0, &visible->x1, FALSE))
  {
    snap_check(old_outline->x1, new_outline->x1, snap_outline->x1,
               &visible->x0, &visible->x1, TRUE);
  }

  if (snap_check(old_outline->y1, new_outline->y1, snap_outline->y1,
                 &visible->y0, &visible->y1, TRUE))
  {
    return TRUE;
  }
  else
  {
    return snap_check(old_outline->y0, new_outline->y0, snap_outline->y0,
                      &visible->y0, &visible->y1, FALSE);
  }
  return FALSE;
}

static void snap_to_screen(const os_box *old_outline, const os_box *new_outline,
                           os_box *visible)
{
  os_box screen_outline = {0, 0, screen_size.width, screen_size.height};

  if (winsnap_options.snap_to_iconbar && !screen_size.iconbar_height)
  {
    wimp_outline ibar_outline;

    ibar_outline.w = wimp_ICON_BAR;
    if (E(xwimp_get_window_outline(&ibar_outline)))
      return;
    screen_size.iconbar_height = ibar_outline.outline.y1 -
                     (winsnap_options.overlap_iconbar ? OVERLAP_OFFSET : 0);
  }

  if (!snap_to_outline(old_outline, new_outline, &screen_outline, visible)
      && winsnap_options.snap_to_iconbar
      && !snap_check(old_outline->y0, new_outline->y0,
                     screen_size.iconbar_height,
                     &visible->y0, &visible->y1, FALSE))
  {
    snap_check(old_outline->y1, new_outline->y1,
               screen_size.iconbar_height,
               &visible->y0, &visible->y1, TRUE);
  }
}

#define WINSNAP_ABS(a) (((a) >= 0) ? (a) : -(a))

#if 0
/* Find whether any new_edges are nearer our_outline than old_edges;
   calculated distances are preserved to pass to next call for efficiency
*/
static void compare_nearer_edges(os_box *old_edges, os_box *distances,
                                 const os_box *our_outline,
                                 const os_box *new_edges)
{
  int dist;

  /* Order of comparisons and use of <= are such that right edges take priority      against our left edges and vice versa, and similarly for vert, and front
     windows take priority over those behind */

  if ((dist = WINSNAP_ABS(new_edges->x1 - our_outline->x0)) <= distances->x0)
  {
    old_edges->x0 = new_edges->x1;
    distances->x0 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->x0 - our_outline->x0)) <= distances->x0)
  {
    old_edges->x0 = new_edges->x0;
    distances->x0 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->x0 - our_outline->x1)) <= distances->x1)
  {
    old_edges->x1 = new_edges->x0;
    distances->x1 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->x1 - our_outline->x1)) <= distances->x1)
  {
    old_edges->x1 = new_edges->x1;
    distances->x1 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->y1 - our_outline->y0)) <= distances->y0)
  {
    old_edges->y0 = new_edges->y1;
    distances->y0 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->y0 - our_outline->y0)) <= distances->y0)
  {
    old_edges->y0 = new_edges->y0;
    distances->y0 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->y0 - our_outline->y1)) <= distances->y1)
  {
    old_edges->y1 = new_edges->y0;
    distances->y1 = dist;
  }
  if ((dist = WINSNAP_ABS(new_edges->y1 - our_outline->y1)) <= distances->y1)
  {
    old_edges->y1 = new_edges->y1;
    distances->y1 = dist;
  }
}

static void snap_to_windows(wimp_w our_handle,
                            const os_box *old_outline,
                            const os_box *new_outline,
                            os_box *visible)
{
  wimp_window_state wstate;
  /* First need to find nearest edges to our window, starting with screen edges
   */
  os_box edges;
  os_box distances = {INT_MAX, INT_MAX, INT_MAX, INT_MAX};

  L("Called snap_to_windows with new_outline %d %d %d %d\n",
    new_outline->x0, new_outline->y0, new_outline->x1, new_outline->y1);
  /* Now start checking, starting with the background */
  wstate.w = wimp_BACKGROUND;
  do
  {
    wimp_outline outline;

    /* First read window state to get next handle and skip panes etc */
    if (E(xwimp_get_window_state(&wstate)))
      return;
    /* Don't try to snap to self */
    if (wstate.w == our_handle)
      continue;
    /* or panes */
    if (wstate.flags & (wimp_WINDOW_PANE | wimp_WINDOW_FOREGROUND_WINDOW))
      continue;
    outline.w = wstate.w;
    if (E(xwimp_get_window_outline(&outline)))
      return;
    if (wstate.w == wimp_ICON_BAR && winsnap_options.overlap_iconbar)
    {
      outline.outline.y1 -= OVERLAP_OFFSET;
    }
    compare_nearer_edges(&edges, &distances, new_outline, &outline.outline);
  }
  while ((wstate.w = wstate.next) != wimp_BACKGROUND);

  edges.x0 = 0;
  edges.y0 = 0;
  edges.x1 = screen_size.width;
  edges.y1 = screen_size.height;

  snap_to_outline(old_outline, new_outline, &edges, visible);
}
#endif /* Comment out -w stuff */

int open_filter_handler(_kernel_swi_regs *r, void *pw)
{
  wimp_window_state old_state;
  wimp_outline old_outline;
  os_box new_outline;
  wimp_open *open_event = (wimp_open *) r->r[1];

  (void) pw;

  if (r->r[0] != wimp_OPEN_WINDOW_REQUEST)
    return 1;

  /* First get window's old position and flags */
  old_state.w = open_event->w;
  if (E(xwimp_get_window_state(&old_state)))
    return 1;

  /* Don't snap panes or nested windows (I think) or when open event was
     caused by toggle */
  if (/* winsnap_options.snap_panes && */ (old_state.flags &
      (wimp_WINDOW_PANE | wimp_WINDOW_FOREGROUND_WINDOW | wimp_WINDOW_TOGGLED)))
  {
    return 1;
  }

  /* Find window's new outline by reading its old outline and applying the
     difference between old and new visible areas */
  old_outline.w = open_event->w;
  if (E(xwimp_get_window_outline(&old_outline)))
    return 1;
  new_outline.x0 = old_outline.outline.x0 + open_event->visible.x0 -
                   old_state.visible.x0;
  new_outline.y0 = old_outline.outline.y0 + open_event->visible.y0 -
                   old_state.visible.y0;
  new_outline.x1 = old_outline.outline.x1 + open_event->visible.x1 -
                   old_state.visible.x1;
  new_outline.y1 = old_outline.outline.y1 + open_event->visible.y1 -
                   old_state.visible.y1;
/*
  if (winsnap_options.snap_to_windows)
  {
    snap_to_windows(open_event->w, &old_outline.outline, &new_outline,
                    &open_event->visible);
  }
  else
  {
*/
  snap_to_screen(&old_outline.outline, &new_outline,
                   &open_event->visible);
/*
  }
*/

  return 1;	/* Behave as if we haven't claimed an interrupt
  		   to make veneer return MOVS pc, lr */
}

int taskinit_filter_handler(_kernel_swi_regs *r, void *pw)
{
  /*static wimp_t last_task = 0;*/
  wimp_block *e = (wimp_block *) r->r[1];
  if ((r->r[0] == wimp_USER_MESSAGE || r->r[0] == wimp_USER_MESSAGE_RECORDED)
      && e->message.action == message_TASK_INITIALISE
      /*&& e->message.sender != last_task*/)
  {
    /*last_task = e->message.sender;*/
    deregister_open_filter(pw);
    register_open_filter(pw);
  }
  return 1;	/* Behave as if we haven't claimed an interrupt
  		   to make veneer return MOVS pc, lr */
}

/* Valid options are:
   -m margin width
   // -s snap off only
   // -t sticky only
   // -p snap panes
   -i snap to iconbar
   -o overlap icon bar
   -w snap to other windows
*/
#define MAXINITARGS 4

const os_error *initialise(const char *cmd_tail, int len, void *pw)
{
  char *args;
  int argc;
  char *argv[MAXINITARGS];
  int i;
  const os_error *e;

  OPEN_LOG();

  winsnap_options.margin = 28;
  /*
  winsnap_options.snap_off_only = FALSE;
  winsnap_options.sticky_only = FALSE;
  winsnap_options.snap_panes = FALSE;
  */
  winsnap_options.snap_to_iconbar = FALSE;
  winsnap_options.overlap_iconbar = FALSE;
  //winsnap_options.snap_to_windows = FALSE;

  /* Make writable version of cmd_tail */
  len = strlen(cmd_tail);
  args = malloc(len + 1);
  if (!args)
    return &nomem_error;
  memcpy(args, cmd_tail, len + 1);

  /* Split up args into argv */
  argc = 0;
  for (i = 0; i < len; ++i)
  {
    while (isspace(args[i])) ++i;
    if (!iscntrl(args[i]) && argc == MAXINITARGS)
    {
      return &rmload_syntax_error;
    }
    argv[argc++] = args + i;
    while (i < len && !isspace(args[i])) ++i;
    args[i] = 0;
  }

  /* Process argv */
  for (i = 0; i < argc; ++i)
  {
    if (argv[i][0] == '-')
    {
      if (argv[i][2])
        return &rmload_syntax_error;
      switch (argv[i][1])
      {
        case 'm':
          if (++i == argc)
            return &rmload_syntax_error;
          winsnap_options.margin = atoi(argv[i]);
          break;
        /*
        case 's':
          winsnap_options.snap_off_only = TRUE;
          break;
        case 't':
          winsnap_options.sticky_only = TRUE;
          break;
        case 'p':
          winsnap_options.snap_panes = TRUE;
          break;
        */
        case 'i':
          winsnap_options.snap_to_iconbar = TRUE;
          break;
        case 'o':
          winsnap_options.overlap_iconbar = TRUE;
          break;
/*
        case 'w':
          winsnap_options.snap_to_windows = TRUE;
          break;
*/
        default:
          return &rmload_syntax_error;
      }
    }
    else if (argv[i][0])
      return &rmload_syntax_error;
  }
  free(args);
  /*
  if (winsnap_options.sticky_only && winsnap_options.snap_only)
    winsnap_options.sticky_only = FALSE;
  */

  read_screen_size();

  e = register_taskinit_filter(pw);
  if (e)
    return e;
  e = register_open_filter(pw);
  if (e)
    deregister_taskinit_filter(pw);
  return e;
}

void service_handler(int sn, _kernel_swi_regs *r, void *pw)
{
  (void) r;

  switch (sn)
  {
    case Service_FilterManagerInstalled:
      if (!taskinit_filter_set)
        register_taskinit_filter(pw);
      if (!open_filter_set)
        register_open_filter(pw);
      break;
    case Service_FilterManagerDying:
      taskinit_filter_set = FALSE;
      open_filter_set = FALSE;
      break;
    case Service_ModeChange:
      read_screen_size();
      break;
  }
}
