/* Simple BBC News ticker client
 * (c) Darren Salt
 * GPL applies
 * $Id: configure.c,v 1.23 2007/04/25 14:11:15 ds Exp $
 */

/* System includes */

#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <swis.h>
#include <limits.h>

/* Program includes */

#include "globals.h"
#include "fetchers.h"
#include "wimp.h"
#include "wimpmenu.h"
#include "util.h"


#define DEFAULT_SERVERS "<Ticker$Dir>.DefServers"
#define CUSTOM_SERVERS "Choices:ymbj.Ticker.Servers"

const struct MENU_ENTRIES (16) colour_menu =
{
  "Colours", 0x70207, 64, 44, 0,
  {
    {
    0, -1, 0x07000021, "White"}
    ,
    {
    0, -1, 0x17400021, "Grey 1"}
    ,
    {
    0, -1, 0x27000021, "Grey 2"}
    ,
    {
    0, -1, 0x37000021, "Grey 3"}
    ,
    {
    0, -1, 0x40000021, "Grey 4"}
    ,
    {
    0, -1, 0x50000021, "Grey 5"}
    ,
    {
    0, -1, 0x60000021, "Grey 6"}
    ,
    {
    0, -1, 0x70000021, "Black"}
    ,
    {
    0, -1, 0x80000021, "Blue"}
    ,
    {
    0, -1, 0x97000021, "Yellow"}
    ,
    {
    0, -1, 0xA0000021, "Green"}
    ,
    {
    0, -1, 0xB0000021, "Red"}
    ,
    {
    0, -1, 0xC7000021, "Cream"}
    ,
    {
    0, -1, 0xD0000021, "Olive"}
    ,
    {
    0, -1, 0xE7000021, "Orange"}
    ,
    {
    128, -1, 0xF7000021, "Cyan"}
  }
};


/* Enumeration labels and their corresponding values.
 * Each group has an entry in enum[].
 */

struct enum_info
{
  const char *label;
  int value;
};

static const struct enum_info pause_types[] = {
  {"never", pause_NEVER},
  {"pointer", pause_POINTER},
  {"highlight", pause_HIGHLIGHT},
  {0}
};

static const struct enum_info init_types[] = {
  {"open", start_OPEN},
  {"minimised", start_MINIMISED},
  {"iconised", start_MINIMISED},
  {"closed", start_CLOSED},
  {0}
};

static const struct enum_info open_types[] = {
  {"above iconbar", open_ABOVE_ICONBAR},
  {"at top", open_AT_TOP},
  {0}
};

static const struct enum_info align_types[] = {
  {"left", open_ALIGN_LEFT},
  {"centre", open_ALIGN_CENTRE},
  {"right", open_ALIGN_RIGHT},
  {0}
};

static const struct enum_info *enums[] =
  { pause_types, init_types, open_types, align_types };

static int config_pane = -1;


/* A few necessary declarations... */

static void read_bool_option (struct options *myopt, const char *, int);
static void read_string_option (struct options *myopt, const char *, int);
static void read_int_option (struct options *myopt, const char *, int);
static void read_enum_option (struct options *myopt, const char *, int);

static void write_bool_option (const struct options *myopt, FILE *, int);
static void write_string_option (const struct options *myopt, FILE *, int);
static void write_int_option (const struct options *myopt, FILE *, int);
static void write_enum_option (const struct options *myopt, FILE *, int);


#define MAX_SCROLL_RATE 10
#define MAX_FAST_SCROLL_RATE 15

/* tokens[] describes the available configuration options.
 * It gives the option names, their read/write functions and offsets in
 * struct options, and some arguments.
 *	Bool	arg[] = unused
 *	String	arg[] = max length
 *	Int	arg[] = sizeof (option), min value, max value; UNSIGNED
 *	Enum	arg[] = sizeof (option), index in enums[]
 *	Server  arg[] = unused
 */

struct token
{
  const char *token;
  void (*read) (struct options *, const char *, int);
  void (*write) (const struct options *, FILE *, int);
  int offset;
  int arg[3];
};

const struct token tokens[] = {
  {"Use proxy", read_bool_option, write_bool_option,
   offsetof (struct options, use_proxy)},
  {"Proxy", read_string_option, write_string_option,
   offsetof (struct options, proxy), {MAX_PROXY_LENGTH}},
  {"Proxy port", read_int_option, write_int_option,
   offsetof (struct options, proxyport),
   {sizeof (opt.proxyport), 1, 65535}
   }
  ,
  {"Update", read_bool_option, write_bool_option,
   offsetof (struct options, update)},
  {"Update every", read_int_option, write_int_option,
   offsetof (struct options, update_every),
   {sizeof (opt.update_every), 1, 999}
   }
  ,
  {"Update time in RSS", read_bool_option, write_bool_option,
   offsetof (struct options, use_doc_update)},
  {"Update iconised", read_bool_option, write_bool_option,
   offsetof (struct options, iconised_update)},
  {"Update if set", read_string_option, write_string_option,
   offsetof (struct options, connect_var), {MAX_CONNECT_VAR_LENGTH}},
  {"Scroll rate", read_int_option, write_int_option,
   offsetof (struct options, scroll_rate),
   {sizeof (opt.scroll_rate), 1, MAX_SCROLL_RATE}
   }
  ,
  {"Fast scroll rate", read_int_option, write_int_option,
   offsetof (struct options, fast_scroll_rate),
   {sizeof (opt.fast_scroll_rate), 1, MAX_FAST_SCROLL_RATE}
   }
  ,
  {"Pause", read_enum_option, write_enum_option,
   offsetof (struct options, pause_when),
   {sizeof (opt.pause_when), 0}
   }
  ,
  {"Highlight colour", read_int_option, write_int_option,
   offsetof (struct options, highlight),
   {sizeof (opt.highlight), 0, 15}
   }
  ,
  {"Auto-show description", read_bool_option, write_bool_option,
   offsetof (struct options, auto_desc)},
  {"To front on update", read_bool_option, write_bool_option,
   offsetof (struct options, update_to_front)},

  {"First open update", read_bool_option, write_bool_option,
   offsetof (struct options, delay_init_fetch)},
  {"Initial state", read_enum_option, write_enum_option,
   offsetof (struct options, initial_state),
   {sizeof (opt.initial_state), 1}
   }
  ,

  {"Width", read_int_option, write_int_option,
   offsetof (struct options, width),
   {sizeof (opt.width), 0, 9999}
   }
  ,
  {"Width is %age", read_bool_option, write_bool_option,
   offsetof (struct options, width_is_percentage)},
  {"Display less width", read_bool_option, write_bool_option,
   offsetof (struct options, less_than_display)},
  {"Open", read_enum_option, write_enum_option,
   offsetof (struct options, open_at),
   {sizeof (opt.open_at), 2}
   }
  ,
  {"Align", read_enum_option, write_enum_option,
   offsetof (struct options, open_align),
   {sizeof (opt.open_align), 3}
   }
  ,
  {"Offset X", read_int_option, write_int_option,
   offsetof (struct options, offset.x),
   {sizeof (opt.offset.x), -9999, 9999}
  }
  ,
  {"Offset Y", read_int_option, write_int_option,
   offsetof (struct options, offset.y),
   {sizeof (opt.offset.y), -9999, 9999}
  }
  ,
  {"On top", read_bool_option, write_bool_option,
   offsetof (struct options, at_top)},

  {"No iconbar icon", read_bool_option, write_bool_option,
   offsetof (struct options, no_ibar_icon)},

  {"Low graphics", read_bool_option, 0,
   offsetof (struct options, low_graphics)},

  {"Default server", read_string_option, write_string_option,
   offsetof (struct options, selected_server), {MAX_SERVER_LABEL_LENGTH}},

  {0}
};


/* Local function declarations */

static void merge_servers (FILE *, int replace);


/* Set the default configuration. */

static void
config_default (struct options *myopt)
{
  myopt->use_proxy = 0;
  myopt->proxy[0] = 0;
  myopt->proxyport = 0;

  myopt->iconised_update = 1;
  myopt->update = 1;
  myopt->update_every = 15;
  myopt->use_doc_update = 1;
  myopt->connect_var[0] = 0;

  myopt->scroll_rate = 1;
  myopt->fast_scroll_rate = 3;
  myopt->pause_when = pause_NEVER;
  myopt->highlight = 8;
  myopt->auto_desc = 0;
  myopt->update_to_front = 1;

  myopt->delay_init_fetch = 1;
  myopt->initial_state = start_OPEN;

  myopt->width = 100;
  myopt->width_is_percentage = 1;
  myopt->less_than_display = 0;
  myopt->open_at = open_AT_TOP;
  myopt->open_align = open_ALIGN_LEFT;
  myopt->offset.x = 0;
  myopt->offset.y = 0;
  myopt->at_top = 0;
  myopt->no_ibar_icon = 0;

  myopt->low_graphics = 0;

  strcpy (myopt->selected_server, fetcher_bbc.label);
}


static int
menucmp (const void *p1, const void *p2)
{
  int i;
  if (((struct menu_entry *)p1)->submenu != ((struct menu_entry *)p2)->submenu)
  {
    /* pull group names to top */
    if (((struct menu_entry *)p1)->submenu == -1)
      return 1;
    if (((struct menu_entry *)p2)->submenu == -1)
      return -1;
  }
  i = stricmp (((const char **) p1)[3], ((const char **) p2)[3]);
  return i ? i : strcmp (((const char **) p1)[3], ((const char **) p2)[3]);
}


void
create_servers_menu (const char *group, const char *label)
{
  int i, j;
  int server = find_server (label);
  base_menu_type *menu;
  static int topserver;

  if (!group)
  {
    static int oldsize = -1;
    if (oldsize != num_servers)
    {
      oldsize = num_servers;
      servers_menu = realloc (servers_menu, 28 + 24 * num_servers);
      if (!servers_menu)
      {
	oldsize = -1;
	report_error ("Out of memory");
	return;
      }
    }
    menu = servers_menu;
    topserver = server;
  }
  else
  {
    static int oldsize = -1;
    if (oldsize != num_servers)
    {
      oldsize = num_servers;
      servers_submenu = realloc (servers_submenu, 28 + 24 * num_servers);
      if (!servers_submenu)
      {
	oldsize = -1;
	report_error ("Out of memory");
	return;
      }
    }
    menu = servers_submenu;
    server = topserver;
  }

  strcpy (menu->title, "Tickers");
  menu->colours = 0x70207;
  menu->height = 44;
  menu->gap = 0;

  j = 0;

  /* List tickers in the given group (may be null) */
  for (i = 0; i < num_servers; ++i)
    if (!strcmp (servers[i].group, group ? group : ""))
    {
      menu->items[j].menuflags = (i == server);
      menu->items[j].submenu = -1;
      menu->items[j].icon_flags = 0x07000121;
      menu->items[j].text.it.text = servers[i].label;
      menu->items[j].text.it.validation = (char *) -1;
      menu->items[j].text.it.text_len = strlen (servers[i].label);
      j++;
    }

  /* List ticker groups (if we've been given the null group) */
  if (!group)
    for (i = 0; i < num_servers; ++i)
    {
      int k = -1;
      if (!servers[i].group[0])
        continue;
      while (++k < j)
	if (!strcmp (servers[i].group, menu->items[k].text.it.text))
	  break;
      if (k == j)
      {
	menu->items[j].menuflags = 8;
	menu->items[j].submenu = 2;
	menu->items[j].icon_flags = 0x07000121;
	menu->items[j].text.it.text = servers[i].group;
	menu->items[j].text.it.validation = (char *) -1;
	menu->items[j].text.it.text_len = strlen (servers[i].group);
	j++;
      }
    }

  /* Tick the group entry for the selected ticker */
  for (i = 0; i < j; ++i)
    if (menu->items[i].submenu == 2
	&& !strcmp (servers[server].group, menu->items[i].text.it.text))
      menu->items[i].menuflags |= 1;

  /* Sort the entries & terminate the menu */
  i = group == 0;
  if (j > i + 1)
    qsort (menu->items + i, j - i, 24, menucmp);

  menu->items[j - 1].menuflags |= 128;
}


/* Find a server by label. Returns its index, or -1 if not found. */

int
find_server (const char *label)
{
  int i;
  const char *l = label ? label : opt.selected_server;
  for (i = 0; i < num_servers; ++i)
    if (!strnicmp (l, servers[i].label, sizeof (servers[i].label)))
      return i;
  return label ? -1 : 0;
}


int
find_server_from_menu (const unsigned int *menu)
{
  if (menu[0] == -1)
    return -1;
  else if (servers_menu->items[menu[0]].submenu == -1)
    return find_server (servers_menu->items[menu[0]].text.it.text);
  else if (menu[1] == -1)
    return -1;
  else
    return find_server (servers_submenu->items[menu[1]].text.it.text);
}


void
set_fetcher (const struct server *newfetcher)
{
  fetcher = *newfetcher;
  ticker_data.update_time = 0;
}


/* Config read subs */

static void
poke_option (struct options *myopt, int offset, int value, size_t length)
{
  char *data = (char *) myopt + offset;
  assert (length && length <= sizeof (int));
  memcpy (data, &value, length);
}


static void
read_bool_option (struct options *myopt, const char *line, int i)
{
  poke_option (myopt, tokens[i].offset, !!atoi (line), sizeof (char));
}


static void
read_string_option (struct options *myopt, const char *line, int i)
{
  /* Preallocated string */
  int length = strlen (line);
  if (length < tokens[i].arg[0])
    strcpy ((char *) myopt + tokens[i].offset, line);
}


static void
read_int_option (struct options *myopt, const char *line, int i)
{
  int num = atoi (line);
  if (num >= tokens[i].arg[1] && num <= tokens[i].arg[2])
    poke_option (myopt, tokens[i].offset, num, tokens[i].arg[0]);
}


static void
read_enum_option (struct options *myopt, const char *line, int i)
{
  int d = -1;
  const struct enum_info *data = enums[tokens[i].arg[1]];
  while (data[++d].label)
    if (!strcmp (line, data[d].label))
    {
      poke_option (myopt, tokens[i].offset, data[d].value, tokens[i].arg[0]);
      return;
    }
}


/* Config write subs */

static const void *
peek_option (const struct options *myopt, int offset, size_t length)
{
  static int value;
  const char *data = (const char *) myopt + offset;
  assert (length && length <= sizeof (int));
  value = 0;
  memcpy (&value, data, length);
  return &value;
}


static void
write_bool_option (const struct options *myopt, FILE *config, int i)
{
  char flag = *(char *) peek_option (myopt, tokens[i].offset, sizeof (char));
  fputc (flag ? '1' : '0', config);
}


static void
write_string_option (const struct options *myopt, FILE *config, int i)
{
  fputs ((char *) myopt + tokens[i].offset, config);
}


static void
write_int_option (const struct options *myopt, FILE *config, int i)
{
  char buf[12];
  sprintf (buf, "%i",
	   *(int *) peek_option (myopt, tokens[i].offset, tokens[i].arg[0]));
  fputs (buf, config);
}


static void
write_enum_option (const struct options *myopt, FILE *config, int i)
{
  int index =
    *(int *) peek_option (myopt, tokens[i].offset, tokens[i].arg[0]);
  const char *label = enums[tokens[i].arg[1]][index].label;
  fputs (label, config);
}


/* Simple (non-error) sanity checks */
static void
config_sane (struct options *myopt)
{
  if (!myopt->less_than_display && !myopt->width_is_percentage &&
      myopt->width < 400)
    myopt->width = 400;
  if (!myopt->proxy[0])
    myopt->use_proxy = 0;
  if (myopt->fast_scroll_rate <= myopt->scroll_rate)
    myopt->fast_scroll_rate = myopt->scroll_rate + 1;
  if (find_server (myopt->selected_server)
      == -1)
    strcpy (myopt->selected_server, fetcher_bbc.label);
}


/* Read the configuration file */

void
load_config (struct options *myopt)
{
  FILE *config;
  int long_line = 0;

  config_default (myopt);
  set_fetcher (&fetcher_bbc);
  _kernel_last_oserror ();

  if (osfile_get_type ("Choices:ymbj.Ticker") == 1)
    config = fopen ("Choices:ymbj.Ticker", "r");
  else
    config = fopen ("Choices:ymbj.Ticker.Choices", "r");
  if (!config)
    config = fopen ("<Ticker$Dir>.Choices", "r");
  if (!config)
    return;

  while (!feof (config) && !ferror (config))
  {
    char line[256];
    int i;

    line[0] = 0;
    fgets (line, sizeof (line), config);
    if (ferror (config))
    {
      _kernel_oserror *e = _kernel_last_oserror ();
      if (e)
	report_oserror (e);
      else
	report_errno ();
      fclose (config);
      return;
    }

    if (!line[0])
    {
      long_line = 0;
      continue;
    }
    if (line[i = strlen (line) - 1] == '\n')
    {
      int l = long_line;
      line[i] = '\0';
      long_line = 0;
      if (l)
	continue;
    }
    else
    {
      int l = long_line;
      long_line = 1;
      if (l)
	continue;
    }
    if (line[0] == '#')
      continue;

    i = 0;
    while (tokens[i].token)
    {
      int length = strlen (tokens[i].token);
      if (strncmp (line, tokens[i].token, length) || line[length] != ':')
	i++;
      else
      {
	tokens[i].read (myopt, line + length + 1, i);
	break;
      }
    }
  }

  fclose (config);

  config_sane (myopt);
  set_fetcher (&servers[find_server (myopt->selected_server)]);
}


/* Save the current configuration */

void
save_config (void)
{
  FILE *config = 0;
  _kernel_oserror *e;

  _kernel_last_oserror ();

  if (strlen (getenv ("Choices$Write")))
  {
    e = _swix (OS_File, _INR (0, 1) | _IN (4), 8, "<Choices$Write>.ymbj", 0);
    if (!e && osfile_get_type ("<Choices$Write>.ymbj.Ticker") == 1)
      /* Old config file location. Replace with a directory. */
      e = _swix (OS_File, _INR (0, 1), 6, "<Choices$Write>.ymbj.Ticker");
    if (!e)
      e = _swix (OS_File, _INR (0, 1) | _IN (4), 8,
		 "<Choices$Write>.ymbj.Ticker", 0);
    if (e)
      goto error;
    config = fopen ("<Choices$Write>.ymbj.Ticker.Choices", "w");
  }
  if (!config)
    config = fopen ("<Ticker$Dir>.Choices", "w");
  if (!config)
    goto fail;

  fputs ("# Ticker configuration\n# Generated - do not edit\n\n", config);
  {
    int i = -1;
    while (!feof (config) && tokens[++i].token)
      if (tokens[i].write)
      {
	fprintf (config, "%s:", tokens[i].token);
	tokens[i].write (&opt, config, i);
	fputc ('\n', config);
      }
  }

  if (ferror (config))
  {
  fail:
    e = _kernel_last_oserror ();
    if (e)
      report_oserror (e);
    else
      report_errno ();
  }
  if (config)
    fclose (config);
  return;

error:
  report_oserror (e);
}


static char *
skip_arg (char *p)
{
  char *np;
  if (!p || !*p)
    return p;
  np = strpbrk (p, " \t");
  if (!np)
    return 0;
  *np++ = 0;
  return np + strspn (np, " \t");
}


void
read_servers (int replace)
{
  FILE *config;

  _kernel_last_oserror ();

  if (!replace)
  {
    num_servers = 1;
    servers = realloc (servers, sizeof (struct server));
  }
  if (!servers)
  {
    report_error ("Out of memory");
    exit (2);
  }
  servers[0] = fetcher_bbc;

  config = fopen (CUSTOM_SERVERS, "r");
  if (!config)
    config = fopen (DEFAULT_SERVERS, "r");
  if (!config)
    return;

  merge_servers (config, replace);
  fclose (config);
}


static void
merge_servers (FILE *config, int replace)
{
  int long_line = 0;

  while (!feof (config) && !ferror (config))
  {
    char line[1024];
    char *group = "", *label, *server, *path;
    int port;

    int i, serverno;

    line[0] = 0;
    fgets (line, sizeof (line), config);
    if (ferror (config))
    {
      _kernel_oserror *e = _kernel_last_oserror ();
      if (e)
	report_oserror (e);
      else
	report_errno ();
      return;
    }

    if (!line[0])
    {
      long_line = 0;
      continue;
    }
    if (line[i = strlen (line) - 1] == '\n')
    {
      int l = long_line;
      line[i] = '\0';
      long_line = 0;
      if (l)
	continue;
    }
    else
    {
      int l = long_line;
      long_line = 1;
      if (l)
	continue;
    }
    if (line[0] == '#')
      continue;

    server = line + strspn (line, " \t");
    path = skip_arg (server);
    port = atoi (path);
    if (port < 1 || port > 65535)
      continue;			/* invalid port */
    path = skip_arg (path);
    label = skip_arg (path);
    if (strchr (label, '\t'))
    {
      group = label;
      label = strchr (label, '\t');
      while (*label == ' ' || *label == '\t')
	*label++ = '\0';
    }
    if (!*server || !*path || !*label)
      continue;			/* missing data */

    serverno = find_server (label);
    if (!replace && serverno != -1)
      continue;			/* already exists, and not replacing */
    if (replace && serverno == 0)
      continue;			/* can't replace entry 0... */
    if (serverno > 0 && servers[serverno].modified == MODIFIED_AUTOMATICALLY)
      continue;			/* modified automatically - never replace */

    if (serverno == -1)
    {
      serverno = num_servers++;
      servers = realloc (servers, num_servers * sizeof (struct server));
      if (!servers)
      {
	report_error ("Out of memory");
	exit (2);
      }
    }
    {
      struct server *s = &servers[serverno];
      *s = fetcher_rdf;
      STRNCPY (s->group, group);
      STRNCPY (s->label, label);
      STRNCPY (s->server, server);
      STRNCPY (s->stories_path, path);
      s->port = port;
      s->modified = 0;
    }
  }
}


int
servers_modified (void)
{
  int i, mod = 0;
  for (i = 0; i < num_servers; ++i)
    mod |= servers[i].modified;
  return mod;
}


void
save_servers (void)
{
  FILE *config = 0;
  _kernel_oserror *e;

  /* Anything to save? */
  if (!servers_modified ())
    return;

  _kernel_last_oserror ();

  if (strlen (getenv ("Choices$Write")))
  {
    int move = osfile_get_type ("<Choices$Write>.ymbj.Ticker") == 1;
    e = _swix (OS_File, _INR (0, 1) | _IN (4), 8, "<Choices$Write>.ymbj", 0);
    if (!e && move)
      /* Old config file location. We need to move it... */
      e = _swix (OS_FSControl, _INR (0, 2), 25,
		 "<Choices$Write>.ymbj.Ticker",
		 "<Choices$Write>.ymbj.Ticker/tmp");
    if (!e)
      e = _swix (OS_File, _INR (0, 1) | _IN (4), 8,
		 "<Choices$Write>.ymbj.Ticker", 0);
    if (!e && move)
      e = _swix (OS_FSControl, _INR (0, 2), 25,
		 "<Choices$Write>.ymbj.Ticker/tmp",
		 "<Choices$Write>.ymbj.Ticker.Choices");
    if (e)
      goto error;
    config = fopen ("<Choices$Write>.ymbj.Ticker.Servers", "w");
  }
  if (!config)
    config = fopen (DEFAULT_SERVERS, "w");
  if (!config)
    goto fail;

  fputs ("# Ticker configuration\n# Generated - do not edit\n\n", config);
  {
    int i = 0;
    while (++i < num_servers)
      if (servers[i].group[0])
        fprintf (config, "%s\t%i\t%s\t%s\t%s\n",
		 servers[i].server, servers[i].port,
		 servers[i].stories_path, servers[i].group,
		 servers[i].label);
      else
        fprintf (config, "%s\t%i\t%s\t%s\n",
		 servers[i].server, servers[i].port,
		 servers[i].stories_path, servers[i].label);
  }

  if (ferror (config))
  {
  fail:
    e = _kernel_last_oserror ();
    if (e)
      report_oserror (e);
    else
      report_errno ();
  }
  else
  {
    int i = 0;
    while (++i < num_servers)
      servers[i].modified = 0;
  }
  if (config)
    fclose (config);
  return;

error:
  report_oserror (e);
}



static void
set_icon_bgcol (int w, int i, int wimpcol)
{
  static const char fgcol[] =
    { 0x07, 0x17, 0x27, 0x37, 0x40, 0x50, 0x60, 0x70, 0x80, 0x97, 0xA0, 0xB0,
    0xC7, 0xD0, 0xE7, 0xF7
  };
  char buf[3];
  sprintf (buf, "%i", wimpcol);
  set_icon_text (w, i, buf);
  set_icon_state (w, i, fgcol[wimpcol] << 24, 0xFFU << 24);
}


/* Reflect the current configuration in the choices window. */


static void
tick_icon_range (int w, int fi, int li, int which)
{
  while (li >= fi)
  {
    tick_icon (w, li, which == li - fi);
    li--;
  }
}


static void
set_icon_value (int w, int i, int v)
{
  char buf[16];
  sprintf (buf, "%i", v);
  set_icon_text (w, i, buf);
}


static int
get_icon_value (int w, int i)
{
  return atoi (get_icon_text (w, i));
}


static void
config_show_current (const struct options *myopt)
{
  char buf[96];
  int i;

  tick_icon (CONFIG_PANE (CONN), 0, myopt->use_proxy);
  if (*myopt->proxy)
  {
    sprintf (buf, "%s:%i", myopt->proxy, myopt->proxyport);
    set_icon_text (CONFIG_PANE (CONN), 1, buf);
  }
  else
    set_icon_text (CONFIG_PANE (CONN), 1, "");
  tick_icon (CONFIG_PANE (CONN), 2, myopt->update);
  set_icon_value (CONFIG_PANE (CONN), 3, myopt->update_every);
  tick_icon (CONFIG_PANE (CONN), 7, !myopt->iconised_update);
  set_icon_text (CONFIG_PANE (CONN), 9, myopt->connect_var);
  set_icon_text (CONFIG_PANE (CONN), 11, myopt->selected_server);
  shade_icon (CONFIG_PANE (CONN), 12, num_servers == 1);
  tick_icon (CONFIG_PANE (CONN), 13, myopt->use_doc_update);

  sprintf (buf, "%i", myopt->scroll_rate * 50);
  set_icon_text (CONFIG_PANE (TICK), 1, buf);
  sprintf (buf, "%i", myopt->fast_scroll_rate * 50);
  set_icon_text (CONFIG_PANE (TICK), 15, buf);
  tick_icon_range (CONFIG_PANE (TICK), 6, 8, myopt->pause_when);
  set_icon_bgcol (CONFIG_PANE (TICK), 10, myopt->highlight);
  tick_icon (CONFIG_PANE (TICK), 13, myopt->auto_desc);
  tick_icon (CONFIG_PANE (TICK), 14, myopt->update_to_front);

  tick_icon (CONFIG_PANE (INIT), 0, myopt->delay_init_fetch);
  tick_icon_range (CONFIG_PANE (INIT), 2, 4, myopt->initial_state);
  shade_icon (CONFIG_PANE (INIT), 4, myopt->no_ibar_icon);
  tick_icon (CONFIG_PANE (INIT), 5, myopt->no_ibar_icon);

  set_icon_value (CONFIG_PANE (SHOW), 1, myopt->width);
  tick_icon_range (CONFIG_PANE (SHOW), 2, 3, myopt->width_is_percentage);
  tick_icon (CONFIG_PANE (SHOW), 4, myopt->less_than_display);
  tick_icon_range (CONFIG_PANE (SHOW), 6, 7, myopt->open_at);
  tick_icon_range (CONFIG_PANE (SHOW), 9, 11, myopt->open_align);
  tick_icon (CONFIG_PANE (SHOW), 12, myopt->at_top);
  set_icon_value (CONFIG_PANE (SHOW), 16, myopt->offset.x);
  set_icon_value (CONFIG_PANE (SHOW), 18, myopt->offset.y);

  tick_icon (CONFIG_PANE (BBC), 0, myopt->low_graphics);
}


/* Read the new configuration from the choices window.
 * Returns 0 if there's an error.
 */

static void config_set_position (void);

static int
config_set_current (void)
{
  const char *ptr, *colon;
  int i;

  /* NOTE: if another error-checked config option is added, must alter this
   * to store the new config in a temporary struct, freeing on failure,
   * copying on success.
   */

  ptr = get_icon_text (CONFIG_PANE (CONN), 1);
  colon = strchr (ptr, ':');
  if (ptr == colon)
  {
    report_error ("Missing proxy name or IP address");
    return 0;
  }
  if (*ptr && colon)
  {
    int port = atoi (colon + 1);
    if (colon - ptr >= MAX_PROXY_LENGTH)
    {
      report_error ("Eek! The proxy name is too long!");
      return 0;
    }
    if (port < 1 || port > 65535)
    {
      report_error ("Invalid or missing proxy port number");
      return 0;
    }
    opt.proxy[colon - ptr] = '\0';
    strncpy (opt.proxy, ptr, colon - ptr);
    opt.proxyport = port;
  }
  else if (*ptr)
  {
    report_error ("Invalid or missing proxy port number");
    return 0;
  }
  else
    opt.proxy[0] = 0;
  opt.use_proxy = icon_ticked (CONFIG_PANE (CONN), 0);
  opt.update = icon_ticked (CONFIG_PANE (CONN), 2);
  opt.update_every = get_icon_value (CONFIG_PANE (CONN), 3);
  opt.iconised_update = !icon_ticked (CONFIG_PANE (CONN), 7);
  STRNCPY (opt.connect_var, get_icon_text (CONFIG_PANE (CONN), 9));
  opt.use_doc_update = icon_ticked (CONFIG_PANE (CONN), 13);

  opt.scroll_rate = get_icon_value (CONFIG_PANE (TICK), 1) / 50;
  opt.fast_scroll_rate = get_icon_value (CONFIG_PANE (TICK), 15) / 50;
  opt.pause_when = pause_NEVER;
  if (icon_ticked (CONFIG_PANE (TICK), 7))
    opt.pause_when = pause_POINTER;
  else if (icon_ticked (CONFIG_PANE (TICK), 8))
    opt.pause_when = pause_HIGHLIGHT;
  opt.highlight = get_icon_value (CONFIG_PANE (TICK), 10);
  opt.auto_desc = icon_ticked (CONFIG_PANE (TICK), 13);
  opt.update_to_front = icon_ticked (CONFIG_PANE (TICK), 14);

  opt.delay_init_fetch = icon_ticked (CONFIG_PANE (INIT), 0);
  opt.initial_state = start_OPEN;
  if (icon_ticked (CONFIG_PANE (INIT), 3))
    opt.initial_state = start_MINIMISED;
  else if (icon_ticked (CONFIG_PANE (INIT), 4))
    opt.initial_state = start_CLOSED;
  opt.no_ibar_icon = icon_ticked (CONFIG_PANE (INIT), 5);

  opt.low_graphics = icon_ticked (CONFIG_PANE (BBC), 0);

  ptr = get_icon_text (CONFIG_PANE (CONN), 11);
  if (find_server (ptr) == -1)
  {
    report_error ("Deleted server details - defaulting to BBC News");
    ptr = fetcher_bbc.label;
  }
  strcpy (opt.selected_server, ptr);

  config_set_position ();
  wimp_create_iconbar_icon ();

  config_sane (&opt);
  return 1;
}


static void
config_set_position (void)
{
  opt.width = get_icon_value (CONFIG_PANE (SHOW), 1);
  opt.width_is_percentage = icon_ticked (CONFIG_PANE (SHOW), 3);
  if (opt.width_is_percentage && opt.width > 100)
    opt.width = 100;
  opt.less_than_display = icon_ticked (CONFIG_PANE (SHOW), 4);
  opt.open_at = open_ABOVE_ICONBAR;
  if (icon_ticked (CONFIG_PANE (SHOW), 7))
    opt.open_at = open_AT_TOP;
  opt.open_align = open_ALIGN_LEFT;
  if (icon_ticked (CONFIG_PANE (SHOW), 10))
    opt.open_align = open_ALIGN_CENTRE;
  else if (icon_ticked (CONFIG_PANE (SHOW), 11))
    opt.open_align = open_ALIGN_RIGHT;
  opt.at_top = icon_ticked (CONFIG_PANE (SHOW), 12);
  opt.offset.x = get_icon_value (CONFIG_PANE (SHOW), 16);
  opt.offset.y = get_icon_value (CONFIG_PANE (SHOW), 18);
}


/* Open the configuration window.
 * If it's not already open, open it centred.
 * Bring it to the front if requested.
 */

void
open_config_window (int tofront)
{
  int i, open = 1;
  struct wimp_getwindowstate_block *pane = &poll_block.open_window + 1;

  if (tofront)
  {
    wimp_get_window_state (&poll_block.open_window, wind.config);
    open = poll_block.open_window.window_flags & 1 << 16;
  }

  if (!open)
  {
    centre_window (&poll_block.open_window);
    config_show_current (&opt);
    config_pane = 0;

    tick_icon (wind.config, 5, 1);
    for (i = 6; i < 10; i++)
      tick_icon (wind.config, i, 0);
  }

  /* get the info for the pane window... */
  wimp_get_window_state (pane, wind.config_pane[config_pane]);

  if (tofront)
    poll_block.open_window.handle_behind = -1;

  /* Now actually open the windows... */
  open_sub (&poll_block.open_window, pane, 16, 16, 0);

  if (!open && config_pane == 0)
    set_caret_icon (CONFIG_PANE (CONN), 1);
}


void
close_config_window (void)
{
  wimp_close_window (&wind.config_pane[config_pane]);
  wimp_close_window (&wind.config);
}


void
open_servers_window (int tofront, int is_new, const struct server *server)
{
  int i = server ? 1 : -1, open = 1;
  char s[520];

  if (tofront)
  {
    wimp_get_window_state (&poll_block.open_window, wind.servers);
    open = poll_block.open_window.window_flags & 1 << 16;
    if (!open && !server)
      i = find_server (0);
  }
  if (i < 0)
  {
    i = find_server (get_icon_text (wind.servers, 1));
    if (i < 0)
      i = 0;
  }
  if (!server)
    server = &servers[i];
  else
    i = (server != &servers[0]);

  if (!open)
    centre_window (&poll_block.open_window);

  {
    int s = !i;
    shade_icon (wind.servers, 2, num_servers == 1);
    shade_icon (wind.servers, 4, s);
    shade_icon (wind.servers, 5, s);
    shade_icon (wind.servers, 6, s);
    set_icon_state (wind.servers, 7, i ? icn_TEXT_BIT : 0, icn_TEXT_BIT);
    shade_icon (wind.servers, 8, s);
    shade_icon (wind.servers, 9, s);
    shade_icon (wind.servers, 10, s);
  }

  if (!i)
  {
    struct wimp_getcaretposition_block caret_pos;
    _swi (Wimp_GetCaretPosition, _IN (1), &caret_pos);
    if (caret_pos.window_handle == wind.servers)
      set_caret_icon (-1, -1);
  }

  set_icon_text (wind.servers, 1, is_new ? "" : server->label);
  set_icon_text (wind.servers, 4, server->label);
  set_icon_text (wind.servers, 5, server->group);
  if (server->port == 80)
    sprintf (s, "%s/%s", server->server, server->stories_path);
  else
    sprintf (s, "%s:%i/%s", server->server, server->port,
	     server->stories_path);
  set_icon_text (wind.servers, 6, s);

  if (tofront == 1)
    poll_block.open_window.handle_behind = -1;
  wimp_open_window (&poll_block.open_window);

  if (!open && i)
    set_caret_icon (wind.servers, 4);
}


static int
bump_icon (int w, int i, int min, int max, int mul,
	   const struct wimp_getpointerinfo_block *block)
{
  int orig = get_icon_value (w, i) / mul;
  int num = orig;
  if (block)
    num += (((block->icon_handle - i) ^ (block->buttons == 1)) & 1) ? 1 : -1;
  else if (num < min)
    num = min;
  else if (num > max)
    num = max;
  if (num >= min && num <= max && num != orig)
  {
    char buf[4];
    sprintf (buf, "%i", num * mul);
    set_icon_text (w, i, buf);
  }
  return num;
}


/* Handle mouse clicks in the configuration window. */

void
click_config_window (void)
{
  int buttons = poll_block.mouse_click.buttons;
  if (buttons != 2)
    switch (poll_block.mouse_click.icon_handle)
    {
    case 0:			/* default */
      {
	struct options myopt = { 0 };
	/* use saved config unless Adjust click */
	if (buttons == 1)
	  config_default (&myopt);
	else
	  load_config (&myopt);
	config_show_current (&myopt);
      }
      break;
    case 2:			/* save */
      if (!config_set_current ())
	break;
      save_config ();
      goto set_top;
    case 3:			/* ok */
      if (!config_set_current ())
	break;
    set_top:
      set_ticker_top ();
      /* fall through */
    case 1:			/* cancel */
      if (buttons == 1)
	config_show_current (&opt);
      else
	close_config_window ();
      break;

    case 5:			/* pane select */
    case 6:
    case 7:
    case 8:
    case 9:
    case 10:
      tick_icon (wind.config, poll_block.mouse_click.icon_handle, 1);
      if (config_pane != poll_block.mouse_click.icon_handle - 5)
      {
	struct wimp_getwindowstate_block block;
	wimp_get_window_state (&block, wind.config_pane[config_pane]);
	wimp_close_window ((int *) &block);
	config_pane = poll_block.mouse_click.icon_handle - 5;
	block.window_handle = wind.config_pane[config_pane];
	wimp_open_window_ext (&block, wind.config, 0);
	if (config_pane == 0)
	  set_caret_icon (CONFIG_PANE (CONN), 1);
      }
      break;

    case 11:
      open_servers_window (1, 0, 0);
      break;
    }
}


void
click_config_connection (void)
{
  if (poll_block.mouse_click.buttons != 2)
    switch (poll_block.mouse_click.icon_handle)
    {
    case 4:			/* increment icon 3 (update time) */
    case 5:			/* decrement icon 3 (update time) */
      bump_icon (CONFIG_PANE (CONN), 3, 1, 999, 1, &poll_block.mouse_click);
      break;
    case 12:
      create_servers_menu (0, get_icon_text (CONFIG_PANE (CONN), 11));
      open_menu (servers_menu, CONFIG_PANE (CONN), 12);
      break;
    }
}


void
click_config_ticker (void)
{
  int scroll;

  if (poll_block.mouse_click.buttons != 2)
    switch (poll_block.mouse_click.icon_handle)
    {
    case 2:			/* increment icon 1 (scroll rate) */
    case 3:			/* decrement icon 1 (scroll rate) */
      scroll = bump_icon (CONFIG_PANE (TICK), 1, 1, MAX_SCROLL_RATE, 50,
			  &poll_block.mouse_click);
      bump_icon (CONFIG_PANE (TICK), 15, scroll + 1, MAX_FAST_SCROLL_RATE,
		 50, 0);
      break;

    case 6:			/* pause icons */
    case 7:
    case 8:
      tick_icon (CONFIG_PANE (TICK), poll_block.mouse_click.icon_handle, 1);
      break;

    case 11:
    case 12:
      open_menu ((void *) &colour_menu, CONFIG_PANE (TICK), 11);
      break;

    case 16:			/* increment icon 15 (fast scroll rate) */
    case 17:			/* decrement icon 15 (fast scroll rate) */
      scroll = bump_icon (CONFIG_PANE (TICK), 15, 2, MAX_FAST_SCROLL_RATE,
			  50, &poll_block.mouse_click);
      bump_icon (CONFIG_PANE (TICK), 1, 1, scroll - 1, 50, 0);
      break;
    }
}


void
click_config_startup (void)
{
  if (poll_block.mouse_click.buttons != 2)
    switch (poll_block.mouse_click.icon_handle)
    {
    case 2:			/* initial window state icons */
    case 3:
    case 4:
      tick_icon (CONFIG_PANE (INIT), poll_block.mouse_click.icon_handle, 1);
      break;
    case 5:			/* no icon */
      shade_icon (CONFIG_PANE (INIT), 4,
		  !icon_ticked (CONFIG_PANE (INIT), 5));
      break;
    }
}


static void
config_try_ticker (void)
{
  close_window (wind.ticker_tbar);
  wimp_get_window_state (&poll_block.open_window, wind.ticker_tbar);
  set_ticker_align (&poll_block.open_window);
  set_ticker_width (&poll_block.open_window);
  open_ticker_window_current (&poll_block.open_window, -1, 0);
  set_ticker_top ();
}


static void
config_get_ticker_offset (void)
{
  int w, v = 0;

  wimp_get_window_state (&poll_block.open_window, wind.ticker_tbar);

  w = opt.width_is_percentage
      ? (opt.less_than_display
	 ? mode.size.x * (100 - opt.width) / 100
	 : mode.size.x * opt.width / 100)
      : (opt.less_than_display ? mode.size.x - opt.width : opt.width);

  switch (opt.open_align)
  {
  case open_ALIGN_LEFT:
    v = poll_block.open_window.min.x;
    break;
  case open_ALIGN_CENTRE:
    v = poll_block.open_window.min.x + w / 2 - mode.size.x / 2;
    break;
  case open_ALIGN_RIGHT:
    v = mode.size.x - poll_block.open_window.min.x - w;
    break;
  }
  set_icon_value (CONFIG_PANE (SHOW), 16, v);

  if (poll_block.open_window.max.y == poll_block.open_window.min.y)
  {
    struct wimp_extend_11_block extend;
    wimp_extend_11 (&extend);
    poll_block.open_window.max.y += extend.toggle.height;
  }
  switch (opt.open_at)
  {
  case open_AT_TOP:
    v = mode.max.y - poll_block.open_window.max.y;
    break;
  case open_ABOVE_ICONBAR:
    v = poll_block.open_window.min.y - 132;
    break;
  }
  set_icon_value (CONFIG_PANE (SHOW), 18, v);
}


void
click_config_appearance (void)
{
  if (poll_block.mouse_click.buttons != 2)
    switch (poll_block.mouse_click.icon_handle)
    {
    case 2:			/* units icons */
    case 3:
    case 6:			/* open at icons */
    case 7:
    case 9:			/* alignment icons */
    case 10:
    case 11:
      tick_icon (CONFIG_PANE (SHOW), poll_block.mouse_click.icon_handle, 1);
      break;

    case 13:
      {
	struct options my_opt = opt;	/* preserve options */
	config_set_position ();
	config_try_ticker ();
	opt = my_opt;		/* restore preserved options */
      }
      break;

    case 14:
      config_try_ticker ();
      break;

    case 20:
      config_get_ticker_offset ();
      break;
    }
}


void
click_config_bbc (void)
{
  /* Nothing needed. */
}


int
click_config_menu (const void *menu, const unsigned int sel[], int window,
		   int icon)
{
  if (window == CONFIG_PANE (TICK))
  {
    if (menu == &colour_menu && icon == 11)
    {
      if (sel[0] < 16 && sel[0] != 1)
	set_icon_bgcol (window, 10, sel[0]);
    }
  }
  else
    return 0;

  return 1;
}


int
parse_server (struct server* server, const char *label, const char *group,
	      const char *url)
{
  char *e_server = strchr (url, ':');
  char *path = strchr (url, '/');
  int port = 80;

  if (!*label)
    return 3;

  if (!*url || !*path)
    return 1;

  if (!e_server || e_server > path)
    e_server = 0;
  else
    port = atoi (e_server + 1);
  if (port < 1 || port > 65535)
    return 2;

  if (e_server)
    *e_server = 0;
  if (path)
    *path = 0;
  STRNCPY (server->group, group);
  STRNCPY (server->label, label);
  STRNCPY (server->server, url);
  STRNCPY (server->stories_path, path + 1);
  if (e_server)
    *e_server = ':';
  if (path)
    *path = '/';
  server->port = port;

  return 0;
}

static void
accept_server (void)
{
  int server = find_server (get_icon_text (wind.servers, 1));
  if (server)
  {
    /* Update an existing server, or add a new server */
    struct server newserver = fetcher_rdf;
    const char *label = get_icon_text (wind.servers, 4);
    int i = find_server (label);
    if (i != server && i != -1)
    {
      report_error ("This label is already in use. You must pick another.");
      return;
    }
    switch (parse_server
	    (&newserver, label, get_icon_text (wind.servers, 5),
	     get_icon_text (wind.servers, 6)))
    {
    case 1:
      report_error ("Invalid URL: give host/path or host:port/path");
      return;
    case 2:
      report_error ("Invalid port number");
      return;
    case 3:
      report_error ("Labels must not be blank");
      return;
    }
    if (server == -1)
    {
      struct server *s = calloc (num_servers + 1, sizeof (struct server));
      if (!s)
      {
	report_error ("Out of memory");
	return;
      }
      memcpy (s, servers, num_servers * sizeof (struct server));
      server = num_servers++;
      free (servers);
      servers = s;
    }
    servers[server] = newserver;
    servers[server].modified = MODIFIED_BY_USER;
    shade_icon (CONFIG_PANE (CONN), 12, 0);
    open_servers_window (2, 0, &servers[server]);
  }
}


void
delete_server (int server)
{
  int i;
  if (server < 1 || server >= num_servers)
    return;
  i = server + 1;
  while (i < num_servers)
    servers[server++] = servers[i++];
  servers = realloc (servers, --num_servers * sizeof (struct server));
  servers[0].modified = MODIFIED_BY_USER;
}


/* Handle mouse clicks in the servers window. */

void
click_servers_window (void)
{
  int i;
  if (poll_block.mouse_click.buttons == 2)
  {
    set_icon_text (wind.save_link, 1, "TickerURL");
    open_menu ((void *) wind.save_link, poll_block.mouse_click.pos.x - 64, poll_block.mouse_click.pos.y + 64);
    current_menu.window = wind.servers;
    current_menu.story = 0;
  }
  else
    switch (poll_block.mouse_click.icon_handle)
    {
    case 2:			/* menu */
      create_servers_menu (0, get_icon_text (wind.servers, 1));
      open_menu (servers_menu, wind.servers, 2);
      break;
    case 8:			/* restore this item to stored settings */
      open_servers_window (0, 0, 0);
      break;
    case 9:			/* delete this item */
      {
	int server = find_server (get_icon_text (wind.servers, 1));
	if (server == -1)
	  report_error ("Erk! This item is already deleted!");
	else if (server == find_server (0))
	  report_error
	    ("You must choose a different default server before you can delete this item.");
	else if (server != 0)
	{
	  /* Default the server in the configuration window if we're deleting
	   * its displayed server...
	   */
	  if (find_server (get_icon_text (CONFIG_PANE (CONN), 11)) >= server)
	    set_icon_text (CONFIG_PANE (CONN), 11, "");
	  /* Close the gap left by this server & free up a little space. */
	  delete_server (server);
	  shade_icon (wind.servers, 9, 1);
	  shade_icon (CONFIG_PANE (CONN), 12, num_servers == 1);
	}
      }
      break;
    case 10:			/* accept the changes to this item */
      accept_server ();
      break;
    case 12:			/* close the window */
      wimp_close_window (&wind.servers);
      break;
    case 13:			/* save the current settings */
      save_servers ();
      if (poll_block.mouse_click.buttons != 1)
	wimp_close_window (&wind.servers);
      break;
    case 14:
      read_servers (0);
      if (find_server (opt.selected_server) == -1)
      {
	report_error
	  ("The current server is not in the saved list. Defaulting to BBC News.");
	strcpy (opt.selected_server, fetcher_bbc.label);
      }
      shade_icon (CONFIG_PANE (CONN), 12, num_servers == 1);
      open_servers_window (0, 0, 0);
      break;
    case 15:
      set_icon_text (wind.servers, 1, "");
      for (i = 4; i < 7; ++i)
      {
        set_icon_text (wind.servers, i, "");
        shade_icon (wind.servers, i, 0);
      }
      set_icon_state (wind.servers, 7, icn_TEXT_BIT, icn_TEXT_BIT);
      shade_icon (wind.servers, 8, 1);
      shade_icon (wind.servers, 9, 1);
      shade_icon (wind.servers, 10, 0);
      set_caret_icon (wind.servers, 4);
      break;
    case 16:
      {
	FILE *config = fopen (DEFAULT_SERVERS, "r");
	if (config)
	{
	  merge_servers (config, poll_block.mouse_click.buttons == 1);
	  fclose (config);
	  open_servers_window (0, 0, 0);
	}
      }
      break;
    }
}


void
click_servers_menu (const unsigned int sel[], int window, int icon)
{
  int i = find_server_from_menu (sel);
  if (i != -1)
  {
    if (window == CONFIG_PANE (CONN))
      set_icon_text (window, 11, servers[i].label);
    else if (window == wind.servers)
      open_servers_window (0, 0, &servers[i]);
  }
}


int
key_servers (void)
{
  switch (poll_block.key_pressed.code)
  {
  case 13:
    if (poll_block.key_pressed.icon_handle == 6)
    {
      accept_server ();
      return 1;
    }
    break;
  case 27:
    wimp_close_window (&wind.servers);
    return 1;
  }

  return 0;
}
