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

/* System includes */

#include <assert.h>
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <swis.h>

/* Program includes */

#include "globals.h"

#include "fetch.h"
#include "wimp.h"
#include "wimpmenu.h"
#include "wimpticker.h"


/* Current ticker state */
struct ticker_t ticker = {is_CLOSED};

/* Scheduled monotonic time of the next autoscroll */
long next_scroll_time = 0;

int fast_scroll = 0;

/* Scroll position (for modes with Xeig > 0) */
static long scroll_offset = 0;



const struct story *
which_story (const struct wimp_getpointerinfo_block *block)
{
  const int icon = block->icon_handle;
  int section = fetcher.first_section;
  int story = 0;

  if (!ticker_data.stories || block->window_handle != wind.ticker)
    return 0;

  if (icon == 1)
    return &ticker_data.title;

  while (section < ticker_data.num_headings)
  {
    if (!ticker_data.stories[section]
	|| !ticker_data.stories[section][story].headline)
    {
      section++;
      story = 0;
    }
    else if (ticker_data.stories[section][story].icon == icon)
      return &ticker_data.stories[section][story];
    else
      story++;
  }
  return 0;
}


/**********************
 * Description window *
 **********************/


static void
open_description (const char *desc, const char *date)
{
  union wimp_windowinfo_block block;
  struct coords tpos_min, tpos_max;
  int chunk, i, lines = 0, width;
  struct wimp_createicon_block icon = { 0, {8, -8}, {0, -8}, 0x17003101 };
  int font, height = 36;
  const char *const which[2] = { date, desc }; /* reverse order */

  struct
  {
    struct coords space, letter;
    int splitchr;
    struct coordspair bbox;
  } fontblock = { {0, 0}, {0, 0}, ' '};

  static char *buffer[2] = { NULL, NULL };

  if (current_menu.handle)
    return; /* close_menu (); */

  for (chunk = 1; chunk >= 0; --chunk)
    if (which[chunk])
    {
      buffer[chunk] = realloc (buffer[chunk], strlen (which[chunk]) + 1);
      if (!buffer)
      {
	report_error ("Out of memory");
	return;
      }
      strcpy (buffer[chunk], which[chunk]);
    }
    else
    {
      free (buffer[chunk]);
      buffer[chunk] = NULL;
    }

  icon.contents.it.validation = (char *) -1;

  wimp_get_window_info (&block.info, wind.ticker);
  tpos_min = block.state.min;
  tpos_max = block.state.max;

  width = tpos_max.x - tpos_min.x - 32;

  wimp_get_window_info (&block.info, wind.ticker_desc);

  /* Reset the desc window */
  for (i = 0; i < block.info.data.number_of_icons; ++i)
    wimp_delete_icon (wind.ticker_desc, i);

  /* Fill the desc window */
  if (_swix (Wimp_ReadSysInfo, _IN (0) | _OUT (0), 8, &font))
    font = 0;
  if (font)
  {
    char tmp[256];		/* should really read buffer size... */
    if (_swix
	(Font_ReadDefn, _INR (0, 1) | _IN (3) | _OUT (3), font, tmp, 0, &i))
      font = 0;
    else
      height =
	(int) (ceil (i * 180.0 / 16 / 72) + 3 + mode.min.x) & ~(mode.min.x -
								1);
  }
  icon.min.y -= height;

  for (chunk = 1; chunk >= 0; --chunk)
  {
    char *ptr, *end;

    if (!buffer[chunk])
      continue; /* no text? skip it */

    /* black for description, red for publication date */
    icon.icon_flags = (icon.icon_flags & 0xF0FFFFFF) | (chunk ? 7 : 11) << 24;

    ptr = buffer[chunk];
    end = ptr + strlen (ptr);

    while (ptr < end)
    {
      char *start, *split;
      while (*ptr == ' ')
	++ptr;
      start = ptr;
      if (!font ||
	  _swix (Font_ScanString, _INR (0, 6) | _OUT (1) | _OUT (7), /*in*/
		 font, ptr, 1<<20 | 1<<18 | 1<<5, width * 400, INT_MAX,
		 &fontblock, 0, /*out*/ &split, &i))
      { /* System font */
	split = 0;
	--ptr;
	while ((++ptr - start) < width / 16)
	  if (*ptr == ' ')
	    split = ptr;
	icon.max.x = 24 + (ptr - start) * 16;
	if (split)
	{
	  *split = 0;
	  ptr = split + 1;
	}
      }
      else
      { /* Outline font */
	if (i == 0 && *split != ' ') /* no split found */
	{
	  fontblock.splitchr = -1;
	  _swix (Font_ScanString, _INR (0, 6) | _OUT (1) | _OUT (7), /*in*/
		 font, ptr, 1<<20 | 1<<18 | 1<<5, width * 400, INT_MAX,
		 &fontblock, 0, /*out*/ &split, &i);
	  fontblock.splitchr = ' ';
	}
	ptr = split;
	if (i)
	  *ptr++ = '\0';
	icon.max.x = 24 + (fontblock.bbox.br.x + 399) / 400;
      }
      icon.contents.it.text = (char *)start;
      icon.contents.it.text_len = ptr - start;
      wimp_create_icon (&icon, wind.ticker_desc, 0);
      ++lines;
      icon.max.y = icon.min.y;
      icon.min.y -= height;
    }
  }

  /* Position the desc window */
  block.open.min = tpos_min;
  block.open.max = tpos_max;
  if (tpos_min.y > lines * height + mode.min.y + 16)
  {
    block.open.max.y = tpos_min.y - mode.min.y;
    block.open.min.y = block.open.max.y - lines * height - 16;
  }
  else
  {
    block.open.min.y = tpos_max.y + mode.min.y;
    block.open.max.y = block.open.min.y + lines * height + 16;
  }
  block.open.scroll.y = block.open.scroll.x = 0;

  wimp_close_window (&block.window_handle);

  {
    struct coordspair size =
    {
      {0, block.open.min.y - block.open.max.y},
      {block.open.max.x - block.open.min.x, 0}
    };
    _swix (Wimp_SetExtent, _INR (0, 1), block.window_handle, &size);
  }

  if (block.open.min.y < mode.min.y)
    block.open.min.y = mode.min.y;
  if (block.open.max.y > mode.max.y)
    block.open.min.y = mode.max.y;

  block.open.handle_behind = -1;
  if (wimp_version < 400)
    wimp_open_window (&block.open);
  else
  {
    block.open.window_flags &= ~(1 << 23);	/* clear front */
    if (opt.at_top)
      block.open.window_flags |= 1 << 23;
    wimp_open_window_ext (&block.open, -1, 1);
  }
}


/*****************
 * Ticker window *
 *****************/

/* Create the scrolltext icons in the ticker window from the headlines text.
 * This uses get_text_width, so the effect is that the icons are the correct
 * width for the current desktop font (when this is called).
 */

void
process_stories (void)
{
  if (!task_handle)
    return;

  {
    struct wimp_getwindowinfo_block block;
    int i;

    wimp_get_window_info (&block, wind.ticker);
    i = block.data.number_of_icons;

    while (--i > 0)
      wimp_delete_icon (wind.ticker, i);
    set_icon_state (wind.ticker, 0, 0, icn_DELETED_BIT);	/* ?! */
  }

  if (!ticker_data.stories)
    return;
  {
    int section, x;
    static struct wimp_createicon_block texticon = {
      0, {0, -128}, {0, 128},
      icn_TEXT_DEFAULTS | icn_TYPE (6) | icn_INDIRECTED_BIT , ""
    };

    /* Label the ticker... */
    texticon.contents.it.text = (char *)ticker_data.title.headline;
    texticon.contents.it.text_len = strlen (ticker_data.title.headline);
    texticon.min.x = 0;
    texticon.max.x = x = get_text_width (texticon.contents.it.text);
    texticon.icon_flags &= ~icn_FGCOL (1);
    if (ticker_data.title.url)
    {
      texticon.contents.it.validation = "Pptr_link,4,0";
      texticon.icon_flags |= icn_ESG (1);
    }
    else
      texticon.contents.it.validation = "";
    erx (wimp_create_icon (&texticon, wind.ticker, 0));
    texticon.icon_flags |= icn_FGCOL (1);

    section = -1;
    while (++section < ticker_data.num_headings)
      if (ticker_data.stories[section])
      {
	int story = -1;

	while (ticker_data.stories[section][++story].headline)
	{
	  /* We need to insert a bullet character between two items. */
	  static struct wimp_createicon_block doticon = {
	    0, {0, -128}, {0, 128}, icn_TEXT_DEFAULTS | icn_TYPE (11),
	    " \x8F "
	  };
	  doticon.min.x = x;
	  doticon.max.x = x += get_text_width (" \x8F ");
	  erx (wimp_create_icon (&doticon, wind.ticker, 0));

	  texticon.contents.it.text =
	    (char *)ticker_data.stories[section][story].headline;
	  texticon.contents.it.text_len = strlen (texticon.contents.it.text);
	  texticon.min.x = x;
	  texticon.max.x = x += get_text_width (texticon.contents.it.text);
	  texticon.icon_flags &= ~icn_ESG (15);
	  if (ticker_data.stories[section][story].url)
	  {
	    texticon.contents.it.validation = "Pptr_link,4,0";
	    texticon.icon_flags |= icn_ESG (1);
	  }
	  else
	  {
	    texticon.contents.it.validation = "";
	    texticon.icon_flags &= ~icn_ESG (15);
	  }
	  erx (wimp_create_icon (&texticon, wind.ticker,
				 &ticker_data.stories[section][story].icon));
	}
      }

    ticker.max_x = x;
  }
}


/* Highlight an icon (in the ticker window). */

void
set_highlight (int icon)
{
  struct wimp_getpointerinfo_block block = { 0, 0, 0, wind.ticker, icon };
  const struct story *story;
  ticker.highlighted = icon;
  set_icon_state (wind.ticker, icon, icn_FGCOL (opt.highlight),
		  icn_FGCOL (15));
  if (opt.auto_desc && (story = which_story (&block)) != 0
      && (story->description || story->publish_date))
    open_description (story->description, story->publish_date);
  else
    wimp_close_window (&wind.ticker_desc);
}


/* Unhighlight whichever icon is highlighted. */

void
clear_highlight (void)
{
  if (ticker.highlighted > -1)
  {
    wimp_close_window (&wind.ticker_desc);
    set_icon_state (wind.ticker, ticker.highlighted, icn_FGCOL (7),
		    icn_FGCOL (15));
    ticker.highlighted = -1;
    ticker.paused = 0;
    next_scroll_time = read_monotonic_time ();
  }
}


/* Do we need to unhighlight an icon?
 * Do we need to highlight a different icon?
 */

void
check_highlight (void)
{
  struct wimp_getpointerinfo_block block;
  struct wimp_geticonstate_block icon;

  wimp_get_pointer_info (&block);
  if (block.window_handle != wind.ticker
      || block.icon_handle == ticker.highlighted)
    return;

  clear_highlight ();
  if (block.icon_handle < 1)
    return;

  wimp_get_icon_state (wind.ticker, block.icon_handle, &icon);
  if (icon.icon_flags & icn_ESG (15))
    set_highlight (icon.icon_handle);
}


void
go_fetch_ticker (int force)
{
  wimp_close_window (&wind.ticker_desc);
  if (fetch_ticker (force))
    ticker.done_first_fetch = 1;
}


void
reposition_ticker_window (struct wimp_openwindow_block *block)
{
  if (opt.open_align != open_ALIGN_LEFT)
  {
    int d = (mode.max.x - prevmode.max.x) - (mode.min.x - prevmode.min.x);
    if (opt.open_align == open_ALIGN_CENTRE)
      d = block->min.x + block->max.x >= prevmode.max.x ? d / 2  : -d / 2;
    block->min.x += d;
    block->max.x += d;
  }
  if (opt.open_at == open_AT_TOP)
  {
    int d = (mode.max.y - prevmode.max.y) - (mode.min.y - prevmode.min.y);
    block->min.y += d;
    block->max.y += d;
  }
}


void
set_ticker_state (enum ticker_state state)
{
  if (ticker.pinned || ticker.state != is_OPEN)
    next_scroll_time = (read_monotonic_time () + 1L) & ~1L;
  ticker.state = state;
}


#define ABS(v) ((v) < 0 ? -(v) : (v))

void
set_ticker_align (struct wimp_openwindow_block *block)
{
  struct wimp_extend_11_block extend;

  /* Get the current tool icon sizes. */
  wimp_extend_11 (&extend);

  switch (opt.open_align)
  {
  case open_ALIGN_LEFT:
    block->min.x = ABS (opt.offset.x);
    break;
  case open_ALIGN_CENTRE:
    block->max.x -= block->min.x;
    block->min.x = (mode.size.x - block->max.x) / 2 + opt.offset.x;
    block->max.x += block->min.x;
    break;
  case open_ALIGN_RIGHT:
    block->max.x -= block->min.x;
    block->min.x = mode.size.x - ABS (opt.offset.x) - block->max.x;
    block->max.x = mode.size.x - ABS (opt.offset.x);
  }
  block->max.y = block->min.y =
    (opt.open_at == open_AT_TOP)
    ? (mode.max.y - extend.toggle.height - ABS (opt.offset.y))
    : (132 + opt.offset.y);
}

void
set_ticker_width (struct wimp_openwindow_block *block)
{
  block->max.x =
    opt.width_is_percentage ? mode.max.x * opt.width / 100 : opt.width;
  if (opt.less_than_display)
    block->max.x = mode.max.x - block->max.x;
  if (block->max.x < 400)
    block->max.x = 400;
  block->max.x += block->min.x;
}


static void
open_ticker_common (struct wimp_getwindowstate_block **const ptbar,
		    struct wimp_getwindowstate_block *const pane,
		    struct wimp_extend_11_block *const extend, int under)
{
  struct wimp_getwindowstate_block *tbar = *ptbar;

if (under == -1)
{
  extend->back.width = 0; /* somewhere to breakpoint */
}

  /* Get the current tool icon sizes. */
  wimp_extend_11 (extend);
  if (opt.at_top)
    extend->back.width = 0;

  /* Get the ticker tbar state (if not supplied). */
  if (!tbar)
  {
    *ptbar = tbar = &poll_block.open_window;
    wimp_get_window_state (tbar, wind.ticker_tbar);
  }

  /* Get the pane window state */
  wimp_get_window_state (pane, wind.ticker);

  /* Set the tbar stack position */
  if (under)
    tbar->handle_behind = under;

  /* If the ticker isn't open, position it according to config settings. */
  if (ticker.state == is_CLOSED)
  {
    tbar->handle_behind = -1;
    set_ticker_width (tbar);
    set_ticker_align (tbar);
  }
}


static void
open_ticker_pane_common (const struct wimp_getwindowstate_block *const tbar,
			 struct wimp_getwindowstate_block *const pane,
			 const struct wimp_extend_11_block *const extend)
{
  /* Set the position and size of the pane. */
  pane->min.x = tbar->min.x + extend->back.width + extend->close.width + 84;
  pane->max.x = tbar->max.x - extend->iconise.width - 2 - mode.min.x;
  pane->min.y = tbar->max.y + 2 + mode.min.y;
  pane->max.y = tbar->max.y + extend->toggle.height - 2;
  pane->scroll.y =
    (ticker.state == is_MINIMISED) ? INT_MIN : (pane->max.y - pane->min.y) / 2;
}


/* Open the ticker window. (open_ticker_window{,_minimised,_current})
 * tbar : ptr to block for Wimp_OpenWindow (poll_block is used if this is 0)
 * under: if non-zero, open at this position in the window stack
 * reset: if non-zero, reset the scroll position, or display status
 * If opening non-iconised then if the intended stack position is -3, then
 * the window is minimised.
 */
void
open_ticker_window_current (struct wimp_getwindowstate_block *tbar, int under,
			    int reset)
{
  if (ticker.state == is_MINIMISED)
    open_ticker_window_minimised (tbar, under);
  else
    open_ticker_window (tbar, under, reset);
}


void
open_ticker_window_minimised (struct wimp_getwindowstate_block *tbar,
			      int under)
{
  struct wimp_getwindowstate_block *pane = &poll_block.open_window + 1;
  struct wimp_extend_11_block extend;	/* for Wimp_Extend 11 */

  if (!task_handle)
    return;

  open_ticker_common (&tbar, pane, &extend, under);
  set_ticker_state (is_MINIMISED);
  wimp_open_window (tbar);
  tbar->max.x =
    tbar->min.x + extend.back.width + extend.close.width + 84 +
    2 * extend.iconise.width;
  open_ticker_pane_common (tbar, pane, &extend);
  pane->window_flags |= 1 << 23;	/* set furniture bit */
  open_sub (tbar, pane, INT_MAX, 0, 1);
}


void
open_ticker_window (struct wimp_getwindowstate_block *tbar, int under,
		    int reset)
{
  struct wimp_getwindowstate_block *pane = &poll_block.open_window + 1;
  struct wimp_extend_11_block extend;	/* for Wimp_Extend 11 */

  if (!task_handle)
    return;

  if (under == -3 || (!under && tbar && tbar->handle_behind == -3))
  {
    open_ticker_window_minimised (tbar, under);
    return;
  }

  open_ticker_common (&tbar, pane, &extend, under);
  if (tbar->handle_behind == -3)
  {
    open_ticker_window_minimised (tbar, under);
    return;
  }
  if (ticker.state == is_CLOSED)
    reset = 1;
  set_ticker_width (tbar);
  set_ticker_state (is_OPEN);
  wimp_open_window (tbar);
  open_ticker_pane_common (tbar, pane, &extend);

  /* Reset the scroll position. */
  if (reset)
  {
    clear_highlight ();
    scroll_offset = pane->scroll.x = pane->min.x - pane->max.x;
  }
  if (ticker_data.state != state_ACTIVE)
    scroll_offset = pane->scroll.x = -4096;

  pane->window_flags |= 1 << 23;	/* set furniture bit */
  open_sub (tbar, pane, INT_MAX, 0, 1);
}


/* Reopen the ticker window in response to a user command. */

void
reopen_ticker_window (int restart, int minimise)
{
  ticker.pinned = 0;
  pinboard_remove_ticker ();
  if (minimise == 2)
    minimise = _kernel_osbyte (129, 255, 255) & 0xFF00;
  if (minimise)
    open_ticker_window_minimised (NULL, -1);
  else if (ticker_data.state != state_LOADING && ticker.state != is_OPEN
      && !is_save_window_open () && time_to_update ())
    go_fetch_ticker (1);
  else
    open_ticker_window (NULL, -1, restart);
}


void
set_ticker_top (void)
{
  struct wimp_getwindowstate_block block;
  enum ticker_state state = ticker.state;

  if (wimp_version < 400)
    return;

  wimp_get_window_state (&block, wind.ticker_tbar);

  if (state == is_CLOSED)
    block.handle_behind = -3;

  block.window_flags &= ~(3 << 23);	/* clear front, to-back */
  block.window_flags |= 1 << (opt.at_top ? 23 : 24);

  wimp_open_window_ext (&block, -1, 1);
  open_ticker_window_current (&block, 0, 0);
  if (state == is_CLOSED)
    close_window (block.window_handle);
}


void
toggle_ticker_minimise (void)
{
  if (ticker.state == is_MINIMISED)
    open_ticker_window (NULL, 0, 0);
  else
    open_ticker_window_minimised (NULL, 0);
/*
  if (!ticker.pinned && ticker.state == is_OPEN
      && (!ticker.done_first_fetch || (opt.update && time_to_update ()))
      && !is_save_window_open ())
    go_fetch_ticker (0);
*/
}


/* Scroll the ticker window (if necessary). */

void
scroll_ticker (int step)
{
  int rate = fast_scroll ? opt.fast_scroll_rate : opt.scroll_rate;
  step *= rate;
  if (step > 32)
    step = step >= 96 ? rate : step >= 64 ? 96 - step : 32;

  wimp_get_window_state (&poll_block.open_window, wind.ticker);
  poll_block.open_window.scroll.x = scroll_offset + step;
  /* update if necessary */
  if (poll_block.open_window.scroll.x >= ticker.max_x)
  {
    if ((opt.iconised_update || ticker.state != is_MINIMISED) && opt.update
	&& !is_save_window_open () && time_to_update ())
      go_fetch_ticker (0);
    else
      open_ticker_window (NULL, 0, 1);
  }
  else if (opt.pause_when == pause_NEVER
	   || (opt.pause_when == pause_POINTER && !ticker.has_pointer)
	   || (opt.pause_when == pause_HIGHLIGHT && ticker.highlighted == -1))
  {
    scroll_offset += step;
    wimp_open_window (&poll_block.open_window);
  }
}


void
open_ticker_menu (void)
{
  struct wimp_getwindowstate_block block;
  int h = opt.no_ibar_icon ? TICKER_MENU_NO_IBAR_HEIGHT : TICKER_MENU_HEIGHT;
  void *m =
    opt.no_ibar_icon ? (void *) &ticker_menu_no_ibar : (void *) &ticker_menu;
  wimp_get_window_state (&block, wind.ticker);
  open_menu (m, poll_block.mouse_click.pos.x - 64,
	     (opt.at_top && wimp_version >= 400)
	     ? ((block.min.y < h + 40) ? block.max.y + h : block.min.y - 40)
	     : poll_block.mouse_click.pos.y + 64);
}

#include <stdio.h>
void
click_ticker (void)
{
  const struct story *story = which_story (&poll_block.mouse_click);
  int old_fast_scroll = fast_scroll;

  fast_scroll = 0;

  if (poll_block.mouse_click.icon_handle != -1)
    set_icon_state (wind.ticker, 0, 0, icn_SELECTED_BIT);

  if (poll_block.mouse_click.buttons == 2)
  {
    /* Menu click */
    current_menu.story = story;
    open_ticker_menu ();
    return;
  }
  else if (poll_block.mouse_click.buttons == 0x10)
  {
    ticker.paused = 0;
    wimp_close_window (&wind.ticker_desc);
    fast_scroll = 1;
  }
  else if (ticker_data.stories && story
	   && poll_block.mouse_click.icon_handle > 0)
  {
    /* Select, Adjust - is it on a headline? */
    if (_kernel_osbyte (129, 254, 255) & 0xFF00
	&& is_window_open (wind.ticker_desc))
    {
      struct wimp_getwindowstate_block block;
      wimp_get_window_state (&block, wind.ticker_desc);
      switch (poll_block.mouse_click.buttons)
      {
      case 4:
	block.scroll.y -= 128;
	wimp_open_window (&block);
	break;
      case 1:
	block.scroll.y += 128;
	wimp_open_window (&block);
	break;
      }
    }
    else if (story && story->url && poll_block.mouse_click.buttons == 4)
      launch_url (story->url, 1);
    else if (!old_fast_scroll && story &&
	     (story->description || story->publish_date) &&
	     poll_block.mouse_click.buttons == 1)
    {
      ticker.paused = !ticker.paused;
      next_scroll_time = read_monotonic_time ();
      if (!is_window_open (wind.ticker_desc))
	open_description (story->description, story->publish_date);
      else if (ticker.highlighted == -1)
	wimp_close_window (&wind.ticker_desc);
    }
  }
}
