/* -*- mode: C; mode: fold; -*- */
/* Copyright (c) 1998 John E. Davis (davis@space.mit.edu)
 *
 * This file is part of slrn.
 *
 * Slrn is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2, or (at your option) any
 * later version.
 * 
 * Slrn is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Slrn; see the file COPYING.  If not, write to the Free
 * Software Foundation, 59 Temple Place - Suite 330, 
 * Boston, MA  02111-1307, USA.
 */

#include "config.h"
#include "slrnfeat.h"

/*{{{ system include files */
#include <stdio.h>
#include <string.h>
#include <time.h>


#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif

#ifdef HAVE_STDLIB_H
# include <stdlib.h>
#endif

#include <ctype.h>
#include <slang.h>
#include "jdmacros.h"


#ifndef isalpha
# define isalpha(x) \
  ((((x) <= 'Z') && ((x) >= 'A')) \
   || (((x) <= 'z') && ((x) >= 'a')))
#endif

#ifndef isspace
# define isspace(x) (((x) == ' ') || ((x) == '\t'))
#endif

/*}}}*/
/*{{{ slrn include files */
#include "slrn.h"
#include "group.h"
#include "server.h"
#include "art.h"
#include "misc.h"
#include "post.h"
/* #include "clientlib.h" */
#include "startup.h"
#include "hash.h" 
#include "score.h"
#include "menu.h"
#include "util.h"
#include "xover.h"
#include "chmap.h"
#include "print.h"

#if SLRN_HAS_UUDEVIEW
# include <uudeview.h>
#endif
#include "uudecode.h"

#if SLRN_HAS_MIME 
# include "mime.h"
#endif

#if SLRN_HAS_GROUPLENS
# include "grplens.h"
#endif

/*}}}*/

/*{{{ extern Global variables  */

SLKeyMap_List_Type *Slrn_Article_Keymap;
char *Slrn_X_Browser;
char *Slrn_NonX_Browser;
char *Slrn_Quote_String;
char *Slrn_Save_Directory;
char *Slrn_Header_Help_Line;
char *Slrn_Art_Help_Line;
char *Slrn_Followup_Custom_Headers;
char *Slrn_Reply_Custom_Headers;

#if SLRN_HAS_TILDE_FEATURE
int Slrn_Use_Tildes = 1;
#endif

int Slrn_Startup_With_Article = 0;

int Slrn_Query_Next_Article = 1;
int Slrn_Query_Next_Group = 1;
int Slrn_Auto_CC_To_Poster = 0;
int Slrn_Score_After_XOver;
int Slrn_Use_Tmpdir = 0;
int Slrn_Threads_Visible = 0;
int Slrn_Use_Header_Numbers = 1;
#if SLRN_HAS_SORT_BY_SCORE
int Slrn_Display_Score;
#endif
int Slrn_High_Score_Min = 1;
int Slrn_Low_Score_Max = 0;
int Slrn_Kill_Score_Max = -9999;
int Slrn_Article_Window_Border = 0;
int Slrn_Reads_Per_Update = 50;
int Slrn_Sig_Is_End_Of_Article = 0;
#if SLRN_HAS_SPOILERS
int Slrn_Spoiler_Char = 42;
int Slrn_Spoiler_Display_Mode = 1;
#endif
int Slrn_New_Subject_Breaks_Threads = 0;

int Slrn_Simulate_Graphic_Chars = 0;
int Slrn_Del_Article_Upon_Read = 1;

char *Slrn_Current_Group_Name;
Slrn_Header_Type *Slrn_First_Header;
Slrn_Header_Type *Slrn_Current_Header;

/* range of articles on server for current group */
int Slrn_Server_Min, Slrn_Server_Max;

/* Sorting mode:
 *   0 No sorting
 *   1 sort by threads
 *   2 sort by subject
 *   3 sort by subject and threads
 *   4 sort by score
 *   5 sort by score and threads
 *   6 sort by score then by subject
 *   7 thread, then sort by score then subject
 *   8 sort by date
 */
#define SORT_BY_THREADS  1
#define SORT_BY_SUBJECT  2
#if SLRN_HAS_SORT_BY_SCORE
# define SORT_BY_SCORE   4
#endif
#define SORT_BY_DATE	8

int Slrn_Sorting_Mode = (SORT_BY_THREADS|SORT_BY_SUBJECT);

#define ALL_THREAD_FLAGS (FAKE_PARENT | FAKE_CHILDREN | FAKE_HEADER_HIGH_SCORE)

Slrn_Article_Line_Type *Slrn_Article_Lines;

/*}}}*/
/*{{{ static global variables */
static SLscroll_Window_Type Slrn_Article_Window;
static SLscroll_Window_Type Slrn_Header_Window;

static int Header_Window_Nrows;
static unsigned int Number_Killed;
static unsigned int Number_High_Scored;
static unsigned int Number_Low_Scored;
static int User_Aborted_Group_Read;
#if SLRN_HAS_SPOILERS
static Slrn_Header_Type *Spoilers_Visible;
#endif

#if SLRN_HAS_GROUPLENS
static int Num_GroupLens_Rated = -1;
#endif
static Slrn_Header_Type *Mark_Header;  /* header with mark set */

static Slrn_Group_Type *Current_Group; /* group being processed */

static int Total_Num_Headers;	       /* headers retrieved from server.  This
					* number is used only by update meters */
static Slrn_Header_Type *Headers;
static int Last_Cursor_Row;	       /* row where --> cursor last was */
static Slrn_Header_Type *Header_Showing;    /* header whose article is selected */
static Slrn_Header_Type *Last_Read_Header;
static int Article_Visible;	       /* non-zero if article window is visible */
static char Output_Filename[256];
static int Headers_Threaded;

/* If +1, threads are all collapsed.  If zero, none are.  If -1, some may
 * be and some may not.  In other words, if -1, this variable should not
 * be trusted.
 */
static int Threads_Collapsed = 0;

static int Graphic_LTee_Char = SLSMG_LTEE_CHAR;
static int Graphic_UTee_Char = SLSMG_UTEE_CHAR;
static int Graphic_LLCorn_Char = SLSMG_LLCORN_CHAR;
static int Graphic_HLine_Char = SLSMG_HLINE_CHAR;
static int Graphic_VLine_Char = SLSMG_VLINE_CHAR;
static int Graphic_ULCorn_Char = SLSMG_ULCORN_CHAR;

#define ALT_CHAR_SET_MODE	1
#define SIMULATED_CHAR_SET_MODE	2

static int Graphic_Chars_Mode = ALT_CHAR_SET_MODE;

#define HEADER_TABLE_SIZE 1250
static Slrn_Header_Type *Header_Table[HEADER_TABLE_SIZE];
static int Headers_Hidden_Mode = 1;
static int Quotes_Hidden = 0;
static char *Super_Cite_Regexp = "^[^A-Za-z0-9]*\"\\([-_a-zA-Z/]+\\)\" == .+";
static int Do_Rot13;
static int Perform_Scoring;
static int Largest_Header_Number;
static int Article_Window_Nrows;
static int Article_Window_HScroll;
static int Article_Wrapped;
static SLKeymap_Function_Type *Art_Functions_Ptr;
static Slrn_Article_Line_Type *Article_Current_Line;
static int Header_Window_HScroll;

static Slrn_Header_Type *At_End_Of_Article;
/* If this variable is NULL, then we are not at the end of an article.  If it
 * points at the current article, the we are at the end of that article.
 * If it points anywhere else, ignore it.
 */

/*}}}*/
/*{{{ static function declarations */

static void toggle_header_formats (void);
static void slrn_art_hangup (int);
static void sort_threads (void);
static void hide_quotes (void);
static void wrap_article (void);
static void unwrap_article (void);
static void art_update_screen (void);
static void art_next_unread (void);
static void thread_headers (void);
static void sort_by_threads (void);
static void sort_by_sorting_mode (void);
static int select_article (int);
static Slrn_Article_Line_Type *unwrap_line (Slrn_Article_Line_Type *);
static void quick_help (void);
static void for_this_tree (Slrn_Header_Type *, void (*)(Slrn_Header_Type *));
static void find_non_hidden_header (void);

static void skip_to_next_group (void);

#if SLRN_HAS_SPOILERS
static void show_spoilers (void);
#endif
/*}}}*/

/*{{{ utility functions */

static int is_blank_line (unsigned char *b) /*{{{*/
{
   b = (unsigned char *) slrn_skip_whitespace ((char *) b);
   return (*b == 0);
}

/*}}}*/

static char *map_char_to_string (int ch) /*{{{*/
{
   static char charbuf[8];
   switch (ch)
     {
      case ' ': 	return "SPACE";
      case '\t': 	return "TAB";
      case '\r': 	return "RETURN";
      case 27: 		return "ESCAPE";
      case 127: 	return "DELETE";
      case 8: 		return "BACKSPACE";
     }

   if (ch < 32)
     {
	sprintf (charbuf, "Ctrl-%c", ch + '@');
	return charbuf;
     }
   
   sprintf (charbuf, "'%c'", ch);
   return charbuf;
}

/*}}}*/

/*}}}*/


/*{{{ SIGWINCH and window resizing functions */
static void art_winch (void) /*{{{*/
{
   static int rows = 0;
   
#if SLRN_HAS_SLANG
   /* Check to see if rows is still 0. If so, this is the first
    * call here and we should run resize_screen_hook to allow it
    * to change the initial Article_Window_Nrows.
    */
   if (rows == 0)
     {
	rows = SLtt_Screen_Rows;
	SLang_run_hooks ("resize_screen_hook", 0);
     }
#endif

   if ((rows != SLtt_Screen_Rows) 
       || (Article_Window_Nrows <= 1)
       || (Article_Window_Nrows > SLtt_Screen_Rows - 3))
     {
	rows = SLtt_Screen_Rows;
	
	Article_Window_Nrows = (3 * rows) / 4;
	if (rows <= 28)
	  Article_Window_Nrows = rows - 8;
     }

   Header_Window_Nrows = rows - 3;
   if (Article_Visible)
     Header_Window_Nrows -= Article_Window_Nrows;

   if (Header_Window_Nrows < 0) Header_Window_Nrows = 1;
   if (Article_Window_Nrows < 0) Article_Window_Nrows = 1;
   
   Slrn_Article_Window.nrows = Article_Window_Nrows;
   Slrn_Header_Window.nrows = Header_Window_Nrows;

   if (Slrn_Wrap_Mode & 0x4) wrap_article ();

   Slrn_Full_Screen_Update = 1;
}

/*}}}*/



static void set_article_visibility (int visible)
{
   if (visible == Article_Visible)
     return;
   Article_Visible = visible;
   art_winch ();
}

static void art_winch_sig (int old_r, int old_c) /*{{{*/
{
   (void) old_c;
   if (old_r != SLtt_Screen_Rows)
     Article_Window_Nrows = 0;
   
   art_winch ();
}

/*}}}*/

static void shrink_window (void) /*{{{*/
{
   if (Article_Visible == 0) return;
   Article_Window_Nrows++;
   art_winch ();
}

/*}}}*/

static void enlarge_window (void) /*{{{*/
{
   if (Article_Visible == 0) return;
   Article_Window_Nrows--;
   art_winch ();
}

/*}}}*/

void slrn_set_article_window_size (int nrows)
{
   Article_Window_Nrows = nrows;
   art_winch ();
}

/*}}}*/
/*{{{ header hash functions */
static void delete_hash_table (void) /*{{{*/
{
   SLMEMSET ((char *) Header_Table, 0, sizeof (Header_Table));
}

/*}}}*/

static void make_hash_table (void) /*{{{*/
{
   Slrn_Header_Type *h;
   delete_hash_table ();
   h = Slrn_First_Header;
   while (h != NULL)
     {
	h->hash_next = Header_Table[h->hash % HEADER_TABLE_SIZE];
	Header_Table[h->hash % HEADER_TABLE_SIZE] = h;
	h = h->real_next;
     }
}

/*}}}*/

static void free_header (Slrn_Header_Type *h)
{
   slrn_free (h->subject);
   slrn_free ((char *) h);
}

/*}}}*/

/*{{{ article line specific functions */

static void find_article_line_num (void) /*{{{*/
{
   Slrn_Article_Line_Type *l;
   
   /* Make sure Article_Current_Line is not hidden */
   l = Article_Current_Line;
   while ((l != NULL) && (l->flags & HIDDEN_LINE))
     l = l->prev;
   if (l == NULL)
     l = Article_Current_Line;
   while ((l != NULL) && (l->flags & HIDDEN_LINE))
     l = l->next;
   
   Article_Current_Line = l;

   Slrn_Article_Window.current_line = (SLscroll_Type *) Article_Current_Line;
   
   /* Force current line to be at top of window */
   Slrn_Article_Window.top_window_line = Slrn_Article_Window.current_line;
   Slrn_Full_Screen_Update = 1;
   
   SLscroll_find_line_num (&Slrn_Article_Window);
}

/*}}}*/

static void init_article_window_struct (void) /*{{{*/
{
   Slrn_Article_Window.hidden_mask = HIDDEN_LINE;
   Slrn_Article_Window.current_line = (SLscroll_Type *) Article_Current_Line;
   Slrn_Article_Window.cannot_scroll = SLtt_Term_Cannot_Scroll;
   Slrn_Article_Window.lines = (SLscroll_Type *) Slrn_Article_Lines;
   Slrn_Article_Window.border = Slrn_Article_Window_Border;
   art_winch ();		       /* set nrows element */
   find_article_line_num ();
}

/*}}}*/

static void free_article (void) /*{{{*/
{
   Slrn_Article_Line_Type *l, *next;
   
   l = Slrn_Article_Lines;
   while (l != NULL)
     {
	SLFREE (l->buf);
	next = l->next;
	SLFREE (l);
	l = next;
     }
   
   SLMEMSET((char *) &Slrn_Article_Window, 0, sizeof(SLscroll_Window_Type));
   
   Article_Current_Line = Slrn_Article_Lines = NULL;
   Header_Showing = NULL;
   Article_Wrapped = 0;
   set_article_visibility (0);
}

/*}}}*/

typedef struct _Visible_Header_Type
{
   char *header;
   unsigned int len;
   struct _Visible_Header_Type *next;
}
Visible_Header_Type;

Visible_Header_Type *Visible_Headers;

static void free_visible_header_list (void)
{
   while (Visible_Headers != NULL)
     {
	Visible_Header_Type *next;
	next = Visible_Headers->next;
	SLang_free_slstring (Visible_Headers->header);   /* NULL ok */
	SLfree ((char *) Visible_Headers);
	Visible_Headers = next;
     }
}

int slrn_set_visible_headers (char *headers)
{
   char buf[256];
   unsigned int nth;

   free_visible_header_list ();

   nth = 0;
   while (-1 != SLextract_list_element (headers, nth, ',', buf, sizeof(buf)))
     {
	Visible_Header_Type *next;
	
	next = (Visible_Header_Type *) SLmalloc (sizeof (Visible_Header_Type));
	if (next == NULL)
	  return -1;
	memset ((char *) next, 0, sizeof(Visible_Header_Type));
	if (NULL == (next->header = SLang_create_slstring (buf)))
	  {
	     SLfree ((char *) next);
	     return -1;
	  }
	next->len = strlen (buf);
	next->next = Visible_Headers;
	Visible_Headers = next;
	
	nth++;
     }
   return 0;
}

/* Does NOT update line numbers */
static void hide_art_headers (void) /*{{{*/
{
   Slrn_Article_Line_Type *l = Slrn_Article_Lines;
   char ch;
   
   while ((l != NULL) && ((ch = *l->buf) != 0))
     {
	int hide_header;
	Visible_Header_Type *v;

	l->flags = HEADER_LINE;
	ch |= 0x20;

	hide_header = 1;

	v = Visible_Headers;
	while (v != NULL)
	  {
	     char chv = (0x20 | v->header[0]);
	     
	     if ((chv == ch)
		 && (0 == slrn_case_strncmp ((unsigned char *)l->buf, 
					     (unsigned char *)v->header, 
					     v->len)))
	       {
		  hide_header = 0;
		  break;
	       }
	     
	     v = v->next;
	  }

	do
	  {
	     l->flags |= HEADER_LINE;
	     if (Headers_Hidden_Mode && hide_header)
	       {
		  l->flags |= HIDDEN_LINE;
	       }
	     else l->flags &= ~HIDDEN_LINE;
	     
	     l = l->next;
	  }
	while ((l != NULL) && ((*l->buf == ' ') || (*l->buf == '\t')));
     }

   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void skip_quoted_text (void) /*{{{*/
{
   Slrn_Article_Line_Type *l = Article_Current_Line;
   
   /* look for a quoted line */
   while (l != NULL)
     {
	if ((l->flags & HIDDEN_LINE) == 0)
	  {
	     Article_Current_Line = l;
	     if (l->flags & QUOTE_LINE) break;
	  }
	l = l->next;
     }
   
   /* Now we are either at the end of the buffer or on a quote line. Skip
    * past other quote lines.
    */
   
   if (l == NULL)
     {
	find_article_line_num ();
	return;
     }
   
   l = l->next;
   
   while (l != NULL)
     {
	if (l->flags & HIDDEN_LINE)
	  {
	     l = l->next;
	     continue;
	  }
	Article_Current_Line = l;
	if ((l->flags & QUOTE_LINE) == 0)
	  {
	     /* Check to see if it is blank */
	     if (is_blank_line ((unsigned char *) l->buf) == 0) break;
	  }
	l = l->next;
     }
   
   find_article_line_num ();
}

/*}}}*/

static void skip_digest_forward (void) /*{{{*/
{
   Slrn_Article_Line_Type *l;
   int num_passes;
   
   /* We are looking for:
    * <blank line>  (actually, most digests do not have this-- even the FAQ that suggests it!!)
    * ------------------------------
    * <blank line>
    * Subject: something
    *
    * In fact, most digests do not conform to this.  So, I will look for:
    * <blank line>
    * Subject: something
    *
    * Actually, most faqs, etc... do not support this.  So, look for any line
    * beginning with a number on second pass.  Sigh.
    */
   num_passes = 0;
   while (num_passes < 2)
     {
	l = Article_Current_Line;
	if (l != NULL) l = l->next;
	
	while (l != NULL)
	  {
	     char ch;
	     char *buf;
	     
	     if ((l->flags & HIDDEN_LINE) || (l->flags & HEADER_LINE))
	       {
		  l = l->next;
		  continue;
	       }
	     
	     buf = l->buf;
	     if (num_passes == 0)
	       {
		  if ((strncmp ("Subject:", buf, 8))
		      || (((ch = buf[8]) != ' ') && (ch != '\t')))
		    {
		       l = l->next;
		       continue;
		    }
	       }
	     else
	       {
		  ch = *buf;
		  if ((ch > '9') || (ch < '0'))
		    {
		       l = l->next;
		       continue;
		    }
	       }
	     
	     Article_Current_Line = l;
	     find_article_line_num ();
	     return;
	  }
	num_passes++;
     }
   slrn_error ("No next digest.");
}

/*}}}*/

static int try_supercite (void) /*{{{*/
{
   Slrn_Article_Line_Type *l = Slrn_Article_Lines, *last, *lsave;
   static unsigned char compiled_pattern_buf[256];
   static SLRegexp_Type re;
   unsigned char *b;
   int count;
   char name[32];
   unsigned int len;
   int ret;
   
   re.pat = (unsigned char *) Super_Cite_Regexp;
   re.buf = compiled_pattern_buf;
   re.case_sensitive = 1;
   re.buf_len = sizeof (compiled_pattern_buf);
   
   /* skip header --- I should look for Xnewsreader: gnus */
   while ((l != NULL) && (*l->buf != 0)) l = l->next;
   
   if ((*compiled_pattern_buf == 0) && SLang_regexp_compile (&re))
     return -1;
   
   /* look at the first 15 lines on first attempt.
    * After that, scan the whole buffer looking for more citations */
   count = 15;
   lsave = l;
   ret = -1;
   while (1)
     {
	while (count && (l != NULL))
	  {
	     if ((l->flags & QUOTE_LINE) == 0)
	       {
		  if (NULL != slrn_regexp_match (&re, l->buf))
		    {
		       l->flags |= QUOTE_LINE;
		       break;
		    }
	       }
	     l = l->next;
	     count--;
	  }
	
	if ((l == NULL) || (count == 0)) return ret;
	
	/* Now find out what is used for citing. */
	b = (unsigned char *) l->buf + re.beg_matches[1];
	len = re.end_matches[1];
	if (len > sizeof (name) - 2) return ret;
	
	ret = 0;
	strncpy (name, (char *) b, len); name[len] = 0;
	/* strcat (name, ">");
	 len++; */
	
	while (l != NULL)
	  {
	     unsigned char ch;
	     
	     b = (unsigned char *) l->buf;
	     last = l;
	     l = l->next;
	     if (last->flags & QUOTE_LINE) continue;
	     
	     b = (unsigned char *) slrn_skip_whitespace ((char *) b);

	     if (!strncmp ((char *) b, name, len)
		 && (((ch = b[len] | 0x20) < 'a')
		     || (ch > 'z')))
	       {
		  last->flags |= QUOTE_LINE;
		  
		  while (l != NULL)
		    {
		       b = (unsigned char *) slrn_skip_whitespace (l->buf);
		       if (strncmp ((char *) b, name, len)
			   || (((ch = b[len] | 0x20) >= 'a')
			       && (ch <= 'z')))
			 break;
		       l->flags |= QUOTE_LINE;
		       l = l->next;
		    }
	       }
	  }
	count = -1;
	l = lsave;
     }
}

/*}}}*/

#if SLRN_HAS_SPOILERS
static void mark_spoilers (void) /*{{{*/
{
   Slrn_Article_Line_Type *l = Slrn_Article_Lines;
   int spoiler = 0;
   
   /* skip header */
   while ((l != NULL) && (l->flags & HEADER_LINE))
     l = l->next;
   
   while (l != NULL)
     {
	if ((l->buf[0] == 12) && (l->buf[1] == 0))
	  {
	     spoiler = 1;
	  }
	else if (spoiler)
	  {
	     l->flags |= SPOILER_LINE;
	  }
	l = l->next;
     }   
}

/*}}}*/
#endif

static void mark_quotes (void) /*{{{*/
{
   Slrn_Article_Line_Type *l, *last;
   unsigned char *b;
   SLRegexp_Type **r;
   
   if (0 == try_supercite ())
     {
	/* return; */
     }
   
   if (Slrn_Ignore_Quote_Regexp[0] == NULL) return;
   
   /* skip header */
   l = Slrn_Article_Lines;
   while ((l != NULL) && (*l->buf != 0)) l = l->next;
   
   while (l != NULL)
     {
	unsigned int min_len;
	b = (unsigned char *) l->buf;
	min_len = strlen ((char *) b);
	last = l;
	
	l = l->next;		       /* if first time through, this skips
					* blank line at at last header.
					*/
	
	r = Slrn_Ignore_Quote_Regexp;
	
	while (*r != NULL)
	  {
	     SLRegexp_Type *re;
	     
	     re = *r;
	     if ((re->min_length <= min_len) &&
		 (NULL != SLang_regexp_match (b, min_len, re)))
	       {
		  last->flags |= QUOTE_LINE;
		  while (l != NULL)
		    {
		       b = (unsigned char *) l->buf;
		       min_len = strlen ((char *) b);
		       if ((re->min_length <= min_len)
			   && (NULL == SLang_regexp_match (b, min_len, re)))
			 {
			    
			    /* Here it might be a good idea to add:
			     * l = l->prev;
			     */
			    break;
			 }
		       l->flags |= QUOTE_LINE;
		       l = l->next;
		    }
		  break;
	       }
	     r++;
	  }
     }
}

/*}}}*/

static void mark_signature (void) /*{{{*/
{
   Slrn_Article_Line_Type *l = Slrn_Article_Lines;
   int nmax = 10;
   
   if (l == NULL) return;
   /* go to end of article */
   while (l->next != NULL) l = l->next;
   
   /* skip back untill "-- " seen.  Assume that it is not more than
    * 10 lines long.
    */
   
   while ((l != NULL) && nmax--
	  && ((*l->buf != '-')
	      || strcmp (l->buf, "-- ")))
     l = l->prev;
   
   if (nmax == -1) return;
   
   while (l != NULL)
     {
        l->flags |= SIGNATURE_LINE;
        l->flags &= ~(
		      QUOTE_LINE  /* if in a signature, not a quote */
#if SLRN_HAS_SPOILERS
		      | SPOILER_LINE     /* not a spoiler */
#endif
		      );
	l = l->next;
     }
}

/*}}}*/

   
char *slrn_extract_header (char *hdr, unsigned int len) /*{{{*/
{
   Slrn_Article_Line_Type *l;
   
   if (Slrn_Current_Header == NULL)
     return NULL;

   if ((len > 2) && (hdr[len-1] == ' ') && (hdr[len-2] == ':'))
     len--;

   if (Slrn_Current_Header != Header_Showing)
     {
	if (0 == slrn_case_strncmp ((unsigned char *)"From: ", (unsigned char *)hdr, len))
	  return slrn_skip_whitespace (Slrn_Current_Header->from);
	if (0 == slrn_case_strncmp ((unsigned char *)"Subject: ", (unsigned char *)hdr, len))
	  return Slrn_Current_Header->subject;
	if (0 == slrn_case_strncmp ((unsigned char *)"Message-Id: ", (unsigned char *)hdr, len))
	  return slrn_skip_whitespace (Slrn_Current_Header->msgid);
	if (0 == slrn_case_strncmp ((unsigned char *)"Date: ", (unsigned char *)hdr, len))
	  return Slrn_Current_Header->date;
	if (0 == slrn_case_strncmp ((unsigned char *)"References: ", (unsigned char *)hdr, len))
	  return slrn_skip_whitespace (Slrn_Current_Header->refs);
	if (0 == slrn_case_strncmp ((unsigned char *)"Xref: ", (unsigned char *)hdr, len))
	  return Slrn_Current_Header->xref;
	if (0 == slrn_case_strncmp ((unsigned char *)"Lines: ", (unsigned char *)hdr, len))
	  {
	     static char lines_buf[16];
	     sprintf (lines_buf, "%d", Slrn_Current_Header->lines);
	     return lines_buf;
	  }
	return NULL;
     }
   
   l = Slrn_Article_Lines;

   while ((l != NULL)
	  && (*l->buf != 0))
     {
	if (0 == slrn_case_strncmp ((unsigned char *) hdr,
				    (unsigned char *) l->buf, len))
	  {
	     char *result;

	     if ((l->next != NULL) && (l->next->flags & WRAPPED_LINE))
	       unwrap_line (l->next);

	     /* Return the data after the colon */
	     result = slrn_strchr (l->buf, ':');
	     if (result == NULL) 
	       result = l->buf + len;
	     else result += 1;

	     return slrn_skip_whitespace (result);
	  }
	l = l->next;
     }
   return NULL;
}

/*}}}*/

/*{{{ wrap article functions  */
/* The input line is assumed to be the first wrapped portion of a line.  For
 * example, if a series of lines denoted as A/B is wrapped: A0/A1/A2/B0/B1,
 * then to unwrap A, A1 is passed and B0 is returned.
 */
static Slrn_Article_Line_Type *unwrap_line (Slrn_Article_Line_Type *l) /*{{{*/
{
   char *b;
   Slrn_Article_Line_Type *next, *ll;
	     
   ll = l->prev;
   b = ll->buf;
   do 
     {
	b += strlen (b);
	strcpy (b, l->buf + 1);   /* skip the space at beginning of
				   * the wrapped line. */
	next = l->next;
	SLFREE (l->buf);
	SLFREE (l);
	if (l == Article_Current_Line) Article_Current_Line = ll;
	l = next;
     }
   while ((l != NULL) && (l->flags & WRAPPED_LINE));
	     
   ll->next = l;
   if (l != NULL) l->prev = ll;
   return l;
}

/*}}}*/

int Slrn_Wrap_Mode = 3;

static void unwrap_article (void)
{
   Slrn_Article_Line_Type *l;

   if (Article_Wrapped == 0) return;
   Article_Wrapped = 0;

   if (Header_Showing == NULL) return;
   
   l = Slrn_Article_Lines;
   while (l != NULL)
     {
	if (l->flags & WRAPPED_LINE)
	  l = unwrap_line (l);
	else l = l->next;
     }
   
   Slrn_Full_Screen_Update = 1;
   find_article_line_num ();
}

static void wrap_article (void) /*{{{*/
{
   unsigned int len;
   unsigned char *buf, ch;
   Slrn_Article_Line_Type *l;
   unsigned int wrap_mode = Slrn_Wrap_Mode;

   if (Header_Showing == NULL) return;
   
   (void) unwrap_article ();

   l = Slrn_Article_Lines;

   while (l != NULL)
     {
	unsigned char header_char_delimiter = 0;

	if (l->flags & HEADER_LINE) 
	  {
	     if ((wrap_mode & 1) == 0)
	       {
		  l = l->next;
		  continue;
	       }

	     if (0 == slrn_case_strncmp ((unsigned char *)"Path: ",
					 (unsigned char *)l->buf, 6))
	       header_char_delimiter = '!';
	     else if (0 == slrn_case_strncmp ((unsigned char *) "Newsgroups: ",
					      (unsigned char *)l->buf, 12))
	       header_char_delimiter = ',';
	  }
	else if (l->flags & QUOTE_LINE)
	  {
	     if ((wrap_mode & 2) == 0)
	       {
		  l = l->next;
		  continue;
	       }
	  }

	len = 0;
	buf = (unsigned char *) l->buf;
	ch = *buf;
	while (ch != 0)
	  {
	     if ((ch == '\t') && (SLsmg_Tab_Width > 0))
	       {
		  len += SLsmg_Tab_Width;
		  len -= len % SLsmg_Tab_Width;
	       }
	     else if (((ch >= ' ') && (ch < 127))
		      || (ch >= (unsigned char) SLsmg_Display_Eight_Bit))
	       len++;
	     else
	       {
		  len += 2;
		  if (ch & 0x80) len++;
	       }

	     if (len > (unsigned int) SLtt_Screen_Cols)
	       {
		  Slrn_Article_Line_Type *new_l;
		  unsigned char *buf0, *lbuf;

		  /* Try to break the line on a word boundary.  
		   * For now, I will only break on space characters.
		   */
		  buf0 = buf;
		  lbuf = (unsigned char *) l->buf;

		  lbuf += 1;	       /* avoid space at beg of line */

		  while (buf0 > lbuf)
		    {
		       if ((*buf0 == ' ') || (*buf0 == '\t')
			   || (header_char_delimiter 
			       && (*buf0 == header_char_delimiter)))
			 {
			    buf = buf0;
			    break;
			 }
		       buf0--;
		    }

		  if (buf0 == lbuf)
		    {
		       /* Could not find a place to break the line.  Ok, so
			* we will not break this.  Perhaps it is a URL.  
			* If not, it is a long word and who cares about it.
			*/
		       while (((ch = *buf) != 0)
			      && (ch != ' ') && (ch != '\t'))
			 buf++;

		       if (ch == 0)
			 continue;
		    }
		       
		  /* Start wrapped lines with a space.  To do this, I will
		   * _temporally_ modify the previous character for the purpose
		   * of creating the new space.
		   */
		  buf--;
		  ch = *buf;
		  *buf = ' ';
		  
		  new_l = (Slrn_Article_Line_Type *) slrn_malloc (sizeof (Slrn_Article_Line_Type), 1, 1);
		  if (new_l == NULL)
		    return;
		  
		  if (NULL == (new_l->buf = slrn_strmalloc ((char *)buf, 1)))
		    {
		       SLFREE (new_l);
		       return;
		    }

		  *buf++ = ch;
		  *buf = 0;
		  
		  new_l->next = l->next;
		  new_l->prev = l;
		  l->next = new_l;
		  if (new_l->next != NULL) new_l->next->prev = new_l;

		  new_l->flags = l->flags | WRAPPED_LINE;
		  l = new_l;
		  buf = (unsigned char *) new_l->buf;
		  len = 0;
		  Article_Wrapped = 1;
	       }
	     else buf++;
	     
	     ch = *buf;
	  }
	l = l->next;
     }
   if (Quotes_Hidden) hide_quotes ();
   Slrn_Full_Screen_Update = 1;
   find_article_line_num ();
}

/*}}}*/

static void toggle_wrap_article (void)
{
   if (Article_Wrapped)
     unwrap_article ();
   else
     {
	if (Slrn_Prefix_Arg_Ptr != NULL) Slrn_Wrap_Mode = 0x7F;
	Slrn_Prefix_Arg_Ptr = NULL;
	wrap_article ();
     }
}


/*}}}*/

Slrn_Article_Line_Type *slrn_search_article (char *string, /*{{{*/
					     char **ptrp,
					     int is_regexp,
					     int set_current_flag)
{
   SLsearch_Type st;
   Slrn_Article_Line_Type *l;
   char *ptr;
   int ret;
   SLRegexp_Type *re = NULL;
   
   if (-1 == (ret = select_article (1)))
     return NULL;
   
   if (Article_Current_Line == NULL)
     return NULL;
   
   if (is_regexp)
     {
	re = slrn_compile_regexp_pattern (string);
	if (re == NULL)
	  return NULL;
     }
   else SLsearch_init (string, 1, 0, &st);

   l = Article_Current_Line;
   
   if (ret == 1)
     l = l->next;
   
   while (l != NULL)
     {
	if ((l->flags & HIDDEN_LINE) == 0)
	  {
	     if (is_regexp)
	       ptr = (char *) slrn_regexp_match (re, l->buf);
	     else
	       ptr = (char *) SLsearch ((unsigned char *) l->buf,
					(unsigned char *) l->buf + strlen (l->buf),
					&st);
	     
	     if (ptr != NULL)
	       {
		  if (ptrp != NULL) *ptrp = ptr;
		  if (set_current_flag)
		    {
		       Article_Current_Line = l;
		       find_article_line_num ();
		    }
		  break;
	       }
	  }
	l = l->next;
     }
      
   return l;
}

/*}}}*/

static void free_argc_argv_list (unsigned int argc, char **argv)
{
   while (argc)
     {
	argc--;
	slrn_free (argv[argc]);
     }
}


static char *find_url (char *l_buf, unsigned int *p_len) /*{{{*/
{
   char *ptr, *tmp, ch;
   
   while (NULL != (ptr = slrn_strchr (l_buf, ':')))
     {
	int again;
	
	if ((ptr == l_buf) || (ptr[1] != '/') || (ptr[2] != '/'))
	  {
	     l_buf = ptr + 1;
	     continue;
	  }

	tmp = ptr;
	
	while ((ptr > l_buf) 
	       && isalpha((unsigned char)(*(ptr - 1))))
	  ptr--;

	/* all registered and reserved scheme names are >= 3 chars long */
	if (ptr + 3 > tmp)
	  {
	     l_buf = tmp + 3; /* skip :// */
	     continue;
	  }

	tmp = ptr;
     
	again = 1;
	while (again)
	  {
	     ch = *ptr;
	     
	     switch (ch)
	       {
		case '.':
		  ptr++;
		  ch = *ptr;
		  if ((ch == ' ') || (ch == '\t') 
		      || (ch == '\n') || (ch == 0))
		    {
		       ptr--;
		       again = 0;
		    }
		  break;

		case ' ':
		case '\t':
		case '\n':
		case '\"':
		case '}':
		case '{':
		case ')':
		case ',':
		case ';':
		case '>':
		case '<':
		  
		case 0:
		  again = 0;
		  break;
		  
		default:
		  ptr++;
		  break;
	       }
	  }
	     
	l_buf = ptr;

	if ((*p_len = (unsigned int) (ptr - tmp)) < 7)
	  continue;

	ptr -= 3;

	if ((ptr[0] == ':')
	    && (ptr[1] == '/')
	    && (ptr[2] == '/'))
	  continue;
	  
	return tmp;
     }
   
   return NULL;
}

/*}}}*/

static int extract_urls (unsigned int *argc_ptr, char **argv, unsigned int max_argc,
			 unsigned int *start) /*{{{*/
{
   Slrn_Article_Line_Type *l;
   unsigned int argc;

   l = Slrn_Article_Lines;
   *start = 0;
   
   argc = 0;
   while (l != NULL)
     {
	char *ptr;
	unsigned int len;

	if (l == Article_Current_Line)
	  *start = argc;

	ptr = l->buf;
	while (NULL != (ptr = find_url (ptr, &len)))
	  {
	     if (argc == max_argc)
	       {
		  *argc_ptr = argc;
		  return 0;
	       }

	     if (NULL == (argv[argc] = slrn_strnmalloc (ptr, len, 1)))
	       {
		  free_argc_argv_list (argc, argv);
		  return -1;
	       }
	     argc++;
	     ptr += len;
	  }
	
	l = l->next;
     }
   
   *argc_ptr = argc;
   return 0;
}

/*}}}*/

static void launch_url (char *url) /*{{{*/
{
   char command [4096];
   char *browser, *has_percent;
   int reinit;

   /* add code to allow slrn to handle news: and nntp: URLs here */

   reinit = 0;
   if ((NULL == getenv ("DISPLAY")) 
       || (NULL == (browser = slrn_skip_whitespace (Slrn_X_Browser)))
       || (browser [0] == 0))
     {
	reinit = 1;
	browser = Slrn_NonX_Browser;
     }

   if (browser == NULL)
     {
	slrn_error ("No Web Browser has been defined.");
	return;
     }

   if (reinit == 0)		       /* ==> X_Browser != NULL */
     {
	/* Non_X and X browsers may be same. */
	if ((Slrn_NonX_Browser != NULL) 
	    && (0 == strcmp (Slrn_NonX_Browser, Slrn_X_Browser)))
	  reinit = 1;
     }
   
	
   
   /* Perform a simple-minded syntax check. */
   has_percent = slrn_strchr (browser, '%');
   if (has_percent != NULL)
     {
	if ((has_percent[1] != 's') 
	    || ((has_percent != browser) && (*(has_percent - 1) == '\\')))
	  has_percent = NULL;
     }
   
   if (has_percent != NULL)
     sprintf (command, browser, url);
   else
     /* Is this quoting ok on VMS and OS/2?? */
     sprintf (command, "%s '%s'", browser, url);   

   slrn_posix_system (command, reinit);
}

static void browse_url (void) /*{{{*/
{
   char url[256];
   int selected;
   unsigned int argc, start_argc;
#define MAX_URLS 1024
   char *argv[MAX_URLS];
   int want_edit;

   if (-1 == extract_urls (&argc, argv, MAX_URLS, &start_argc))
     return;
   
   if (0 == argc)
     {
	slrn_error ("No URLs found.");
	return;
     }
   
   selected = 0;
   want_edit = 1;
   
   if (argc > 1)
     selected = slrn_select_list_mode ("URL", argc, argv, start_argc, &want_edit);

   if (-1 == selected)
     {
	free_argc_argv_list (argc, argv);
	return;
     }

   strncpy (url, argv[selected], sizeof (url));
   url[sizeof(url) - 1] = 0;

   free_argc_argv_list (argc, argv);

   if (want_edit 
       && (slrn_read_input ("Browse (^G aborts)", NULL, url, 0, 1) <= 0))
     {
	slrn_error ("Aborted.");
	return;
     }

   launch_url (url);
}

/*}}}*/

static void article_search (void) /*{{{*/
{
   static char search_str[256];
   Slrn_Article_Line_Type *l;
   
   if (slrn_read_input ("Search", search_str, NULL, 0, 0) <= 0) return;
   
   l = slrn_search_article (search_str, NULL, 0, 1);
   
   if (l == NULL) slrn_error ("Not found.");
}

/*}}}*/

/*}}}*/
/*{{{ current article movement functions */


static unsigned int art_lineup_n (unsigned int n) /*{{{*/
{
   if (select_article (1) <= 0) return 0;
   
   n = SLscroll_prev_n (&Slrn_Article_Window, n);
   
   Article_Current_Line = (Slrn_Article_Line_Type *) Slrn_Article_Window.current_line;
   
   /* Force current line to be at top of window */
   Slrn_Article_Window.top_window_line = Slrn_Article_Window.current_line;
   Slrn_Full_Screen_Update = 1;
   return n;
}

/*}}}*/

static void art_pageup (void) /*{{{*/
{
   if (select_article (1) <= 0)
     return;
   /* Since we always require the current line to be at the top of the
    * window, SLscroll_pageup cannot be used.  Instead, do it this way:
    */
   art_lineup_n (Slrn_Article_Window.nrows - 1);
}

/*}}}*/

int slrn_get_next_pagedn_action (void)
{
   Slrn_Header_Type *h;

   if (Slrn_Current_Header == NULL)
     return -1;
   
   if ((Article_Visible == 0)
       || (At_End_Of_Article != Slrn_Current_Header))
     return 0;
   
   h = Slrn_Current_Header->next;
   while (h != NULL)
     {
	if (0 == (h->flags & HEADER_READ))
	  return 1;
	h = h->next;
     }
   
   return 2;
}

   
static void art_pagedn (void) /*{{{*/
{
   unsigned char ch, ch1;
   char *msg = NULL;
   
   if (Slrn_Current_Header == NULL) return;

#if SLRN_HAS_SPOILERS
   if (Spoilers_Visible == Slrn_Current_Header)
     {
	show_spoilers ();
	return;
     }
#endif
   
   
   if ((Article_Visible == 0) || (At_End_Of_Article != Slrn_Current_Header))
     {
	int av = Article_Visible;
	
	At_End_Of_Article = NULL;

	if ((select_article (1) <= 0)
	    || (av == 0))
	  return;

	SLscroll_pagedown (&Slrn_Article_Window);
	Article_Current_Line = (Slrn_Article_Line_Type *) Slrn_Article_Window.current_line;
	/* Force current line to be at top of window */
#if 1
	Slrn_Article_Window.top_window_line = Slrn_Article_Window.current_line;
#endif
	Slrn_Full_Screen_Update = 1;
	return;
     }

   At_End_Of_Article = NULL;

   if (Slrn_Batch) return;
   
   if (Slrn_Current_Header->next == NULL)
     {
	if (Slrn_Query_Next_Group)
	  msg = "At end of article, press %s for next group.";
     }
   else if (Slrn_Query_Next_Article)
     msg = "At end of article, press %s for next unread article.";
   
   if ((ch1 = SLang_Last_Key_Char) == 27) ch1 = ' ';
   ch = ch1;
	
   if (msg != NULL)
     {
	slrn_message_now (msg, map_char_to_string (ch));
	ch = SLang_getkey ();
     }
   
   if (ch == ch1)
     {
	At_End_Of_Article = NULL;
	if (Slrn_Current_Header->next != NULL) art_next_unread ();
	else skip_to_next_group ();
     }
   else SLang_ungetkey (ch);
}
   

/*}}}*/

static void art_lineup (void) /*{{{*/
{
   art_lineup_n (1);
}

/*}}}*/

static void art_bob (void) /*{{{*/
{
   while (0xFFFF == art_lineup_n (0xFFFF));
}

/*}}}*/


static unsigned int art_linedn_n (unsigned int n) /*{{{*/
{
   if (select_article (1) <= 0)
     return 0;

   n = SLscroll_next_n (&Slrn_Article_Window, n);
   Article_Current_Line = (Slrn_Article_Line_Type *) Slrn_Article_Window.current_line;
   
   /* Force current line to be at top of window */
   Slrn_Article_Window.top_window_line = Slrn_Article_Window.current_line;
   Slrn_Full_Screen_Update = 1;
   return n;
}

/*}}}*/


static void art_linedn (void) /*{{{*/
{
   art_linedn_n (1);
}

/*}}}*/

static void art_eob (void) /*{{{*/
{
   while (art_linedn_n (0xFFFF) > 0)
     ;
   (void) art_lineup_n (Slrn_Article_Window.nrows / 2);
}

/*}}}*/


/*}}}*/

/*{{{ Tag functions */

typedef struct /*{{{*/
{
   Slrn_Header_Type **headers;
   unsigned int max_len;
   unsigned int len;
}

/*}}}*/
Num_Tag_Type;

static Num_Tag_Type Num_Tag_List;

static void free_tag_list (void) /*{{{*/
{
   if (Num_Tag_List.headers != NULL)
     {
	SLFREE (Num_Tag_List.headers);
	Num_Tag_List.headers = NULL;
	Num_Tag_List.len = Num_Tag_List.max_len = 0;
     }
}

/*}}}*/

int slrn_goto_num_tagged_header (int *nump) /*{{{*/
{
   unsigned int num;
   
   num = (unsigned int) *nump;
   num--;
   
   if (num >= Num_Tag_List.len)
     return 0;
   
   if (Num_Tag_List.headers == NULL)
     return 0;
   
   if (-1 == slrn_goto_header (Num_Tag_List.headers[num], 0))
     return 0;
   
   Slrn_Full_Screen_Update = 1;
   return 1;
}

/*}}}*/

static void num_tag_header (void) /*{{{*/
{
   unsigned int len;
   
   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   if (Num_Tag_List.headers == NULL)
     {
	Slrn_Header_Type **headers;
	unsigned int max_len = 20;
	
	headers = (Slrn_Header_Type **) slrn_malloc (max_len * sizeof (Slrn_Header_Type),
						     0, 1);
	if (headers == NULL)
	  return;

	Num_Tag_List.max_len = max_len;
	Num_Tag_List.headers = headers;
	Num_Tag_List.len = 0;
     }
   
   if (Num_Tag_List.max_len == Num_Tag_List.len)
     {
	Slrn_Header_Type **headers = Num_Tag_List.headers;
	unsigned int max_len = Num_Tag_List.max_len + 20;
	
	headers = (Slrn_Header_Type **) slrn_realloc ((char *)headers,
						      max_len * sizeof (Slrn_Header_Type),
						      1);
	if (headers == NULL)
	  return;
	
	Num_Tag_List.max_len = max_len;
	Num_Tag_List.headers = headers;
     }
   
   Slrn_Full_Screen_Update = 1;
   if ((Slrn_Current_Header->flags & HEADER_NTAGGED) == 0)
     {
	Num_Tag_List.headers[Num_Tag_List.len] = Slrn_Current_Header;
	Num_Tag_List.len += 1;
	Slrn_Current_Header->tag_number = Num_Tag_List.len;
	Slrn_Current_Header->flags |= HEADER_NTAGGED;
	(void) slrn_header_down_n (1, 0);
	return;
     }
   
   /* It is already tagged.  So, what do we do.  The most sensible thing to
    * do is to simply give this header the last number and renumber the others
    * that follow it.  If it is the last one, untag it.
    */
   if (Slrn_Current_Header->tag_number == Num_Tag_List.len)
     {
	Num_Tag_List.len -= 1;
	Slrn_Current_Header->tag_number = 0;
	Slrn_Current_Header->flags &= ~HEADER_NTAGGED;
	return;
     }
   
   for (len = Slrn_Current_Header->tag_number + 1; len <= Num_Tag_List.len; len++)
     {
	Slrn_Header_Type *h = Num_Tag_List.headers[len - 1];
	Num_Tag_List.headers[len - 2] = h;
	h->tag_number -= 1;
     }
   Num_Tag_List.headers[len - 2] = Slrn_Current_Header;
   Slrn_Current_Header->tag_number = len - 1;
}

/*}}}*/

static void num_untag_headers (void) /*{{{*/
{
   unsigned int len;
   for (len = 1; len <= Num_Tag_List.len; len++)
     {
	Slrn_Header_Type *h = Num_Tag_List.headers[len - 1];
	h->flags &= ~HEADER_NTAGGED;
	h->tag_number = 0;
     }
   Num_Tag_List.len = 0;
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void toggle_one_header_tag (Slrn_Header_Type *h) /*{{{*/
{
   if (h == NULL) return;
   if (h->flags & HEADER_TAGGED)
     {
	h->flags &= ~HEADER_TAGGED;
     }
   else h->flags |= HEADER_TAGGED;
}

/*}}}*/

static void toggle_header_tag (void) /*{{{*/
{
   if (Slrn_Prefix_Arg_Ptr != NULL)
     {
	Slrn_Header_Type *h;
	
	Slrn_Prefix_Arg_Ptr = NULL;
	h = Headers;
	while (h != NULL)
	  {
	     h->flags &= ~HEADER_TAGGED;
	     h = h->next;
	  }
	Slrn_Full_Screen_Update = 1;
	return;
     }

   if ((Slrn_Current_Header->parent != NULL)/* in middle of thread */
       || (Slrn_Current_Header->child == NULL)/* At top with no child */
       /* or at top with child showing */
       || (0 == (Slrn_Current_Header->child->flags & HEADER_HIDDEN)))
     {
	toggle_one_header_tag (Slrn_Current_Header);
     }
   else
     {
	for_this_tree (Slrn_Current_Header, toggle_one_header_tag);
     }
   (void) slrn_header_down_n (1, 0);
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

int slrn_prev_tagged_header (void) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_Current_Header;
   
   if (h == NULL) return 0;
   
   while (h->prev != NULL)
     {
	h = h->prev;
	if (h->flags & HEADER_TAGGED)
	  {
	     slrn_goto_header (h, 0);
	     return 1;
	  }
     }
   return 0;
}

/*}}}*/

int slrn_next_tagged_header (void) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_Current_Header;
   
   if (h == NULL) return 0;
   
   while (h->next != NULL)
     {
	h = h->next;
	if (h->flags & HEADER_TAGGED)
	  {
	     slrn_goto_header (h, 0);
	     return 1;
	  }
     }
   return 0;
}

/*}}}*/

/*}}}*/
/*{{{ Header specific functions */

static void find_header_line_num (void) /*{{{*/
{
   Slrn_Full_Screen_Update = 1;
   find_non_hidden_header ();
   Slrn_Header_Window.lines = (SLscroll_Type *) Headers;
   Slrn_Header_Window.current_line = (SLscroll_Type *) Slrn_Current_Header;
   SLscroll_find_line_num (&Slrn_Header_Window);
}

/*}}}*/

static void init_header_window_struct (void) /*{{{*/
{
   Slrn_Header_Window.nrows = 0;
   Slrn_Header_Window.hidden_mask = HEADER_HIDDEN;
   Slrn_Header_Window.current_line = (SLscroll_Type *) Slrn_Current_Header;
   
   Slrn_Header_Window.cannot_scroll = SLtt_Term_Cannot_Scroll;
   Slrn_Header_Window.border = 1;
   
   if (Slrn_Scroll_By_Page)
     {
	/* Slrn_Header_Window.border = 0; */
	Slrn_Header_Window.cannot_scroll = 2;
     }

   Slrn_Header_Window.lines = (SLscroll_Type *) Headers;
   art_winch ();		       /* get row information correct */
   
   find_header_line_num ();
}

/*}}}*/

static Slrn_Header_Type *find_header_from_serverid (int id) /*{{{*/
{
   Slrn_Header_Type *h;
   
   h = Slrn_First_Header;
   while (h != NULL)
     {
	if (h->number > id) return NULL;
	if (h->number == id) break;
	h = h->real_next;
     }
   return h;
}

/*}}}*/

static void kill_cross_references (Slrn_Header_Type *h) /*{{{*/
{
   char *b;
   char group[256], *g;
   long num;
   
   if ((h->xref == NULL) || (*h->xref == 0))
     {
	if ((Header_Showing != h)
	    || (NULL == (b = slrn_extract_header ("Xref: ", 6))))
	  {
	     return;
	  }
     }
   else b = h->xref;
   
   
   /* The format appears to be:
    * Xref: machine group:num group:num...
    */
   
   /* skip machine name */
   while (*b > ' ') b++;
   
   while (*b != 0)
     {
	while (*b == ' ') b++;
	if (*b == 0) break;
	
	/* now we are looking at the groupname */
	g = group;
	while (*b && (*b != ':')) *g++ = *b++;
	if (*b++ == 0) break;
	*g = 0;
	num = atoi (b);
	while ((*b <= '9') && (*b >= '0')) b++;
	if ((num != h->number)
	    || strcmp (group, Slrn_Current_Group_Name))
	  slrn_mark_article_as_read (group, num);
     }
}

/*}}}*/

static void for_all_headers (void (*func)(Slrn_Header_Type *), int all) /*{{{*/
{
   Slrn_Header_Type *h, *end;
   
   Slrn_Full_Screen_Update = 1;

   if (func == NULL) return;
   
   if (all) end = NULL; else end = Slrn_Current_Header;
   
   h = Headers;
   
   while (h != end)
     {
	(*func)(h);
	h = h->next;
     }
}

/*}}}*/

int slrn_goto_header (Slrn_Header_Type *header, int read_flag) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_First_Header;
   
   while ((h != NULL) && (h != header))
     h = h->real_next;

   if (h == NULL) return -1;
   
   Slrn_Current_Header = h;
   if (h->flags & HEADER_HIDDEN) slrn_uncollapse_this_thread (h, 0);
   find_header_line_num ();
   
   if (read_flag) select_article (1);
   return 0;
}

/*}}}*/

/*{{{ parse_from  */

static char *parse_from (char *from) /*{{{*/
{
   static char buf[256], *b;
   char ch, ch1;
   
   /* Here I am assume the following:
    * 1.  parenthesis and double quotes are comments.
    * 2.  If a name is in <> it is given precedence
    */
   
   if (from == NULL) return NULL;
   from = slrn_skip_whitespace (from);
   
   b = buf;
   
   /* strip comments */
   while ((ch = *from++) != 0)
     {
	if (ch == '"')
	  {
	     ch1 = ch;
	     while (((ch = *from++) != 0) && (ch != ch1)) 
	       ;
	     if (ch == 0) return NULL;
	  }
	else if (ch == '(')
	  {
	     int nump = 1;
	     ch1 = ')';
	     
	     while ((ch = *from++) != 0)
	       {
		  if (ch == '(') nump++;
		  else if (ch == ch1)
		    {
		       nump--;
		       if (nump == 0) break;
		    }
	       }
	     if (ch == 0) return NULL;
	  }
	else if (ch == ')') return NULL;
	else if (ch != ' ') *b++ = ch;
     }
   *b = 0;
   
   from = b = buf;
   
   while ((ch = *from++) != 0)
     {
	if (ch == '<')
	  {
	     while (((ch = *from++) != 0) && (ch != '>'))
	       {
		  *b++ = ch;
	       }
	     if (ch == 0) return NULL;
	     *b = 0;
	     break;
	  }
     }
   return buf;
}

/*}}}*/


/*}}}*/

/*{{{ fixup_realname */
static void fixup_realname (Slrn_Header_Type *h) /*{{{*/
{
   char *p, *pmin, *pmax;
   if ((h->realname == NULL) || (h->realname_len == 0)) return;
   pmin = h->realname;
   p = pmin + (h->realname_len - 1);
   while ((p >= pmin) && ((*p == ' ') || (*p == '\t'))) p--;
   p++;
   h->realname_len = (unsigned int) (p - pmin);
   
   /* Check if realname is enclosed in "" and if so strip them off.
    * Leave "" alone though.
    */
   if ((p > pmin + 2) &&
       (*pmin == '"') && (*(p - 1) == '"'))
     {
	h->realname++;
	h->realname_len -= 2;
     }
   
   /* Look for a character in the range [A-Za-z].  If none, found use from.
    */
   p = h->realname;
   pmax = p + h->realname_len;
   
   /* Reject a realname of root */
   if ((4 != h->realname_len)
       || (0 != strncmp (p, "root", 4)))
     {	
	while (p < pmax)
	  {
	     char ch = *p;
	     
	     if (isalpha ((unsigned char)ch)) return;
	     p++;
	  }
     }

   h->realname = parse_from (h->from);
   
   if (h->realname == NULL)
     h->realname = h->from;
   
   h->realname_len = strlen (h->realname);
}

/*}}}*/

/*}}}*/

/*{{{ get_header_real_name */

static void get_header_real_name (Slrn_Header_Type *h) /*{{{*/
{
   register char *f, *f0, *f1;
   register char ch;
   char *from = h->from;
   int n;
   
   f = from;
   while (((ch = *f) != 0) && (ch != '<') && (ch != '(')) f++;
   if (ch == '(')
     {
	/* Look for a '<'.  There is some probability that this is of the
	 * form:  John Doe (800) 555-1212 <doe@nowhere.com>
	 */
	f0 = f;
	f1 = f + strlen (f);
	while ((f1 > f) && (*f1 != ')') && (*f1 != '<')) f1--;
	if (*f1 == '<')
	  {
	     ch = '<';
	     f = f1;
	  }
     }
   
   if (ch != '(')
     {
	if (ch == '<')
	  {
	     f1 = slrn_skip_whitespace (from);
	     if (*f1 == '<')
	       {
		  f = f1 + 1;
		  from = f;
		  while (((ch = *f) != 0) && (ch != '>')) f++;
	       }
	  }
	h->realname = from;
	h->realname_len = (unsigned int) (f - from);
	fixup_realname (h);
	return;
     }
   
   f1 = f;
   f0 = f + 1;
   f0 = slrn_skip_whitespace (f0);
   if (*f0 == ')')
     {
	h->realname = from;
	h->realname_len = (unsigned int) (f1 - from);
	fixup_realname (h);
	return;
     }
   f = f0;
   n = 0;
   while ((ch = *f) != 0)
     {
	if (ch == '(') n++;
	else if (ch == ')')
	  {
	     if (n == 0) break;
	     n--;
	  }
	f++;
     }
   h->realname = f0;
   h->realname_len = (unsigned int) (f - f0);
   fixup_realname (h);
}

/*}}}*/

/*}}}*/

static void extract_real_names (void) /*{{{*/
{
   Slrn_Header_Type *h = Headers;
   while (h != NULL)
     {
	get_header_real_name (h);
	h = h->next;
     }
}

/*}}}*/

static Slrn_Header_Type *find_header_from_msgid (char *r0, char *r1) /*{{{*/
{
   unsigned long hash;
   Slrn_Header_Type *h;
   unsigned int len;
   len = (unsigned int) (r1 - r0);
   hash = slrn_compute_hash ((unsigned char *) r0, (unsigned char *) r1);
   
   h = Header_Table[hash % HEADER_TABLE_SIZE];
   while (h != NULL)
     {
	if (!slrn_case_strncmp ((unsigned char *) h->msgid,
				(unsigned char *) r0,
				len))
	  break;
	
	h = h->hash_next;
     }
   return h;
}

/*}}}*/

Slrn_Header_Type *slrn_find_header_with_msgid (char *msgid) /*{{{*/
{
   return find_header_from_msgid (msgid, msgid + strlen (msgid));
}

/*}}}*/

static void goto_article (void) /*{{{*/
{
   Slrn_Header_Type *h;
   int want_n;
   
   if (-1 == slrn_read_integer ("Goto article", NULL, &want_n))
     return;
   
   h = Headers;
   while (h != NULL)
     {
	if (h->number == want_n)
	  {
	     Slrn_Current_Header = h;
	     if (h->flags & HEADER_HIDDEN) slrn_uncollapse_this_thread (h, 0);
	     find_header_line_num ();
	     return;
	  }
	
	h = h->next;
     }
   
   slrn_error ("Article not found.");
}

/*}}}*/

int slrn_is_article_visible (void)
{
   int mask = 0;
   
   if ((Slrn_Current_Header != NULL)
       && Article_Visible)
     {
	if (Slrn_Current_Header == Header_Showing)
	  mask = 3;
	else
	  mask = 1;
     }
   
   return mask;
}

/*}}}*/

static int unfold_art_header_lines (void)
{
   Slrn_Article_Line_Type *l;
   char ch;
   
   l = Slrn_Article_Lines;
   if (l == NULL) return 0;
   l = l->next;
   
   while ((l != NULL) && (0 != (ch = l->buf[0])))
     {
	if ((ch == ' ') || (ch == '\t'))
	  {
	     unsigned int len0, len1;
	     Slrn_Article_Line_Type *prev;
	     char *new_buf;
	     
	     l->buf[0] = ' ';
	     
	     prev = l->prev;
	     
	     len0 = strlen (prev->buf);
	     len1 = len0 + strlen (l->buf) + 1;
	     
	     new_buf = slrn_realloc (prev->buf, len1, 1);
	     if (new_buf == NULL)
	       return -1;

	     prev->buf = new_buf;
	     
	     strcpy (new_buf + len0, l->buf);
	     prev->next = l->next;
	     if (l->next != NULL) l->next->prev = prev;
	     SLFREE (l->buf);
	     SLFREE (l);
	     l = prev;
	  }
	
	l = l->next;
     }
   return 0;
}

	
static int read_article (Slrn_Header_Type *h, int kill_refs, int do_mime) /*{{{*/
{
   Slrn_Article_Line_Type *l;
   unsigned int total_lines;
   char buf[NNTP_BUFFER_SIZE], *b, *b1, ch;
   unsigned int len;
   int n = h->number;
   Slrn_Header_Type *last_header_showing;
   int num_lines_update;
   int status;

   last_header_showing = Header_Showing;
   
   if (Header_Showing == h)
     {
#if SLRN_HAS_MIME
	if (Slrn_Use_Mime == 0)
	  return 0;
	if ((do_mime && Slrn_Mime_Was_Parsed)
	    || ((do_mime == 0) && (Slrn_Mime_Was_Modified == 0)))
#endif
	  return 0; /* The cached copy is good */
     }
   
   if (h->tag_number) slrn_message_now ("#%2d/%-2d: Retrieving... %s", 
				    h->tag_number, Num_Tag_List.len, 
				    h->subject);
   else slrn_message_now ("[%d]Reading...", h->number);

   status = Slrn_Server_Obj->sv_select_article (n, h->msgid);
   if (status != OK_ARTICLE)
     {
	if (status == -1)
	  {
	     slrn_error ("Server failed to return article.");
	     return -1;
	  }

	slrn_error ("Article %d unavailable.", n);
	
	if (kill_refs && ((h->flags & HEADER_READ) == 0))
	  {
	     kill_cross_references (h);
	     h->flags |= HEADER_READ;
	  }
	return -1;
     }
   
   
   At_End_Of_Article = NULL;
   
   Do_Rot13 = 0;
   Article_Window_HScroll = 0;
   Slrn_Full_Screen_Update = 1;
   
   free_article ();
   
   if ((num_lines_update = Slrn_Reads_Per_Update) < 5)
     {
	if (h->lines < 200)
	  num_lines_update = 20;
	else 
	  num_lines_update = 50;
     }
   
   total_lines = 0;
   while (Slrn_Server_Obj->sv_read_line(buf, sizeof(buf)) != NULL)
     {
	if (SLang_Error == USER_BREAK)
	  {
	     if (Slrn_Server_Obj->sv_reset != NULL)
	       Slrn_Server_Obj->sv_reset ();
	     slrn_error ("Article transfer aborted.");
	     if (Slrn_Article_Lines == NULL)
	       return -1;
	     break;
	  }
	
	total_lines++;
	if ((1 == (total_lines % num_lines_update))
	    /* Just so the ratio does not confuse the reader because of the
	     * header lines...
	     */
	    && (total_lines < (unsigned int) h->lines))
	  {
	     if (h->tag_number)
	       slrn_message_now ("#%2d/%-2d: Read %4d/%-4d lines (%s)",
				 h->tag_number, Num_Tag_List.len,
				 total_lines, h->lines, h->subject);
	     else
	       slrn_message_now ("[%d]Read %d/%d lines so far...", 
				 h->number, total_lines, h->lines);
	  }
	
	len = strlen (buf);
	
	l = (Slrn_Article_Line_Type *) slrn_malloc (sizeof(Slrn_Article_Line_Type),
						    1, 1);
	if ((l == NULL)
	    || (NULL == (l->buf = slrn_malloc (len + 1, 0, 1))))
	  {
	     slrn_free ((char *) l);
	     free_article ();
	     return -1;
	  }
	
	/* here I am going to remove _^H combinations.  Later, it will be a
	 * good idea to perform the implied underlining
	 */
	b1 = l->buf;
	b = buf;
	
	if ((*b == '.') && (*(b + 1) == '.')) b++;
	
	while (0 != (ch = *b++))
	  {
	     if ((ch == '_') && (*b == '\b'))
	       {
		  b++;
	       }
	     else *b1++ = ch;
	  }
	*b1 = 0;
	
	l->next = l->prev = NULL;
	l->flags = 0;
	
	if (Slrn_Article_Lines == NULL)
	  {
	     Slrn_Article_Lines = l;
	  }
	else
	  {
	     /* l->next = Article_Current_Line->next; */
	     l->prev = Article_Current_Line;
	     Article_Current_Line->next = l;
	     
	     /* if (l->next != NULL) l->next->prev = l; */
	  }
	Article_Current_Line = l;
     }
   
   Article_Current_Line = Slrn_Article_Lines;

   if (-1 == unfold_art_header_lines ())
     {
	free_article ();
	return -1;
     }
   
   if (kill_refs && ((h->flags & HEADER_READ) == 0))
     {
	kill_cross_references (h);
	h->flags |= HEADER_READ;
     }
   
   /* Do this now so that header lines are marked.  Mime processing 
    * assumes this.
    */
   if (h == Slrn_Current_Header) hide_art_headers ();

   /* This must be called before any of the other functions are called because
    * they may depend upon the line number and window information.
    */
   init_article_window_struct ();


#if SLRN_HAS_MIME
   if (Slrn_Use_Mime && (h == Slrn_Current_Header))
     {
	/* Note: the mime routines assume that the article flags are valid.
	 * That is, a header line is given by line->flags & HEADER_LINE
	 */
	slrn_mime_article_init ();
	if (do_mime) slrn_mime_process_article ();
     }
#endif

   slrn_chmap_fix_body ();
   
   if (last_header_showing != h)
     {
	Last_Read_Header = last_header_showing;
     }
   Header_Showing = h;

   if (h == Slrn_Current_Header)
     {
#if SLRN_HAS_SPOILERS
	if (Slrn_Spoiler_Char) mark_spoilers ();
#endif
	mark_quotes ();
       /* mark_signature unmarks lines in the signature which look like
	* quotes, so do it after mark_quotes, but before hide_quotes */
	mark_signature ();
	hide_quotes ();
	  
	if (Slrn_Wrap_Mode & 0x4) wrap_article ();

	if (Last_Read_Header == NULL)
	  Last_Read_Header = h;
     }
   
   /* slrn_set_suspension (0); */
   return 0;
}

/*}}}*/

/*{{{ reply, reply_cmd, forward, followup */

int slrn_insert_followup_format (char *f, FILE *fp) /*{{{*/
{
   char ch, *s, *smax;
   
   if (f == NULL)
     return 0;

   if (Slrn_Current_Header == NULL)
     return 0;

   while ((ch = *f++) != 0)
     {
	if (ch != '%')
	  {
	     putc (ch, fp);
	     continue;
	  }
	s = smax = NULL;
	ch = *f++;
	if (ch == 0) break;
	
	switch (ch)
	  {
	   case 's':
	     s = Slrn_Current_Header->subject;
	     break;
	   case 'm':
	     s = Slrn_Current_Header->msgid;
	     break;
	   case 'r':
	     s = Slrn_Current_Header->realname;
	     smax = s + Slrn_Current_Header->realname_len;
	     break;
	   case 'f':
	     s = parse_from (Slrn_Current_Header->from);
	     break;
	   case 'n':
#if 0
	     s = slrn_extract_header ("Newsgroups: ", 12);
#else
	     s = Slrn_Current_Group_Name;
#endif
	     break;
	   case 'd':
	     s = Slrn_Current_Header->date;
	     break;
	   case '%':
	   default:
	     putc (ch, fp);
	  }
	
	if (s == NULL) continue;
	if (smax == NULL) fputs (s, fp);
	else fwrite (s, 1, (unsigned int) (smax - s), fp);
     }
   return 0;
}

/*}}}*/

static char *parse_reply_address (void)
{
   char *from;
   
   if (NULL == (from = slrn_extract_header ("Reply-To: ", 10)))
     from = slrn_extract_header ("From: ", 6);
   
   return parse_from (from);
}

/* If from != NULL, it's taken as the address to send the reply to, otherwise
 * the reply address is taken from Reply-To: or From: */
static void reply (char *from) /*{{{*/
{
   char *msgid, *subject, *f;
   Slrn_Article_Line_Type *l;
   FILE *fp;
   char file[256];
   unsigned int n;

   if (-1 == slrn_check_batch ())
     return;
   
   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   
   if (read_article (Slrn_Current_Header, Slrn_Del_Article_Upon_Read, 1) < 0) return;

#if SLRN_HAS_SLANG
   SLang_run_hooks ("reply_hook", 0);
   if (SLang_Error)
     return;
#endif
   
   /* Check for FQDN.  If it appear bogus, warn user */
   if (from == NULL) from = parse_reply_address ();
   
   if ((from == NULL) 
       || (NULL == (f = slrn_strchr (from, '@')))
       || (f == from)
       || (0 == slrn_is_fqdn (f + 1)))
     {
	if (0 == slrn_get_yesno (1, "%s appears invalid.  Continue anyway",
				 ((from == NULL) ? "Email address" : from)))
	  return;
     }
   
   
   if (Slrn_Use_Tmpdir)
     fp = slrn_open_tmpfile (file, "w");
   else fp = slrn_open_home_file (SLRN_LETTER_FILENAME, "w", file, 0);
   
   if (NULL == fp)
     {
	slrn_error ("Unable to open %s for writing.", file);
	return;
     }
   
   /* parse header */
   msgid = slrn_extract_header ("Message-ID: ", 12);
   subject = slrn_extract_header ("Subject: ", 9);

   if (subject == NULL) subject = "";
   subject = slrn_skip_whitespace (subject);
   if (0 == slrn_case_strncmp ((unsigned char *)"Re:", (unsigned char *)subject, 3))
     subject = slrn_skip_whitespace (subject + 3);

   n = 0;
   fprintf (fp, "To: %s\nSubject: Re: %s\nIn-Reply-To: %s\n",
	    (from == NULL ? "" : from),
	    subject,
	    (msgid == NULL ? "" : msgid));
   n += 3;
   
#if SLRN_GEN_FROM_EMAIL_HEADER
   fprintf (fp, "From: %s@%s (%s)\n", 
	    Slrn_User_Info.username, Slrn_User_Info.host,
	    Slrn_User_Info.realname);
   n += 1;
#endif
   
   if (0 != *Slrn_User_Info.replyto)
     {
	fprintf (fp, "Reply-To: %s\n", Slrn_User_Info.replyto);
	n += 1;
     }
   
   n += slrn_add_custom_headers (fp, Slrn_Reply_Custom_Headers, slrn_insert_followup_format);
   fputs ("\n", fp);
   
   slrn_insert_followup_format (Slrn_User_Info.reply_string, fp);
   fputs ("\n", fp);

   n += 2;

   l = Slrn_Article_Lines;
   if (Slrn_Prefix_Arg_Ptr == NULL)
     {
	while ((l != NULL) && (*l->buf != 0)) l = l->next;
	if (l != NULL) l = l->next;
     }
   
   while (l != NULL)
     {
	fprintf (fp, "%s%s\n", 
		 ((Slrn_Quote_String == NULL) ? ">" : Slrn_Quote_String), 
		 l->buf);
	l = l->next;
     }
   slrn_add_signature (fp);
   slrn_fclose (fp);
   
   slrn_mail_file (file, 1, n, from, subject);
   if (Slrn_Use_Tmpdir) (void) slrn_delete_file (file);
}

/*}}}*/

static void reply_cmd (void) /*{{{*/
{
   if (-1 == slrn_check_batch ())
     return;

   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   
   if (read_article (Slrn_Current_Header, Slrn_Del_Article_Upon_Read, 1) < 0) return;
   
   if (Slrn_User_Wants_Confirmation
       && (slrn_get_yesno_cancel ("Are you sure you want to reply") <= 0))
     return;
   reply (NULL);
}

/*}}}*/

static void forward_article (void) /*{{{*/
{
   char *subject;
   Slrn_Article_Line_Type *l;
   FILE *fp;
   char file[256];
   char to[256];
   int edit;
   
   if (-1 == slrn_check_batch ())
     return;

   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   if (read_article (Slrn_Current_Header, Slrn_Del_Article_Upon_Read, 1) < 0) return;
   
#if SLRN_HAS_SLANG
   SLang_run_hooks ("forward_hook", 0);
   if (SLang_Error)
     return;
#endif
   
   *to = 0;
   if (slrn_read_input ("Forward to (^G aborts)", NULL, to, 1, 0) <= 0)
     {
	slrn_error ("Aborted.  An email address is required.");
	return;
     }
   
   if (-1 == (edit = slrn_get_yesno_cancel ("Edit the message before sending")))
     return;
   

   if (Slrn_Use_Tmpdir)
     {
	fp = slrn_open_tmpfile (file, "w");
     }
   else fp = slrn_open_home_file (SLRN_LETTER_FILENAME, "w", file, 0);
   
   if (fp == NULL)
     {
	slrn_error ("Unable to open %s for writing.", file);
	return;
     }
   
   
   subject = slrn_extract_header ("Subject: ", 9);
   
   fprintf (fp, "To: %s\nSubject: Fwd: %s\n",
	    to, subject == NULL ? "" : subject);
   
#if defined(IBMPC_SYSTEM)
   fprintf (fp, "From: %s@%s (%s)\n", 
	    Slrn_User_Info.username, Slrn_User_Info.host,
	    Slrn_User_Info.realname);
#endif
   
   if (0 != *Slrn_User_Info.replyto)
     fprintf (fp, "Reply-To: %s (%s)\n", 
	      Slrn_User_Info.replyto, Slrn_User_Info.realname);
   putc ('\n', fp);
   
   l = Slrn_Article_Lines;
   while (l != NULL)
     {
	fprintf (fp, "%s\n", l->buf);
	l = l->next;
     }
   slrn_fclose (fp);
      
   (void) slrn_mail_file (file, edit, 3, to, subject);

   if (Slrn_Use_Tmpdir) slrn_delete_file (file);
}

/*}}}*/



/* If prefix arg is 1, insert all headers.  If it is 2, insert all headers
 * but do not quote text nor attach signature.  2 is good for re-posting.
 */
static void followup (void) /*{{{*/
{
   char *msgid, *newsgroups, *subject, *xref, *quote_str, *cc_address;
   Slrn_Article_Line_Type *l;
   FILE *fp;
   char file[256];
   unsigned int n;
   int prefix_arg;
   int perform_cc;
#if SLRN_HAS_SLANG
   int free_cc_string = 0;
#endif

   /* The perform_cc testing is ugly.  Is there an easier way?? */

   if (-1 == slrn_check_batch ())
     return;

   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   if (read_article (Slrn_Current_Header, Slrn_Del_Article_Upon_Read, 1) < 0) return;

#if SLRN_HAS_SLANG
   SLang_run_hooks ("followup_hook", 0);
   if (SLang_Error)
     return;
#endif

   if (Slrn_Prefix_Arg_Ptr != NULL)
     {
	prefix_arg = *Slrn_Prefix_Arg_Ptr;
	Slrn_Prefix_Arg_Ptr = NULL;
     }
   else prefix_arg = -1;
   
   
   if (Slrn_User_Wants_Confirmation
       && (slrn_get_yesno_cancel ("Are you sure you want to followup") <= 0))
     return;

   /* Here is the logic:
    * If followup-to contains an email address, use that as a CC.
    * If followup-to contains 'poster', use poster's email address.
    * Otherwise, check for mail-copies-to header.  If its value is 'never'
    *  do not add cc header.  If it is 'always', add it.  If neither of these,
    *  assume it is an email address and use that.
    * Otherwise, use email addresss.
    */
   perform_cc = -1;		       /* Don't know */
   cc_address = NULL;

   if (NULL != (newsgroups = slrn_extract_header ("Followup-To: ", 13)))
     {
	newsgroups = slrn_skip_whitespace (newsgroups);
	cc_address = parse_from (newsgroups);
	if (cc_address != NULL)
	  {
	     int is_poster;
	     is_poster = (0 == slrn_case_strcmp ((unsigned char *) cc_address,
						 (unsigned char *) "poster"));
	     if (is_poster
		 || (NULL != slrn_strchr (cc_address, '@')))
	       /* The GNU newsgroups appear to have email addresses in the
		* Followup-To header.  Yuk.
		*/
	       {
		  if (is_poster) cc_address = parse_reply_address ();

		  if (slrn_get_yesno (1, "Do you want to reply to POSTER as poster prefers"))
		    {
		       reply (cc_address);
		       return;
		    }
		  newsgroups = NULL;
		  /* Add "Cc:" regardless of user settings */
		  perform_cc = -1;
	       }
	  }
     }
   
   if (Slrn_Post_Obj->po_can_post == 0)
     {
	slrn_error ("Posting not allowed by server");
	return;
     }

   if ((newsgroups == NULL)
       /* Hmm..  I have also seen an empty Followup-To: header on a GNU
	* newsgroup.
	*/
       || (*newsgroups == 0))
     {
	if (NULL == (newsgroups = slrn_extract_header ("Newsgroups: ", 12)))
	  newsgroups = "";
     }
   
   if (perform_cc == -1)
     {
	if ((NULL != (cc_address = slrn_extract_header ("X-Mail-Copies-To: ", 18)))
	    || (NULL != (cc_address = slrn_extract_header ("Mail-Copies-To: ", 16))))
	  {
	     /* Original poster has requested a certain cc-ing behaviour
	      * which should override whatever default the user has set */
	     cc_address = parse_from (cc_address);
	     if ((cc_address == NULL) 
		 || (0 == strcmp (cc_address, "never"))
		 || (0 == strcmp (cc_address, "nobody")))
	       {
		  perform_cc = 0;
		  cc_address = NULL;
	       }
	     else 
	       {
		  perform_cc = 1;
		  if (0 == strcmp (cc_address, "always"))
		    cc_address = NULL;
	       }
	  }

	if (prefix_arg == 2)
	  perform_cc = 0;
     }

   if (cc_address == NULL)
     cc_address = parse_reply_address ();

   if ((perform_cc != 0)
       && (cc_address != NULL))
     {
#if SLRN_HAS_SLANG
	int cc_hook_status;

	if (-1 == (cc_hook_status = SLang_run_hooks ("cc_hook", 1, cc_address)))
	  return;
	
	if (cc_hook_status == 1)
	  {
	     if (-1 == SLang_pop_slstring (&cc_address))
	       return;
	     free_cc_string = 1;
	     if (*cc_address == 0)
	       perform_cc = 0;
	  }
#endif
	if (-1 == perform_cc)
	  {
	     if (-1 == (perform_cc = Slrn_Auto_CC_To_Poster))
	       perform_cc = slrn_get_yesno (1, "Cc message to poster");
	  }
	
	if (perform_cc)
	  {
	     char *ff;
	     
	     if ((NULL == (ff = slrn_strchr (cc_address, '@')))
		 || (ff == cc_address)
		 || (0 == slrn_is_fqdn (ff + 1))
		 || (strlen (ff + 1) < 5))
	       {
		  perform_cc = slrn_get_yesno_cancel ("%s appears invalid.  CC anyway", cc_address);
		  if (perform_cc < 0)
		    goto free_and_return;
	       }
	  }
     }
   
   msgid = slrn_extract_header ("Message-ID: ", 12);
   
   if (NULL != (subject = slrn_extract_header ("Subject: ", 9)))
     {
	subject = slrn_skip_whitespace (subject);
	if (((*subject | 0x20) == 'r')
	    && ((*(subject + 1) | 0x20) == 'e')
	    && (*(subject + 2) == ':'))
	  {
	     subject = slrn_skip_whitespace (subject + 3);
	  }
     }
   else subject = "";
   
   if (Slrn_Use_Tmpdir)
     {
	fp = slrn_open_tmpfile (file, "w");
     }
   else fp = slrn_open_home_file (SLRN_FOLLOWUP_FILENAME, "w", file, 0);
   
   if (fp == NULL)
     {
	slrn_error ("Unable to open %s for writing.", file);
	goto free_and_return;
     }
   
   fprintf (fp, "Newsgroups: %s\nSubject: Re: %s\n",
	    newsgroups, subject);  n = 3;
   
   xref = slrn_extract_header("References: ", 12);
   if (msgid != NULL)
     {
	if (xref == NULL)
	  fprintf (fp, "References: %s\n", msgid);
	else 
	  fprintf (fp, "References: %s %s\n", xref, msgid);
	n++;
     }

   if (Slrn_User_Info.org != NULL)
     {
	fprintf (fp, "Organization: %s\n", Slrn_User_Info.org);
	n++;
     }
   
   if (perform_cc)
     {
	fprintf (fp, "Cc: %s\n", cc_address);
	n++;
     }
   
   if (0 != *Slrn_User_Info.replyto)
     {
	fprintf (fp, "Reply-To: %s\n", Slrn_User_Info.replyto);
	n++;
     }
   
   fprintf (fp, "Followup-To: \n");
   n++;
   
   n += slrn_add_custom_headers (fp, Slrn_Followup_Custom_Headers, slrn_insert_followup_format);
   
   fputs ("\n", fp);
   
   if (prefix_arg != 2)
     {
	slrn_insert_followup_format (Slrn_User_Info.followup_string, fp);
	fputs ("\n", fp);
     }
   n += 1;			       /* by having + 1, the cursor will be
					* placed on the first line of message.
					*/

   /* skip header */
   l = Slrn_Article_Lines;
   if (prefix_arg == -1)
     {
	while ((l != NULL) && (*l->buf != 0)) l = l->next;
	if (l != NULL) l = l->next;
     }
   
   if (prefix_arg == 2) quote_str = ""; 
   else if (NULL == (quote_str = Slrn_Quote_String))
     quote_str = ">";

   while (l != NULL)
     {
	if ((l->next != NULL) && (l->next->flags & WRAPPED_LINE))
	  unwrap_line (l->next);
	
	fprintf (fp, "%s%s\n", quote_str, l->buf);
	
	l = l->next;
     }

   if (prefix_arg != 2) slrn_add_signature (fp);
   slrn_fclose (fp);
   
   if (slrn_edit_file (Slrn_Editor_Post, file, n) >= 0)
     {
	if (0 == slrn_post_file (file, cc_address, 0))
	  {
	     /* Success. */
	     if (Slrn_Last_Message_Id != NULL)
	       {
		  /* Later I want to actually get the article from the server
		   * and display it.  Of course I can only do it if I know
		   * the message-id.  If Slrn_Last_Message_Id is non-null, 
		   * then slrn generated the message-id and we can do it.
		   */
	       }
	  }
     }
   
   if (Slrn_Use_Tmpdir) (void) slrn_delete_file (file);
   
   free_and_return:
#if SLRN_HAS_SLANG
   if (free_cc_string && (cc_address != NULL))
     SLang_free_slstring (cc_address);
#endif
}

/*}}}*/

/*}}}*/

/*{{{ header movement functions */

unsigned int slrn_header_down_n (unsigned int n, int err) /*{{{*/
{
   unsigned int m;
   
   m = SLscroll_next_n (&Slrn_Header_Window, n);
   Slrn_Current_Header = (Slrn_Header_Type *) Slrn_Header_Window.current_line;
   
   if (err && (m != n))
     slrn_error ("End of buffer.");
   
   return m;
}
/*}}}*/

static void header_down (void) /*{{{*/
{
   slrn_header_down_n (1, 1);
}

/*}}}*/

unsigned int slrn_header_up_n (unsigned int n, int err) /*{{{*/
{
   unsigned int m;
   
   m = SLscroll_prev_n (&Slrn_Header_Window, n);
   Slrn_Current_Header = (Slrn_Header_Type *) Slrn_Header_Window.current_line;
   
   if (err && (m != n))
     slrn_error ("Top of buffer.");
   
   return m;
}

/*}}}*/

static void header_up (void) /*{{{*/
{
   slrn_header_up_n (1, 1);
}

/*}}}*/

static void header_pageup (void) /*{{{*/
{
   Slrn_Full_Screen_Update = 1;
   if (-1 == SLscroll_pageup (&Slrn_Header_Window))
     slrn_error ("Top of buffer.");
   Slrn_Current_Header = (Slrn_Header_Type *) Slrn_Header_Window.current_line;
}

/*}}}*/

static void header_pagedn (void) /*{{{*/
{
   Slrn_Full_Screen_Update = 1;
   if (-1 == SLscroll_pagedown (&Slrn_Header_Window))
     slrn_error ("End of buffer.");
   Slrn_Current_Header = (Slrn_Header_Type *) Slrn_Header_Window.current_line;
}

/*}}}*/

static void header_bob (void) /*{{{*/
{
   Slrn_Current_Header = Headers;
   find_header_line_num ();
}

/*}}}*/

static void header_eob (void) /*{{{*/
{
   while (0xFFFF == slrn_header_down_n (0xFFFF, 0));
}

/*}}}*/

static int prev_unread (void) /*{{{*/
{
   Slrn_Header_Type *h;
   
   h = Slrn_Current_Header -> prev;
   
   while (h != NULL)
     {
	if (0 == (h->flags & HEADER_READ)) break;
	h = h->prev;
     }
   
   if (h == NULL)
     {
	slrn_message ("No previous unread articles.");
	return 0;
     }
      
   Slrn_Current_Header = h;

   if (h->flags & HEADER_HIDDEN)
     slrn_uncollapse_this_thread (h, 0);

   find_header_line_num ();
   return 1;
}

/*}}}*/

static void goto_last_read (void) /*{{{*/
{
   if (Last_Read_Header == NULL) return;
   slrn_goto_header (Last_Read_Header, 1);
}

/*}}}*/

static void art_prev_unread (void) /*{{{*/
{
   if (prev_unread () && Article_Visible) art_pagedn  ();
}

/*}}}*/

int slrn_next_unread_header (void) /*{{{*/
{
   Slrn_Header_Type *h;
   
   h = Slrn_Current_Header->next;
   
   while (h != NULL)
     {
	if (0 == (h->flags & HEADER_READ)) break;
	h = h->next;
     }
   
   if (h == NULL)
     {
	slrn_message ("No following unread articles.");
	return 0;
     }
   
   Slrn_Current_Header = h;
   if (h->flags & HEADER_HIDDEN)
     slrn_uncollapse_this_thread (h, 0);
     
   find_header_line_num ();

   return 1;
}

/*}}}*/

static void art_next_unread (void) /*{{{*/
{
   char ch;
   unsigned char ch1;
   
   if (slrn_next_unread_header ())
     {
	if (Article_Visible) art_pagedn  ();
	return;
     }
   
   if (Slrn_Query_Next_Group == 0)
     {
	skip_to_next_group ();
	return;
     }
   
   if (Slrn_Batch) return;
   
   ch1 = SLang_Last_Key_Char;
   if (ch1 == 27) ch1 = 'n';
   slrn_message_now ("No following unread articles.  Press %s for next group.",
		     map_char_to_string (ch1));
   
   ch = SLang_getkey ();
   
   if ((unsigned char)ch == ch1)
     {
	skip_to_next_group ();
     }
   else SLang_ungetkey (ch);
}

/*}}}*/

static void next_high_score (void) /*{{{*/
{
   Slrn_Header_Type *l;
   
   l = Slrn_Current_Header->next;
   
   while (l != NULL)
     {
	if (l->flags & HEADER_HIGH_SCORE)
	  {
	     break;
	  }
	l = l->next;
     }
   
   if (l == NULL)
     {
	slrn_error ("No more high scoring articles.");
	return;
     }
   
   if (l->flags & HEADER_HIDDEN) slrn_uncollapse_this_thread (l, 0);
   
   Slrn_Current_Header = l;
   find_header_line_num ();
   
   if (Article_Visible)
     {
	if (Header_Showing != Slrn_Current_Header) 
	  art_pagedn ();
     }
}

/*}}}*/

static Slrn_Header_Type *Same_Subject_Start_Header;
static void next_header_same_subject (void) /*{{{*/
{
   SLsearch_Type st;
   Slrn_Header_Type *l;
   static char same_subject[256];
   
   if ((Same_Subject_Start_Header == NULL)
       || (Slrn_Prefix_Arg_Ptr != NULL))
     {
	Slrn_Prefix_Arg_Ptr = NULL;
	if (slrn_read_input ("Subject", same_subject, NULL, 0, 0) <= 0) return;
	Same_Subject_Start_Header = Slrn_Current_Header;
     }
   SLsearch_init (same_subject, 1, 0, &st);
   
   l = Slrn_Current_Header->next;
   
   while (l != NULL)
     {
	if (
#if 0
	    /* Do we want to do this?? */
	    ((l->flags & HEADER_READ) == 0) &&
#endif
	    (l->subject != NULL)
	    && (NULL != SLsearch ((unsigned char *) l->subject,
				  (unsigned char *) l->subject + strlen (l->subject),
				  &st)))
	  break;
	
	l = l->next;
     }
   
   if (l == NULL)
     {
	slrn_error ("No more articles on that subject.");
	l = Same_Subject_Start_Header;
	Same_Subject_Start_Header = NULL;
     }
   
   if (l->flags & HEADER_HIDDEN) slrn_uncollapse_this_thread (l, 0);
   Slrn_Current_Header = l;
   find_header_line_num ();
   if ((Same_Subject_Start_Header != NULL)
       && (Article_Visible))
     {
	art_pagedn ();
     }
}

/*}}}*/

static void goto_header_number (void) /*{{{*/
{
   int diff, i, ich;

   if (Slrn_Batch) return;

   i = 0;
   ich = SLang_Last_Key_Char;
   do
     {
	i = i * 10 + (ich - '0');
	if (10 * i > Largest_Header_Number)
	  {
	     ich = '\r';
	     break;
	  }
	slrn_message_now ("Goto Header: %d", i);
     }
   while ((ich = SLang_getkey ()), (ich <= '9') && (ich >= '0'));
   
   if (SLKeyBoard_Quit) return;
   
   if (ich != '\r')
     SLang_ungetkey (ich);
   
   diff = i - Last_Cursor_Row;
   if (diff > 0) slrn_header_down_n (diff, 0); else slrn_header_up_n (-diff, 0);
#if SLRN_HAS_SLANG
   SLang_run_hooks ("header_number_hook", 0);
#endif
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

/*}}}*/

/*{{{ article save/decode functions */

static int write_article_lines (FILE *fp)
{
   Slrn_Article_Line_Type *l;
   
   l = Slrn_Article_Lines;

   while (l != NULL)
     {
	char *buf;
	Slrn_Article_Line_Type *next = l->next;
	
	buf = l->buf;
	if (l->flags & WRAPPED_LINE) buf++;   /* skip space */
	
	if (EOF == fputs (buf, fp))
	  return -1;
	
	if ((next == NULL) || (0 == (next->flags & WRAPPED_LINE)))
	  {
	     if (EOF == putc ('\n', fp))
	       return -1;
	  }
	l = next;
     }
	
   return 0;
}

int slrn_save_current_article (char *file) /*{{{*/
{
   FILE *fp = NULL;
   
   if (Slrn_Current_Header == NULL) 
     return -1;
   
   if (read_article (Slrn_Current_Header, Slrn_Del_Article_Upon_Read, 0) < 0)
     return -1;
   
   fp = fopen (file, "w");
   
   if (fp == NULL)
     {
	slrn_error ("Unable to open %s.", file);
	return -1;
     }

   if (-1 == write_article_lines (fp))
     {
	slrn_error ("Error writing to %s.", file);
	fclose (fp);
	return -1;
     }
   
   fclose (fp);
   return 0;
}


/*}}}*/

static int save_article_as_unix_mail (Slrn_Header_Type *h, FILE *fp) /*{{{*/
{
   char *from;
   time_t now;
   Slrn_Article_Line_Type *l;
   int is_wrapped;

   if (read_article (h, Slrn_Del_Article_Upon_Read, 0) < 0) return -1;
   
   is_wrapped = Article_Wrapped;
   if (is_wrapped) unwrap_article ();

   from = slrn_extract_header ("From: ", 6);
   if (from != NULL) from = parse_from (from);
   if ((from == NULL) || (*from == 0)) from = "nobody@nowhere";
   
   time (&now);
   fprintf (fp, "From %s %s", from, ctime(&now));
   
   l = Slrn_Article_Lines;
   while (l != NULL)
     {
	if ((*l->buf == 'F')
	    && !strncmp ("From", l->buf, 4)
	    && ((unsigned char)(l->buf[4]) <= ' '))
	  {
	     putc ('>', fp);
	  }
	
	fputs (l->buf, fp);
	putc ('\n', fp);
	l = l->next;
     }
   fputs ("\n\n", fp);
   
   if (is_wrapped) wrap_article ();

   return 0;
}

/*}}}*/

static char *save_article_to_file (char *defdir, char *input_string) /*{{{*/
{
   char file[256];
   char name[256];
   int save_tagged = 0;
   int save_thread = 0;
   int save_simple;
   FILE *fp;
   
   if (-1 == slrn_check_batch ())
     return NULL;
   
   if (Num_Tag_List.len)
     {
	save_tagged = slrn_get_yesno_cancel ("Save tagged articles");
	if (save_tagged < 0) return NULL;
     }
   else if ((Slrn_Current_Header->child != NULL)
	    && (Slrn_Current_Header->child->flags & HEADER_HIDDEN))
     {
	save_thread = slrn_get_yesno_cancel ("Save this thread");
	if (save_thread == -1) return NULL;
     }
   
   save_simple = !(save_tagged || save_thread);
   
   if (*Output_Filename == 0)
     {
#ifdef VMS 
	char *p;
#endif
	unsigned int defdir_len;
	if (defdir == NULL) defdir = "News";
	
	defdir_len = strlen (defdir);
#ifdef VMS
	sprintf (name, "%s/", defdir);
	p = name + strlen (name);
	strcpy (p, Slrn_Current_Group_Name);
	while (*p != 0)
	  {
	     if (*p == '.') *p = '_';
	     p++;
	  }
	strcpy (p, ".txt");
#else
	sprintf (name, "%s/%s", defdir, Slrn_Current_Group_Name);
#endif
	
#if !defined(VMS) && !defined(IBMPC_SYSTEM)
	/* Lowercase first letter and see if it exists. */
	name[defdir_len + 1] = LOWER_CASE(name[defdir_len + 1]);
#endif
	slrn_make_home_filename (name, file);

#if !defined(VMS) && !defined(IBMPC_SYSTEM)
	if (1 != slrn_file_exists (file))
	  {
	     /* Lowercase version does not exist so user uppercase form. */
	     name[defdir_len + 1] = UPPER_CASE(name[defdir_len + 1]);
	     slrn_make_home_filename (name, file);
	  }
#endif
     }
   else strcpy (file, Output_Filename);
   
   if (slrn_read_input (input_string, NULL, file, 1, 1) <= 0)
     {
	slrn_error ("Aborted.");
	return NULL;
     }
   
   if (NULL == (fp = fopen (file, "a")))
     {
	slrn_error ("Unable to open %s", file);
	return NULL;
     }

   strcpy (Output_Filename, file);
   if (save_simple) save_article_as_unix_mail (Slrn_Current_Header, fp);
   else if (save_tagged)
     {
	unsigned int i;
	unsigned int num_saved = 0;

	for (i = 0; i < Num_Tag_List.len; i++)
	  {
	     if (-1 == save_article_as_unix_mail (Num_Tag_List.headers[i], fp))
	       {
		  slrn_smg_refresh ();
		  if (SLang_Error == SL_USER_BREAK)
		    break;

		  SLang_Error = 0;
		  (void) SLang_input_pending (5);   /* half second delay */
		  slrn_clear_message ();
	       }
	     else num_saved++;
	  }
	if (num_saved == 0) return NULL;
     }
   else
     {
	Slrn_Header_Type *h = Slrn_Current_Header;
	unsigned int num_saved = 0;
	do
	  {
	     if (-1 == save_article_as_unix_mail (h, fp))
	       {
		  slrn_smg_refresh ();
		  SLang_Error = 0;
		  (void) SLang_input_pending (5);   /* half second delay */
	       }
	     else num_saved++;

	     h = h->next;
	  }
	while ((h != NULL) && (h->parent != NULL));
	if (num_saved == 0) return NULL;
     }
   slrn_fclose (fp);
   
   if (SLang_Error) return NULL;
   
   return Output_Filename;
}

/*}}}*/

static void save_article (void) /*{{{*/
{
   (void) save_article_to_file (Slrn_Save_Directory, "Save to file (^G aborts)");
}

/*}}}*/

#if SLRN_HAS_DECODE
#if SLRN_HAS_UUDEVIEW
static int the_uudeview_busy_callback (void *param, uuprogress *progress)
{
   char stuff[26];
   unsigned int count, count_max;
   int pcts;
   char *ptr;
   
   if (progress->action != UUACT_DECODING) 
     return 0;
   
   pcts = (int)((100 * progress->partno + progress->percent - 100) / progress->numparts);
   
   count_max = sizeof (stuff) - 1;

   for (count = 0; count < count_max; count++)
     stuff[count] = (count < pcts/4) ? '#' : '.';

   stuff [count_max] = 0;

   slrn_message_now ("decoding %10s (%3d/%3d) %s",
		     progress->curfile,
		     progress->partno, progress->numparts,
		     stuff);
   return 0;
}

static int do_slrn_uudeview (char *uu_dir, char *file)
{
   uulist *item;
   char where [SLRN_MAX_PATH_LEN];
   int i, ret;

   slrn_make_home_dirname (uu_dir, where);
   /* this is expecting a '/' at the end...so we put one there */
   strcat (where, "/");
   
   ret = UUInitialize ();
   ret = UUSetBusyCallback (NULL, the_uudeview_busy_callback, 100);
   ret = UUSetOption (UUOPT_DESPERATE, 1, NULL);
   ret = UUSetOption (UUOPT_SAVEPATH, 0, where);
   if (UURET_OK != (ret = UULoadFile (file, NULL, 0)))
     {
	/* Not all systems have strerror... */
	if (ret == UURET_IOERR)
	  slrn_error ("could not load %s: errno = %d", 
		      file, UUGetOption (UUOPT_ERRNO, NULL, NULL, 0));
	else
	  slrn_error ("could not load %s: %s",  UUstrerror (ret));
     }
   
   i = 0;
   while (NULL != (item = UUGetFileListItem (i)))
     {
	i++;
	
	if (UURET_OK != (ret = UUDecodeFile (item, NULL)))
	  {
	     char *f;
	     char *err;
	     
	     if (NULL == (f = item->filename)) f = "oops";
	     
	     if (ret == UURET_IOERR) err = "I/O error.";
	     else err = UUstrerror (ret);
	     
	     slrn_error ("error decoding %s: %s", f, err);
	  }
     }

   UUCleanUp ();
}
#endif

static void decode_article (void) /*{{{*/
{
   char *uu_dir;
   char *file;
   
   if (NULL == (uu_dir = Slrn_Decode_Directory))
     {
	uu_dir = Slrn_Save_Directory;
     }
   else *Output_Filename = 0;	       /* force it to use this directory */

   file = save_article_to_file(uu_dir, "Filename (^G aborts)");

   if (file == NULL) return;
   
   if (1 == slrn_get_yesno (1, "decode %s", file))
     {
# if SLRN_HAS_UUDEVIEW
	(void) do_slrn_uudeview (uu_dir, file);
# else
	(void) slrn_uudecode_file (file, NULL, 0, NULL);
# endif

	if (SLang_Error == 0)
	  {
	     if (1 == slrn_get_yesno (1, "Delete %s", file))
	       {
		  if (-1 == slrn_delete_file (file))
		    slrn_error ("Unable to delete %s", file);
	       }
	  }
     }
   /* Since we have a decode directory, do not bother saving this */
   if (NULL != Slrn_Decode_Directory)
     *Output_Filename = 0;
}

/*}}}*/
#endif  /* SLRN_HAS_DECODE */

/*}}}*/
/*{{{ pipe_article functions */

int slrn_pipe_article_to_cmd (char *cmd) /*{{{*/
{
#if SLRN_HAS_PIPING
   FILE *fp;

   if (-1 == select_article (1))
     return -1;

   if (NULL == (fp = slrn_popen (cmd, "w")))
     {
	slrn_error ("Unable to open pipe to %s", cmd);
	return -1;
     }
   
   if (-1 == write_article_lines (fp))
     {
	slrn_pclose (fp);
	return -1;
     }
   
   slrn_pclose (fp);
   return 0;
#else
   slrn_error ("Piping not implemented on this system.");
   return -1;
#endif
}

/*}}}*/

static void pipe_article (void) /*{{{*/
{
#if SLRN_HAS_PIPING
   static char cmd[256];
   
   if (slrn_read_input ("Pipe to command", NULL, cmd, 1, 1) <= 0)
     {
	slrn_error ("Aborted.  Command name is required.");
	return;
     }
   
   if (-1 == slrn_pipe_article_to_cmd (cmd))
     slrn_message ("Error piping to %s.", cmd);
#else
   slrn_error ("Piping not implemented on this system.");
#endif
}

/*}}}*/

static int print_article (void) /*{{{*/
{
   Slrn_Print_Type *p;
   Slrn_Article_Line_Type *l;

   if (-1 == select_article (1))
     return -1;

   slrn_message_now ("Printing article...");

   p = slrn_open_printer ();
   if (p == NULL)
     return -1;

   l = Slrn_Article_Lines;

   while (l != NULL)
     {
	if ((-1 == slrn_write_to_printer (p, l->buf, strlen (l->buf)))
	    || (-1 == slrn_write_to_printer (p, "\n", 1)))
	  {
	     slrn_close_printer (p);
	     return -1;
	  }

	l = l->next;
     }

   if (-1 == slrn_close_printer (p))
     return -1;

   slrn_message_now ("Printing article...done");
   return 0;
}

/*}}}*/

static void print_article_cmd (void) /*{{{*/
{
   if (Slrn_User_Wants_Confirmation
       && (slrn_get_yesno_cancel ("Are you sure you want to print the article") <= 0))
     return;

   (void) print_article ();
}

/*}}}*/


/*}}}*/

/*{{{ sorting header functions */

static int subject_cmp (register unsigned char *sa, register unsigned char *sb) /*{{{*/
{
   register unsigned char ch;
   
   /* skip past re: */
   while (*sa == ' ') sa++;
   while (*sb == ' ') sb++;

   ch = *sa;
   if (((ch | 0x20) == 'r') && ((*(sa + 1) | 0x20) == 'e')
       && (*(sa + 2) == ':'))
     {
	sa += 3;
     }
   
   ch = *sb;
   if (((ch | 0x20) == 'r') && ((*(sb + 1) | 0x20) == 'e')
       && (*(sb + 2) == ':'))
     {
	sb += 3;
     }
   
   while (1)
     {
	register unsigned char cha, chb;

	while (*sa == ' ') sa++;
	while (*sb == ' ') sb++;

	cha = UPPER_CASE(*sa);
	chb = UPPER_CASE(*sb);
	
	if (cha != chb)
	  return (int) cha - (int) chb;
	
	if (cha == 0)
	  return 0;
	
	sa++;
	sb++;	
     }

   /* return slrn_case_strcmp ((unsigned char *) sa, (unsigned char *) sb); */
}

/*}}}*/

static int header_subj_cmp (Slrn_Header_Type **ap, Slrn_Header_Type **bp) /*{{{*/
{
   int cmp;
   Slrn_Header_Type *a = *ap, *b = *bp;
   int ahigh, bhigh;
   
   ahigh = (0 != (a->flags & (HEADER_HIGH_SCORE | FAKE_HEADER_HIGH_SCORE)));
   bhigh = (0 != (b->flags & (HEADER_HIGH_SCORE | FAKE_HEADER_HIGH_SCORE)));
   
   if (ahigh == bhigh)
     {
	cmp = subject_cmp ((unsigned char *) (a->subject),
			   (unsigned char *) (b->subject));
	if (!cmp) return a->number - b->number;
     }
   else cmp = bhigh - ahigh;
   
   return cmp;
}

/*}}}*/

static int header_date_cmp (Slrn_Header_Type **ap, Slrn_Header_Type **bp) /*{{{*/
{
   Slrn_Header_Type *a = *ap, *b = *bp;
   long ahigh, bhigh;
   
   ahigh = (0 != (a->flags & (HEADER_HIGH_SCORE | FAKE_HEADER_HIGH_SCORE)));
   bhigh = (0 != (b->flags & (HEADER_HIGH_SCORE | FAKE_HEADER_HIGH_SCORE)));
   
   if (ahigh == bhigh)
     {
	ahigh = slrn_date_to_order_parm (a->date);
	bhigh = slrn_date_to_order_parm (b->date);
	
	if (Slrn_Sorting_Mode & SORT_BY_SUBJECT)
	  {
	     long tmp = ahigh;
	     ahigh = bhigh;
	     bhigh = tmp;
	  }
	
	if (ahigh == bhigh)
	  return a->number - b->number;
     }

   return (int) (bhigh - ahigh);
}

/*}}}*/

typedef int (*Header_Cmp_Func_Type)(Slrn_Header_Type **, Slrn_Header_Type **);

static void sort_by_function (Header_Cmp_Func_Type cmp_func) /*{{{*/
{
   Slrn_Header_Type **header_list, *h;
   unsigned int i, nheaders;
   void (*qsort_fun) (char *, unsigned int,
		      unsigned int, int (*)(Slrn_Header_Type **, Slrn_Header_Type **));
   
   /* This is a silly hack to make up for braindead compilers and the lack of
    * uniformity in prototypes for qsort.
    */
   qsort_fun = (void (*)(char *, unsigned int,
			 unsigned int, int (*)(Slrn_Header_Type **, Slrn_Header_Type **)))
     qsort;
   
   /* Count the number we need to sort. */
   nheaders = 0;
   h = Slrn_First_Header;
   while (h != NULL)
     {
	if (h->parent == NULL) nheaders++;
	h = h->real_next;
     }
   if (nheaders < 2) return;
   
   if (NULL == (header_list = (Slrn_Header_Type **) SLCALLOC (sizeof (Slrn_Header_Type *), nheaders + 1)))
     {
	slrn_error ("sort_headers(): memory allocation failure.");
	return;
     }
   
   h = Slrn_First_Header;
   nheaders = 0;
   while (h != NULL)
     {
	if (h->parent == NULL)
	  header_list[nheaders++] = h;
	h = h->real_next;
     }
   header_list[nheaders] = NULL;
   
   (*qsort_fun) ((char *) header_list, nheaders, sizeof (Slrn_Header_Type *), cmp_func);
   
   /* What to do now depends upon the current threading state. */
   
   if (Headers_Threaded == 0)
     {
	header_list[0]->next = header_list[1];
	header_list[0]->prev = NULL;
	
	for (i = 1; i < nheaders; i++)
	  {
	     h = header_list[i];
	     h->next = header_list[i + 1];
	     h->prev = header_list[i - 1];
	  }
     }
   else
     {
	slrn_collapse_threads (0);
	/* The headers are threaded so we simply have sorted parents.  Arrange
	 * those.
	 */
	h = NULL;
	for (i = 0; i <= nheaders; i++)
	  {
	     Slrn_Header_Type *h1 = header_list[i];
	     if (h != NULL)
	       {
		  h->sister = h1;
		  while (h->child != NULL)
		    {
		       h = h->child;
		       while (h->sister != NULL) h = h->sister;
		    }
		  h->next = h1;
	       }
	     if (h1 != NULL) h1->prev = h;
	     h = h1;
	  }
     }
   
   Headers = header_list[0];
   find_header_line_num ();
   
   Slrn_Full_Screen_Update = 1;
   SLFREE (header_list);
   
   if (Slrn_Threads_Visible)
     {
	slrn_uncollapse_threads (1);
     }
}

/*}}}*/

static void sort_by_subject (void) /*{{{*/
{
   sort_by_function (header_subj_cmp);
}

/*}}}*/

static void sort_by_date (void) /*{{{*/
{
   sort_by_function (header_date_cmp);
}

/*}}}*/

#if SLRN_HAS_SORT_BY_SCORE
static int header_score_cmp (Slrn_Header_Type **a, Slrn_Header_Type **b) /*{{{*/
{
   /* sort by *descending* score */
   int cmp = (*b)->thread_score - (*a)->thread_score;
   if (cmp == 0) 
     {
	if (Slrn_Sorting_Mode & SORT_BY_SUBJECT)
	  return header_subj_cmp (a, b);
	else 
	  return (*a)->number - (*b)->number;
     }
   return cmp;
}

/*}}}*/

static void sort_by_score (void) /*{{{*/
{
   sort_by_function (header_score_cmp);
}

/*}}}*/
#endif


static void sort_by_server_number (void) /*{{{*/
{
   Slrn_Header_Type *h;
   
   /* This is easy since the real_next, prev are already ordered. */
   h = Slrn_First_Header;
   while (h != NULL)
     {
	Slrn_Header_Type *next = h->real_next;
	h->next = h->real_next;
	h->prev = h->real_prev;
	h->num_children = 0;
	h->flags &= ~(HEADER_HIDDEN | ALL_THREAD_FLAGS);
	h->sister = h->parent = h->child = NULL;
	h = next;
     }
   
   /* Now find out where to put the Headers pointer */
   while (Headers->prev != NULL) Headers = Headers->prev;
   
   Headers_Threaded = 0;
   
   find_header_line_num ();
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void sort_by_threads (void) /*{{{*/
{
   thread_headers ();
   sort_threads ();
   
   if (Slrn_Threads_Visible)
     {
	slrn_uncollapse_threads (1);
     }
   Slrn_Full_Screen_Update = 1;
   Headers_Threaded = 1;
}

/*}}}*/

static void toggle_sort (void) /*{{{*/
{
   int rsp;
   
   rsp = slrn_sbox_sorting_method ();
   if (rsp != -1)
     {
	Slrn_Sorting_Mode = rsp;
	sort_by_sorting_mode ();
     }
}

/*}}}*/

static void sort_by_sorting_mode (void) /*{{{*/
{
   if ((Slrn_Sorting_Mode & SORT_BY_THREADS) == 0)
     sort_by_server_number ();
   else
     sort_by_threads ();
   
   if (Slrn_Sorting_Mode & SORT_BY_DATE)
     sort_by_date ();
   else if (Slrn_Sorting_Mode & SORT_BY_SUBJECT)
     sort_by_subject ();

#if SLRN_HAS_SORT_BY_SCORE
   if (Slrn_Sorting_Mode & SORT_BY_SCORE)
     sort_by_score ();
#endif
}

/*}}}*/

/*}}}*/
/*{{{ Thread related functions */

static void find_non_hidden_header (void) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_Current_Header;
   
   while ((h != NULL) && (h->flags & HEADER_HIDDEN))
     h = h->prev;

   if (h == NULL)
     {
	h = Slrn_Current_Header;
	while ((h != NULL) && (h->flags & HEADER_HIDDEN))
	  h = h->next;
     }
   
   Slrn_Current_Header = h;
}

/*}}}*/

/* This function cannot depend upon routines which call SLscroll functions if
 * sync_now is non-zero.
 */
void slrn_collapse_threads (int sync_now) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_First_Header;
   
   if ((h == NULL) 
       || (Threads_Collapsed == 1))
     return;   
   
   while (h != NULL)
     {
	if (h->parent != NULL) h->flags |= HEADER_HIDDEN;
	else
	  {
	     h->flags &= ~HEADER_HIDDEN;
	  }
	h = h->real_next;
     }
   
   find_non_hidden_header ();
	
   if (sync_now) find_header_line_num ();

   Slrn_Full_Screen_Update = 1;
   Threads_Collapsed = 1;
}

/*}}}*/

void slrn_uncollapse_threads (int sync_now) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_First_Header;
   
   if ((h == NULL) 
       || (0 == Threads_Collapsed))
     return;
   
   while (h != NULL)
     {
	h->flags &= ~HEADER_HIDDEN;
	h = h->real_next;
     }
   Slrn_Full_Screen_Update = 1;
   Threads_Collapsed = 0;
   if (sync_now) find_header_line_num ();
}

/*}}}*/

static void uncollapse_header (Slrn_Header_Type *h) /*{{{*/
{
   h->flags &= ~HEADER_HIDDEN;
}

/*}}}*/

static void collapse_header (Slrn_Header_Type *h) /*{{{*/
{
   h->flags |= HEADER_HIDDEN;
}

/*}}}*/

static void for_this_tree (Slrn_Header_Type *h, void (*f)(Slrn_Header_Type *)) /*{{{*/
{
   Slrn_Header_Type *child = h->child;
   while (child != NULL)
     {
	for_this_tree (child, f);
	child = child->sister;
     }
   (*f) (h);
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void for_this_family (Slrn_Header_Type *h, void (*f)(Slrn_Header_Type *)) /*{{{*/
{
   while (h != NULL)
     {
	for_this_tree (h, f);
	h = h->sister;
     }
}

/*}}}*/

void slrn_uncollapse_this_thread (Slrn_Header_Type *h, int sync_linenum) /*{{{*/
{
   Slrn_Header_Type *child;
   
   /* if (Threads_Collapsed == 0) return; */
   
   while (h->parent != NULL) h = h->parent;
   if ((child = h->child) == NULL) return;
   if (0 == (child->flags & HEADER_HIDDEN)) return;
   
   for_this_family (child, uncollapse_header);

   if (sync_linenum)
     find_header_line_num ();

   Threads_Collapsed = -1;	       /* uncertain */
}

/*}}}*/

void slrn_collapse_this_thread (Slrn_Header_Type *h, int sync_linenum) /*{{{*/
{
   Slrn_Header_Type *child;
   
   /* if (Threads_Collapsed == 1) return; */
   
   while (h->parent != NULL) h = h->parent;
   
   if ((child = h->child) == NULL) return;
   if (child->flags & HEADER_HIDDEN) return;
   
   for_this_family (child, collapse_header);
   
   if (sync_linenum)
     find_header_line_num ();

   Threads_Collapsed = -1;	       /* uncertain */
}

/*}}}*/

static void toggle_collapse_threads (void) /*{{{*/
{
   if (Slrn_Prefix_Arg_Ptr != NULL)
     {
	if (Threads_Collapsed == 1)
	  {
	     slrn_uncollapse_threads (0);
	  }
	else slrn_collapse_threads (0);
	Slrn_Prefix_Arg_Ptr = NULL;
     }
   else
     {
	if (0 == slrn_is_thread_collapsed (Slrn_Current_Header))
	  slrn_collapse_this_thread (Slrn_Current_Header, 0);
	else
	  slrn_uncollapse_this_thread (Slrn_Current_Header, 0);
	
	find_non_hidden_header ();
     }
   find_header_line_num ();
}

/*}}}*/

static Slrn_Header_Type *sort_thread_node (Slrn_Header_Type *h, char *tree) /*{{{*/
{
   Slrn_Header_Type *last = NULL;
   static unsigned int level;
   unsigned char vline_char;
   
   if (h == NULL) return NULL;

   vline_char = Graphic_VLine_Char;

   while (1)
     {
	last = h;
	
	if (h->child != NULL)
	  {
	     Slrn_Header_Type *child = h->child;
	     unsigned int tree_level;
	     unsigned int save_level = level;
	     
	     h->next = child;
	     child->prev = h;
	     
	     
	     if (level == 0)
	       {
		  if (h->flags & FAKE_CHILDREN)
		    {
		       if ((child->flags & FAKE_PARENT) == 0)
			 {
			    level = 1;
			 }
		    }
	       }
	     else if (h->flags & FAKE_PARENT)
	       {
		  if (h->sister != NULL) tree[0] = vline_char;
		  else tree[0] = ' ';
		  tree[1] = ' ';
		  level = 1;
	       }
	     
	     tree_level = 2 * level - 2;
	     
	     if (level && (tree_level < sizeof (h->tree) - 2))
	       {
		  if (h->sister != NULL)
		    {
		       if (((h->sister->flags & FAKE_PARENT) == 0)
			   || (h->flags & FAKE_PARENT))
			 {
			    tree[tree_level] = vline_char;
			 }
		       else tree[tree_level] = ' ';
		    }
		  else
		    {
		       if ((h->parent == NULL) && (h->flags & FAKE_CHILDREN))
			 {
			    tree[tree_level] = vline_char;
			 }
		       else tree[tree_level] = ' ';
		    }
		  tree[tree_level + 1] = ' ';
		  tree[tree_level + 2] = 0;
	       }
	     
	     level++;
	     last = sort_thread_node (h->child, tree);
	     level--;
	     
	     if (level &&
		 ((tree_level < sizeof (h->tree) - 2)))
	       tree[tree_level] = 0;
	     
	     level = save_level;
	  }
	
	if (h->flags & FAKE_PARENT) *tree = 0;
	
	if (*tree)
	  {
	     strncpy ((char *) h->tree, tree, sizeof (h->tree) - 1);
	     h->tree[sizeof (h->tree) - 1] = 0;
	  }
	h = h->sister;
	last->next = h;
	if (h == NULL) break;
	h->prev = last;
     }
   return last;
}

/*}}}*/

static unsigned int compute_num_children (Slrn_Header_Type *h) /*{{{*/
{
   unsigned int n = 0, dn;
   
   h = h->child;
   while (h != NULL)
     {
	n++;
	if (h->child == NULL) dn = 0;
	else
	  {
	     dn = compute_num_children (h);
	     n += dn;
	  }
	h->num_children = dn;
	h = h->sister;
     }
   return n;
}

/*}}}*/

unsigned int slrn_thread_size (Slrn_Header_Type *h)
{
   if (h == NULL) return 0;
   return 1 + compute_num_children (h);
}

int slrn_is_thread_collapsed (Slrn_Header_Type *h)
{
   if (h == NULL) return 1;
   while (h->parent != NULL) h = h->parent;
   if (h->child == NULL) return 0;
   return (h->child->flags & HEADER_HIDDEN);
}


static void sort_threads (void) /*{{{*/
{
   Slrn_Header_Type *h;
   char tree[MAX_TREE_SIZE];
   
   h = Slrn_First_Header;
   Headers = NULL;
   
   if (h == NULL) return;
   while (h != NULL)
     {
	if ((h->parent == NULL) && (Headers == NULL))
	  Headers = h;
	
	h->prev = h->next = NULL;
	h->flags &= ~HEADER_HIDDEN;
	h = h->real_next;
     }
   Threads_Collapsed = 0;
   
   if (Headers == NULL)
     slrn_exit_error ("Internal Error.");
   
   *tree = 0;
   sort_thread_node (Headers, tree);
   while (Headers->prev != NULL) Headers = Headers->prev;
   
   slrn_collapse_threads (0);
   
   h = Headers;
   while (h != NULL)
     {
	if (h->child == NULL) h->num_children = 0;
	else
	  {
	     Slrn_Header_Type *next;
	     h->num_children = compute_num_children (h);
	     next = h->next;
	     while ((next != NULL) && (next->parent != NULL))
	       {
#if SLRN_HAS_SORT_BY_SCORE
		  if (next->flags & HEADER_HIGH_SCORE)
		    h->flags |= FAKE_HEADER_HIGH_SCORE;
		  if (next->score > h->thread_score)
		    h->thread_score = next->score;
#else
		  if (next->flags & HEADER_HIGH_SCORE)
		    {
		       h->flags |= FAKE_HEADER_HIGH_SCORE;
		       break;
		    }
#endif
		  next = next->next;
	       }
	  }
	h = h->sister;
     }
   find_header_line_num ();
}

/*}}}*/

static void link_same_subjects (void) /*{{{*/
{
   Slrn_Header_Type **header_list, *h;
   unsigned int i, nparents;
   void (*qsort_fun) (char *, unsigned int, unsigned int, int (*)(Slrn_Header_Type **, Slrn_Header_Type **));
   
   /* This is a silly hack to make up for braindead compilers and the lack of
    * uniformity in prototypes for qsort.
    */
   qsort_fun = (void (*)(char *,
			 unsigned int, unsigned int,
			 int (*)(Slrn_Header_Type **, Slrn_Header_Type **)))
     qsort;
   
   h = Slrn_First_Header;
   nparents = 0;
   while (h != NULL)
     {
	if (h->parent == NULL)
	  nparents++;
	h = h->real_next;
     }
   if (nparents < 2) return;
   
   
   if (NULL == (header_list = (Slrn_Header_Type **) SLCALLOC (sizeof (Slrn_Header_Type *), nparents)))
     {
	slrn_error ("link_same_subjects: memory allocation failure.");
	return;
     }
   
   h = Slrn_First_Header;
   i = 0;
   while (i < nparents)
     {
	if (h->parent == NULL) header_list[i++] = h;
	h = h->real_next;
     }
   
   (*qsort_fun) ((char *) header_list,
		 nparents, sizeof (Slrn_Header_Type *), header_subj_cmp);
   
   h = header_list[0];
   for (i = 1; i < nparents; i++)
     {
	Slrn_Header_Type *h1 = header_list[i];
	if (0 == subject_cmp ((unsigned char *) h->subject,
			      (unsigned char *) h1->subject))
	  {
	     if (h->child == NULL)
	       {
		  h->child = h1;
	       }
	     else
	       {
		  Slrn_Header_Type *child = h->child;
		  while (child->sister != NULL) child = child->sister;
		  child->sister = h1;
	       }
	     
	     h1->parent = h;
	     h->flags |= FAKE_CHILDREN;
	     h1->flags |= FAKE_PARENT;
	     if (h1->flags & FAKE_CHILDREN)
	       {
		  /* Well, we have to link them up to the new parent.  That
		   * is, h1 will become their sister.  So, extract the
		   * adopted children of h1 and make them the sister,
		   */
		  Slrn_Header_Type *child = h1->child, *last_child;
		  last_child = child;
		  
		  /* child CANNOT be NULL here!! (the parent claims to have
						  children) */
		  child = child->sister;
		  while (child != NULL)
		    {
		       if (child->flags & FAKE_PARENT)
			 break;
		       last_child = child;
		       child = child->sister;
		    }
		  
		  if (last_child->flags & FAKE_PARENT)
		    {
		       child = last_child;
		       h1->child = NULL;
		    }
		  else last_child->sister = NULL;
		  
		  last_child = child;
		  while (child != NULL)
		    {
		       child->parent = h;
		       /* No need to set fake parent flags since fake children
			* are all group together.  That is, once you loop
			* through the sisters and find one, you have found them
			* all.
			*/
		       child = child->sister;
		    }
		  
		  /* Now h1 will become the sister. */
		  child = h1;
		  while (child->sister != NULL) child = child->sister;
		  child->sister = last_child;
		  h1->flags &= ~FAKE_CHILDREN;
	       }
	  }
	else h = h1;
     }
   SLFREE (header_list);
}

/*}}}*/

typedef struct /*{{{*/
{
   unsigned long ref_hash;
   Slrn_Header_Type *h;
}

/*}}}*/
Relative_Type;

static void link_lost_relatives (void) /*{{{*/
{
   unsigned int n, i, j;
   Slrn_Header_Type *h;
   Relative_Type *relatives;
   
   /* count the number of possible relatives */
   n = 0;
   h = Slrn_First_Header;
   while (h != NULL)
     {
	if ((h->parent == NULL)
	    && (h->refs != NULL)
	    && (*h->refs != 0)) n++;
	
	h = h->real_next;
     }
   if (n < 2) return;
   
   relatives = (Relative_Type *) slrn_malloc (sizeof (Relative_Type) * n, 0, 0);
   if (relatives == NULL)
     return;
   
   n = 0;
   h = Slrn_First_Header;
   while (h != NULL)
     {
	if ((h->parent == NULL)
	    && (h->refs != NULL)
	    && (*h->refs != 0))
	  {
	     unsigned char *r, *ref_begin;
	     
	     r = (unsigned char *) h->refs;
	     while (*r && (*r != '<')) r++;
	     if (*r == '<')
	       {
		  ref_begin = r;
		  while (*r && (*r != '>')) r++;
		  if (*r == '>') r++;
		  relatives[n].ref_hash = slrn_compute_hash (ref_begin, r);
		  relatives[n].h = h;
		  n++;
	       }
	  }
	h = h->real_next;
     }
   
   for (i = 0; i < n; i++)
     {
	unsigned long ref_hash;
	Relative_Type *ri = relatives + i;
	Slrn_Header_Type *rih;
	
	ref_hash = ri->ref_hash;
	rih = ri->h;
	
	for (j = i + 1; j < n; j++)
	  {
	     if (relatives[j].ref_hash == ref_hash)
	       {
		  Slrn_Header_Type *rjh = relatives[j].h;
		  
		  if (Slrn_New_Subject_Breaks_Threads
		      && (rih->subject != NULL)
		      && (rjh->subject != NULL)
		      && (0 != subject_cmp ((unsigned char *)rih->subject, (unsigned char *)rjh->subject)))
		    continue;
		  
		  if (rih->parent != NULL)
		    {
		       rih->sister = rjh;
		       rjh->parent = rih->parent;
		    }
		  else if (rih->child == NULL)
		    {
		       rih->child = rjh;
		       rjh->parent = rih;
		       rih->flags |= FAKE_CHILDREN;
		    }
		  else
		    {
		       Slrn_Header_Type *child = rih->child;
		       /* This is an important step.  All adopted children
			* get linked to the LAST child.  This ordering
			* assumption is used elsewhere.
			*/
		       while (child->sister != NULL) child = child->sister;
		       child->sister = rjh;
		       rjh->parent = rih;
		       rih->flags |= FAKE_CHILDREN;
		    }
		  rjh->flags |= FAKE_PARENT;
		  break;
	       }
	  }
     }
   SLFREE (relatives);
}

/*}}}*/

static void thread_headers (void) /*{{{*/
{
   Slrn_Header_Type *h, *ref;
   char *r0, *r1, *rmin;
   
   make_hash_table ();
   
   h = Slrn_First_Header;
   while (h != NULL)
     {
	h->next = h->prev = h->child = h->parent = h->sister = NULL;
	h->flags &= ~ALL_THREAD_FLAGS;
	*h->tree = 0;
	h = h->real_next;
     }
   
   /* SLMEMSET ((char *) Lost_Ancestors, 0, sizeof (Lost_Ancestors)); */
   
   h = Slrn_First_Header;
   while (h != NULL)
     {
	if (*h->refs == 0)
	  {
	     h = h->real_next;
	     continue;
	  }
	
	rmin = h->refs;
	r1 = rmin + strlen (rmin);
	
	while (1)
	  {
	     while ((r1 > rmin) && (*r1 != '>')) r1--;
	     r0 = r1 - 1;
	     while ((r0 >= rmin) && (*r0 != '<')) r0--;
	     if ((r0 < rmin) || (r1 == rmin)) break;
	     
	     ref = find_header_from_msgid (r0, r1 + 1);
	     
	     if (ref != NULL)
	       {
		  Slrn_Header_Type *child, *rparent;
		  
		  if (Slrn_New_Subject_Breaks_Threads
		      && (h->subject != NULL)
		      && (ref->subject != NULL)
		      && (0 != subject_cmp ((unsigned char *)h->subject, (unsigned char *)ref->subject)))
		    break;
		  
		  rparent = ref;
		  while (rparent->parent != NULL) rparent = rparent->parent;
		  if (rparent == h)
		    {
		       /* self referencing!!! */
		       slrn_error ("Article %d is part of reference loop!", h->number);
		    }
		  else
		    {
		       h->parent = ref;
		       child = ref->child;
		       if (child == NULL) ref->child = h;
		       else
			 {
			    while (child->sister != NULL) child = child->sister;
			    child->sister = h;
			 }
		       break;
		    }
	       }
	     r1 = r0;
	  }
	h = h->real_next;
     }
   
   /* No perform a re-arrangement such that those with the no parents but
    * share the same reference are placed side-by-side as sisters.
    */
   
   link_lost_relatives ();
   
   /* Now perform sort on subject to catch those that have fallen through the
    * cracks, i.e., no references */
   link_same_subjects ();
   
   /* Now link others up as sisters */
   h = Slrn_First_Header;
   while ((h != NULL) && (h->parent != NULL))
     {
	h = h->real_next;
     }
   
   while (h != NULL)
     {
	Slrn_Header_Type *next;
	next = h->real_next;
	while ((next != NULL) && (next->parent != NULL))
	  next = next->real_next;
	h->sister = next;
	h = next;
     }
}

/*}}}*/

/*}}}*/

/*{{{ select_article */

/* returns 0 if article selected, -1 if something went wrong or 1 if article
 * already selected.
 */
static int select_article (int do_mime) /*{{{*/
{
   int ret = 1;
   
   Slrn_Full_Screen_Update = 1;
   
   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   
   if (Slrn_Current_Header != Header_Showing)
     {
	if (read_article (Slrn_Current_Header, Slrn_Del_Article_Upon_Read, 1) < 0) return -1;
#if SLRN_HAS_MIME
	if (do_mime && Slrn_Use_Mime && Slrn_Mime_Needs_Metamail)
	  {
	     if (slrn_mime_call_metamail ())
	       return -1;
	  }
#endif
	ret = 0;
     }
   
   set_article_visibility (1);
   return ret;
}

/*}}}*/

/*}}}*/

/*{{{ mark_spot and exchange_mark */

static void mark_spot (void) /*{{{*/
{
   Mark_Header = Slrn_Current_Header;
   slrn_message ("Mark set.");
}

/*}}}*/


static void exchange_mark (void) /*{{{*/
{
   if (Mark_Header == NULL)
     {
	slrn_error ("Mark not set.");
	return;
     }
   
   if (-1 == slrn_goto_header (Mark_Header, 0)) return;
   mark_spot ();
}

/*}}}*/

/*}}}*/
/*{{{ subject/author header searching commands */

static void header_generic_search (int dir, int type) /*{{{*/
{
   static char search_str[256];
   SLsearch_Type st;
   Slrn_Header_Type *l;
   char prompt[80];
   
   sprintf (prompt, "%s Search %s",
	    type == 's' ? "Subject" : "Author",
	    dir > 0 ? "Forward" : "Backward");
   
   if (slrn_read_input (prompt, search_str, NULL, 0, 0) <= 0)
     return;
   
   SLsearch_init (search_str, 1, 0, &st);
   
   if (dir > 0) l = Slrn_Current_Header->next;
   else l = Slrn_Current_Header->prev;
   
   while (l != NULL)
     {
	if (type == 's')
	  {
	     if ((l->subject != NULL)
		 && (NULL != SLsearch ((unsigned char *) l->subject,
				       (unsigned char *) l->subject + strlen (l->subject),
				       &st)))
	       break;
	  }
	else if ((l->from != NULL)
		 && (NULL != SLsearch ((unsigned char *) l->from,
				       (unsigned char *) l->from + strlen (l->from),
				       &st)))
	  break;
	
	if (dir > 0) l = l->next; else l = l->prev;
     }
   
   if (l == NULL)
     {
	slrn_error ("Not found.");
	return;
     }
   
   if (l->flags & HEADER_HIDDEN) slrn_uncollapse_this_thread (l, 0);
   Slrn_Current_Header = l;
   find_header_line_num ();
}

/*}}}*/

static void subject_search_forward (void) /*{{{*/
{
   header_generic_search (1, 's');
}

/*}}}*/

static void subject_search_backward (void) /*{{{*/
{
   header_generic_search (-1, 's');
}

/*}}}*/

static void author_search_forward (void) /*{{{*/
{
   header_generic_search (1, 'a');
}

/*}}}*/

static void author_search_backward (void) /*{{{*/
{
   header_generic_search (-1, 'a');
}

/*}}}*/

/*}}}*/

/*{{{ score header support */

/*{{{ kill list functions */


typedef struct Kill_List_Type /*{{{*/
{
#define MAX_DKILLS 50
   int nums[MAX_DKILLS];
   unsigned int num_used;
   struct Kill_List_Type *next;
}

/*}}}*/

Kill_List_Type;

static Kill_List_Type *Kill_List;
static Kill_List_Type *Missing_Article_List;

static Kill_List_Type *add_to_specified_kill_list (int num, Kill_List_Type *root) /*{{{*/
{
   if (num < 0) return root;
   
   if ((root == NULL) || (root->num_used == MAX_DKILLS))
     {
	Kill_List_Type *k;
	k = (Kill_List_Type *) SLMALLOC (sizeof (Kill_List_Type));
	if (k == NULL) return root;
	k->num_used = 0;
	k->next = root;
	root = k;
     }
   root->nums[root->num_used++] = num;
   return root;
}

/*}}}*/

static void add_to_kill_list (int num) /*{{{*/
{
   Kill_List = add_to_specified_kill_list (num, Kill_List);
   Number_Killed++;
}

/*}}}*/

static void add_to_missing_article_list (int num) /*{{{*/
{
   Missing_Article_List = add_to_specified_kill_list (num, Missing_Article_List);
}

/*}}}*/

static void free_specific_kill_list_and_update (Kill_List_Type *k) /*{{{*/
{
   while (k != NULL)
     {
	Kill_List_Type *next = k->next;
	unsigned int i, imax = k->num_used;
	int *nums = k->nums;
	if (User_Aborted_Group_Read == 0) for (i = 0; i < imax; i++)
	  {
	     slrn_mark_article_as_read (NULL, nums[i]);
	  }
	SLFREE (k);
	k = next;
     }
}

/*}}}*/

static void free_kill_lists_and_update (void) /*{{{*/
{
   free_specific_kill_list_and_update (Kill_List);
   Kill_List = NULL;
   Number_Killed = 0;
   free_specific_kill_list_and_update (Missing_Article_List);
   Missing_Article_List = NULL;
}

/*}}}*/


/*}}}*/

Slrn_Header_Type *slrn_set_header_score (Slrn_Header_Type *h, 
					 int score, int apply_kill)
{
   if (h == NULL) return NULL;
   
   if (score >= Slrn_High_Score_Min)
     {
	h->flags &= ~(HEADER_LOW_SCORE);
	h->flags |= HEADER_HIGH_SCORE;
	Number_High_Scored++;
     }
   else if (score < Slrn_Low_Score_Max)
     {
	if ((score <= Slrn_Kill_Score_Max) && apply_kill)
	  {
	     int number = h->number;
	     free_header (h);
	     add_to_kill_list (number);
	     return NULL;
	  }
	
	h->flags &= ~(HEADER_HIGH_SCORE);
	h->flags |= (HEADER_READ | HEADER_LOW_SCORE);
	Number_Low_Scored++;
	/* The next line should be made configurable */
	kill_cross_references (h);
     }
#if SLRN_HAS_SORT_BY_SCORE
   h->thread_score = h->score = score;
#endif
   return h;
}

/*{{{ apply_score */
static Slrn_Header_Type *apply_score (Slrn_Header_Type *h) /*{{{*/
{
   int score;
   
   if (h == NULL) return h;
   
   if (Slrn_Apply_Score && Perform_Scoring)
     score = slrn_score_header (h, Slrn_Current_Group_Name);
   else score = 0;
   
   return slrn_set_header_score (h, score, 1);
}

/*}}}*/

/*}}}*/

/*{{{ score_headers */
static void score_headers (void) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_First_Header;
   int percent, last_percent, delta_percent;
   int num;
   
   if ((h == NULL) || (Slrn_Apply_Score == 0)) return;
   
   /* slrn_set_suspension (1); */
   
   percent = num = 0;
   delta_percent = (30 * 100) / Total_Num_Headers + 1;
   last_percent = -delta_percent;
   
   while (h != NULL)
     {
	Slrn_Header_Type *prev, *next;
	prev = h->real_prev;
	next = h->real_next;
	percent = (100 * num) / Total_Num_Headers;
	if (percent >= last_percent + delta_percent)
	  {
	     slrn_message_now ("Scoring articles: %2d%%, Killed: %u, High: %u, Low: %u",
			       percent, Number_Killed, Number_High_Scored, Number_Low_Scored);
	     last_percent = percent;
	  }
	
	num++;
	h = apply_score (h);
	
	if (h == NULL)
	  {
	     num--;
	     if (prev == NULL)
	       Slrn_First_Header = next;
	     else
	       prev->next = prev->real_next = next;
	     
	     if (next != NULL)
	       next->prev = next->real_prev = prev;
	  }
	h = next;
     }
   Slrn_Current_Header = Headers = Slrn_First_Header;
   /* slrn_set_suspension (0); */
}

/*}}}*/

/*}}}*/

static void create_score (void) /*{{{*/
{
   if (Slrn_Batch) return;

   (void) slrn_edit_score (Slrn_Current_Header, Slrn_Current_Group_Name);
}

/*}}}*/


/*}}}*/

/*{{{ get headers from server and process_xover */

static Slrn_Header_Type *process_xover (Slrn_XOver_Type *xov)
{
   Slrn_Header_Type *h;
   
   h = (Slrn_Header_Type *) slrn_safe_malloc (sizeof (Slrn_Header_Type));
   
   slrn_map_xover_to_header (xov, h);
   
   if ((Slrn_Score_After_XOver == 0) && Perform_Scoring)
     {
	if (NULL == (h = apply_score (h)))
	  return h;
     }
   
#if SLRN_HAS_MIME
   if (Slrn_Use_Mime)
     {
	slrn_rfc1522_decode_string (h->subject);
	slrn_rfc1522_decode_string (h->from);
     }
#endif
#if SLRN_HAS_GROUPLENS
   if (Slrn_Use_Group_Lens)
     {
	h->gl_rating = h->gl_pred = -1;
     }
#endif
   return h;
}


/*}}}*/

/*{{{ get_headers from server */
static int get_headers (int min, int max, int *totalp) /*{{{*/
{
   Slrn_Header_Type *h;
   /* int percent, last_percent, dpercent, */
   int expected_num;
   int scoring;
   int total = *totalp;
   int reads_per_update;
   int num_processed;
   Slrn_XOver_Type xov;
   
   if (total == 0)
     return 0;
   
   if (SLang_Error == USER_BREAK)
     return -1;
   
   scoring = Perform_Scoring && !Slrn_Score_After_XOver;

   if ((reads_per_update = Slrn_Reads_Per_Update) < 5)
     reads_per_update = 50;

   if (scoring) reads_per_update = reads_per_update / 3 + 1;
      
   /* slrn_set_suspension (1); */
   
   if (OK_XOVER != slrn_open_xover (min, max))
     return -1;
   
   num_processed = 0;
   expected_num = min;
   while (slrn_read_xover(&xov) > 0)
     {
	int this_num;
	int num = Total_Num_Headers + Number_Killed + num_processed;
	
	if (SLang_Error == USER_BREAK)
	  {
	     if (Slrn_Server_Obj->sv_reset != NULL)
	       Slrn_Server_Obj->sv_reset ();
	     return -1;
	  }

	this_num = xov.id;
	
	if (expected_num != this_num)
	  {
	     int bad_num;
	     
	     total -= (this_num - expected_num);
	     
	     for (bad_num = expected_num; bad_num < this_num; bad_num++)
	       add_to_missing_article_list (bad_num);
	  }
	
	expected_num = this_num + 1;
	
	h = process_xover (&xov);
	
	num++;

	if ((1 == (num % reads_per_update))
	    && (SLang_Error == 0))
	  {
	     if (scoring)
	       {
		  slrn_message_now ("Headers Received and Scored: %3d/%-3d, Killed: %u, High: %u, Low: %u",
				    num, total, 
				    Number_Killed, Number_High_Scored, Number_Low_Scored);
	       }
	     else
	       slrn_message_now ("Headers Received: %2d/%d", num, total);
	  }
	
	if (h == NULL) continue;
	
	if (Slrn_First_Header == NULL)
	  Slrn_First_Header = Headers = h;
	else
	  {
	     h->real_next = Slrn_Current_Header->real_next;
	     h->real_prev = Slrn_Current_Header;
	     Slrn_Current_Header->real_next = h;
	     
	     if (h->real_next != NULL)
	       {
		  h->real_next->real_prev = h;
	       }
	  }
	
	Slrn_Current_Header = h;
	num_processed++;
     }
   
   slrn_close_xover ();
   
   if (expected_num != max + 1)
     {
	int bad_num;
	     
	total -= (max - expected_num) + 1;
	     
	for (bad_num = expected_num; bad_num <= max; bad_num++)
	  add_to_missing_article_list (bad_num);
     }
   
   /* slrn_set_suspension (0); */
   *totalp = total;
   
   Total_Num_Headers += num_processed;

   return (int) num_processed;
}

/*}}}*/


/*}}}*/


/*}}}*/
/*{{{ get parent/children headers, etc... */

/* Nothing is synced by this routine.  It is up to the calling routine. */
static void insert_header (Slrn_Header_Type *ref) /*{{{*/
{
   int n, id;
   Slrn_Header_Type *h;
   Slrn_Range_Type *r;
   
   ref->hash_next = Header_Table[ref->hash % HEADER_TABLE_SIZE];
   Header_Table[ref->hash % HEADER_TABLE_SIZE] = ref;
   
   n = ref->number;
   h = Slrn_First_Header;
   while (h != NULL)
     {
	if (h->number >= n)
	  {
	     ref->real_next = h;
	     ref->real_prev = h->real_prev;
	     if (h->real_prev != NULL) h->real_prev->real_next = ref;
	     h->real_prev = ref;
	     
	     if (h == Slrn_First_Header) Slrn_First_Header = ref;
	     if (h == Headers) Headers = ref;
	     
	     break;
	  }
	h = h->real_next;
     }
   
   if (h == NULL)
     {
	h = Slrn_First_Header;
	while (h->real_next != NULL) h = h->real_next;
	
	ref->real_next = NULL;
	ref->real_prev = h;
	h->real_next = ref;
     }
   
   if ((id = ref->number) <= 0) return;
   
   /* Set the flags for this guy. */
   r = Current_Group->range.next;
   while (r != NULL)
     {
	if (r->min > id) break;
	if (r->max >= id)
	  {
	     ref->flags = HEADER_READ;
	     return;
	  }
	r = r->next;
     }
   ref->flags &= ~HEADER_READ;
}

/*}}}*/

/* line number is not synced. */
static int get_header_by_message_id (char *msgid, int no_error_no_thread) /*{{{*/
{
   Slrn_Header_Type *ref;
   Slrn_XOver_Type xov;
   
   if ((msgid == NULL) || (*msgid == 0)) return -1;
   
   ref = find_header_from_msgid (msgid, msgid + strlen (msgid));
   if (ref != NULL)
     {
	Slrn_Current_Header = ref;
	if (no_error_no_thread == 0)
	  find_header_line_num ();
	Slrn_Full_Screen_Update = 1;
	return 0;
     }
   
   slrn_message_now ("Finding %s from server...", msgid);
   
   /* Try reading it from the server */
   if (-1 == slrn_xover_for_msgid (msgid, &xov))
     {
	if (no_error_no_thread == 0)
	  {
	     slrn_error ("Article %s not available.", msgid);
	     return -1;
	  }
	return 1;
     }
   
   ref = process_xover (&xov);

   if (ref == NULL) return -1;
   
   get_header_real_name (ref);
   
   insert_header (ref);
   
   Slrn_Current_Header = ref;
   if (no_error_no_thread == 0)
     {
	sort_by_sorting_mode ();
     }
   return 0;
}

/*}}}*/

/* returns -1 if not implemented or the number of children returned from
 * the server.  It does not sync line number. 
 */  
static int find_children_headers (Slrn_Header_Type *parent) /*{{{*/
{
   char buf[NNTP_BUFFER_SIZE];
   int id_array[1000];
   int num_ids, i, id;
   char *fmt = "Finding children from server...[%c]";
   char *meter_chars = "|/-\\";
   static unsigned int last_meter_char;

   if (OK_HEAD != Slrn_Server_Obj->sv_xpat_cmd ("References",
						Slrn_Server_Min, Slrn_Server_Max,
						parent->msgid))
     {
	slrn_error ("Your server does not provide support for this feature.");
	return -1;
     }
   
   if (meter_chars[last_meter_char] == 0)
     last_meter_char = 0;
   
   slrn_message_now (fmt, meter_chars[last_meter_char]);
   last_meter_char++;
   
   num_ids = 0;
   while (Slrn_Server_Obj->sv_read_line (buf, sizeof (buf) - 1) != NULL)
     {
	if (meter_chars[last_meter_char] == 0)
	  last_meter_char = 0;
   
	slrn_message_now (fmt, meter_chars[last_meter_char]);
	last_meter_char++;
	
	id = atoi (buf);
	if (id <= 0) continue;
	
	if (NULL != find_header_from_serverid (id)) continue;
	id_array[num_ids] = id;
	num_ids++;
     }
   
   for (i = 0; i < num_ids; i++)
     {
	Slrn_XOver_Type xov;
	
	id = id_array[i];

	if (OK_XOVER != slrn_open_xover (id, id))
	  break;
	
	/* This will loop once. */
	while (slrn_read_xover (&xov) > 0)
	  {
	     Slrn_Header_Type *bad_h, *h;
	     
	     h = process_xover (&xov);
	     if (h == NULL) continue;
	     
	     /* We may already have this header.  How is this possible?
	      * If the header was retrieved sometime earlier via HEAD
	      * <msgid>, the server may not have returned the article
	      * number.  As a result, that header may have a number
	      * of -1.  Here, we really have the correct article
	      * number since the previous while loop made sure of that.
	      * So, before inserting it, check to see whether or not we
	      * have it and if so, fixup the id.
	      */
	     
	     bad_h = slrn_find_header_with_msgid (h->msgid);
	     if (bad_h != NULL)
	       {
		  bad_h->number = h->number;
		  free_header (h);
		  continue;
	       }

	     get_header_real_name (h);
	     insert_header (h);
	  }
	slrn_close_xover ();
     }
   return num_ids;
}

/*}}}*/

/* Line number not synced. */
static void get_children_headers_1 (Slrn_Header_Type *h) /*{{{*/
{
   while (h != NULL)
     {
	(void) find_children_headers (h);
	if (h->child != NULL)
	  {
	     get_children_headers_1 (h->child);
	  }
	h = h->sister;
     }
}

/*}}}*/

static void get_children_headers (void) /*{{{*/
{
   Slrn_Header_Type *h;
   
   /* slrn_set_suspension (1); */
   
   if (find_children_headers (Slrn_Current_Header) < 0)
     {
	/* slrn_set_suspension (0); */
	return;
     }
   
   sort_by_sorting_mode ();
   
   h = Slrn_Current_Header->child;
   if (h != NULL) 
     {
	/* Now walk the tree getting children headers.  For efficiency,
	 * only children currently threaded will be searched.  Hopefully the
	 * above attempt got everything.  If other newsreaders did not chop off
	 * headers, this would be unnecessary!
	 */
	get_children_headers_1 (h);
	sort_by_sorting_mode ();
     }
   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   
   /* slrn_set_suspension (0); */
}

/*}}}*/

static void get_parent_header (void) /*{{{*/
{
   char *r1, *r0, *rmin;
   unsigned int len;
   char buf[512];
   int no_error_no_thread;
   Slrn_Header_Type *last_header;
   
   if (Slrn_Current_Header == NULL) return;
   
   if (Slrn_Prefix_Arg_Ptr == NULL) no_error_no_thread = 0;
   else no_error_no_thread = 1;
   
   last_header = NULL;
   r1 = rmin = NULL;
   do
     {
	rmin = Slrn_Current_Header->refs;
	if (rmin == NULL) break;
	
	if (last_header != Slrn_Current_Header)
	  {
	     last_header = Slrn_Current_Header;
	     r1 = rmin + strlen (rmin);
	  }
	
	while ((r1 > rmin) && (*r1 != '>')) r1--;
	r0 = r1 - 1;
	while ((r0 >= rmin) && (*r0 != '<')) r0--;
	
	if ((r0 < rmin) || (r1 == rmin))
	  {
	     if (no_error_no_thread) break;
	     slrn_error ("Article has no parent reference.");
	     return;
	  }
	
	len = (unsigned int) ((r1 + 1) - r0);
	strncpy (buf, r0, len);
	buf[len] = 0;
	r1 = r0;
     }
   while ((get_header_by_message_id (buf, no_error_no_thread) >= 0)
	  && no_error_no_thread);
   
   if (no_error_no_thread)
     {
	sort_by_sorting_mode ();
	if (SLKeyBoard_Quit == 0) get_children_headers ();
     }
   
   slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
   slrn_chmap_fix_headers ();  /* We didn't decode parent headers */
}

/*}}}*/

static void locate_header_by_msgid (void) /*{{{*/
{
   char msgid[256];
   *msgid = 0;
   if (slrn_read_input ("Enter Message-Id", NULL, msgid, 1, 0) <= 0) return;
   if (0 == get_header_by_message_id (msgid, 0))
     {
	/* The actual header might be part of a collapsed thread.  Make sure that
	 * it is uncollapsed.
	 */
	slrn_uncollapse_this_thread (Slrn_Current_Header, 1);
	(void) get_header_by_message_id (msgid, 0);
     }
}

/*}}}*/


/*}}}*/

/*{{{ article window display modes */

static void hide_article (void) /*{{{*/
{
   Slrn_Full_Screen_Update = 1;
   if (Article_Visible == 0)
     {
	select_article (1);
	return;
     }
   
   set_article_visibility (0);
   Article_Window_HScroll = 0;
}

/*}}}*/

static void zoom_article_window (void)
{
   int zoomed_rows;

   if (Article_Visible == 0)
     hide_article ();

   if (Article_Visible == 0)
     return;
   
   zoomed_rows = SLtt_Screen_Rows - 3;
   if (zoomed_rows == Article_Window_Nrows)
     /* already zoomed.  Unzoom */
     Article_Window_Nrows = 0;
   else
     Article_Window_Nrows = zoomed_rows;

   art_winch ();
}

int slrn_is_article_win_zoomed (void)
{
   return (Article_Window_Nrows == SLtt_Screen_Rows - 3);
}

static void art_left (void) /*{{{*/
{
   if ((Article_Visible == 0)
       || (Article_Window_HScroll == 0))
     {
	if (Header_Window_HScroll == 0) return;
	Header_Window_HScroll -= SLtt_Screen_Cols / 5;
	if (Header_Window_HScroll < 0) Header_Window_HScroll = 0;
     }
   else
     {
	if (Article_Window_HScroll == 0) return;
	Article_Window_HScroll -= (SLtt_Screen_Cols * 2) / 3;
	if (Article_Window_HScroll < 0) Article_Window_HScroll = 0;
     }
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void art_right (void) /*{{{*/
{
   if (Article_Visible == 0)
     Header_Window_HScroll += SLtt_Screen_Cols / 5;
   else
     Article_Window_HScroll += (SLtt_Screen_Cols * 2) / 3;

   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

/*{{{ rot13 and spoilers */
static void toggle_rot13 (void) /*{{{*/
{
   Do_Rot13 = !Do_Rot13;
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

 
#if SLRN_HAS_SPOILERS
static void show_spoilers (void) /*{{{*/
{
   Slrn_Article_Line_Type *l1 = NULL;
   Slrn_Article_Line_Type *l = Slrn_Article_Lines;
   
   do
     {
	/* first find the first spoiler-ed line */
	while ((l != NULL) && 
	       (0 == (l->flags & SPOILER_LINE)))
	  {
	     l = l->next;
	  }
	if (l1 == NULL) l1 = l;
	/* now un-spoiler until we hit an un-spoiler-ed line */
	while ((l != NULL) && (l->flags & SPOILER_LINE))
	  {
	     l->flags &= ~SPOILER_LINE;
	     l = l->next;
	  }
     } /* Prefix arg means un-spoiler the whole article */
   while ((l != NULL)
	  && ((Slrn_Prefix_Arg_Ptr != NULL) 
	      || (Slrn_Spoiler_Display_Mode & 2)));

   Slrn_Prefix_Arg_Ptr = NULL;
   
   if ((Slrn_Spoiler_Display_Mode & 1) && (l1 != NULL))
     {
	Article_Current_Line = l1;
	find_article_line_num ();
     }
   
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

#endif


/*}}}*/

/*{{{ hide/toggle quotes */

/* Does NOT update article line number */
static void hide_quotes (void) /*{{{*/
{
   Slrn_Article_Line_Type *l = Slrn_Article_Lines, *last = NULL;
   while (l != NULL)
     {
	if (l->flags & QUOTE_LINE)
	  {
	     if (Quotes_Hidden && (last != NULL)) l->flags |= HIDDEN_LINE;
	     else l->flags &= ~HIDDEN_LINE;
	     last = l;
	  }
	else last = NULL;
	l = l->next;
     }
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

/* This function needs generalized on an article by article basis */
static void toggle_quotes (void) /*{{{*/
{
   Quotes_Hidden = !Quotes_Hidden;
   hide_quotes ();
   find_article_line_num ();
}

/*}}}*/

/*}}}*/


static void toggle_headers (void) /*{{{*/
{
   Headers_Hidden_Mode = !Headers_Hidden_Mode;
   hide_art_headers ();
   find_article_line_num ();
   if (Headers_Hidden_Mode == 0) art_bob ();
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

/*}}}*/

/*{{{ leave/suspend article mode and support functions */

static void update_ranges (void) /*{{{*/
{
   int min, max;
   int queued = 0;
   Slrn_Range_Type *r, *r_save;
   Slrn_Header_Type *h = Slrn_First_Header;
   int save_min, save_max, save_dirty;
   
   if (User_Aborted_Group_Read) return;
   
   /* skip articles for which the numeric id was not available */
   while ((h != NULL) && (h->number < 0)) h = h->real_next;
   if (h == NULL) return;
   
   /* we are creating new ranges so steal old. */
   r = Current_Group->range.next;
   Current_Group->range.next = NULL;
   
   /* Save the range context because we will do a comparison to see whether
    * or not the group was modified.
    */
   r_save = r;
   save_min = Current_Group->range.min;
   save_max = Current_Group->range.max;
   save_dirty = Slrn_Groups_Dirty;
   
   /* fill in range for articles prior to current group of headers.  This
    * is done in two parts.  First, mark as read everything up to the
    * first article on the server plus what we have read after that.  Then,
    * fill in the gap for articles on the server up to this group.
    */
   
   max = Slrn_Server_Min - 1;
   min = h->number;	       /* starting number of headers */
   
   /* Part one.  Skip past ranges that are below the server minimum number.
    * They will be fused together.
    */
   while ((r != NULL) && (r->min <= max))
     {
	if (r->max >= max)
	  {
	     max = r->max;
	     if (max >= min) max = min - 1;
	     r = r->next;
	     break;
	  }
	r = r->next;
     }
   slrn_add_group_ranges (Current_Group, 1, max);
   
   /* second part */
   while ((r != NULL) && (r->min < min))
     {
	max = r->max;
	if (max >= min) max = min - 1;
	slrn_add_group_ranges (Current_Group, r->min, max);
	r = r->next;
     }
   
   /* Now handle ranges in the current group.
    */
   
   queued = 0;
   max = Slrn_Server_Max;
   while (h != NULL)
     {
	max = h->number;
	if (h->flags & HEADER_READ)
	  {
	     queued = 1;
	  }
	else
	  {
	     if (queued || (min != max))
	       {
		  slrn_add_group_ranges (Current_Group, min, max - 1);
		  queued = 0;
	       }
	     
	     min = max + 1;	       /* mark next number as start of
					* a read range.  We mark it now
					* because h->next->number might not
					* be max + 1.
					*/
	  }
	h = h->real_next;
     }
   
   
   if (queued == 0) min = max + 1;
   slrn_add_group_ranges (Current_Group, min, Slrn_Server_Max);
   
   Slrn_Groups_Dirty = 1;
   
   if ((save_dirty == 0) 
       && (Current_Group->range.min == save_min) 
       && (Current_Group->range.max == save_max)
       && (Current_Group->range.next != r_save))
     {
	/* Compare the newly constructed ranges to the old.  If they differ
	 * then things were changed.
	 */
	Slrn_Range_Type *new_r;
	
	new_r = Current_Group->range.next;
	r = r_save;
	
	while ((new_r != NULL) && (r != NULL)
	       && (new_r->min == r->min)
	       && (new_r->max == r->max))
	  {
	     new_r = new_r->next;
	     r = r->next;
	  }
	
	if ((new_r == NULL) && (r == NULL))
	  Slrn_Groups_Dirty = save_dirty;
     }
   
   r = r_save;
   while (r != NULL)
     {
	r_save = r->next;
	SLFREE (r);
	r = r_save;
     }
}

/*}}}*/


/*{{{ art_quit */
static void art_quit (void) /*{{{*/
{
   Slrn_Header_Type *h = Headers, *next;

#if SLRN_HAS_SLANG
   (void) SLang_run_hooks ("article_mode_quit_hook", 0);
#endif

   slrn_init_hangup_signals (0);

#if SLRN_HAS_GROUPLENS
   if (Slrn_Use_Group_Lens) slrn_put_grouplens_scores ();
#endif
   
   free_article ();
   
   free_kill_lists_and_update ();
   free_tag_list ();
   
   slrn_close_score ();
   
   if (h != NULL)
     {
	update_ranges ();
     }
   
   while (h != NULL)
     {
	next = h->next;
	free_header (h);
	h = next;
     }
   
   Slrn_First_Header = Headers = Slrn_Current_Header = NULL;
   SLMEMSET ((char *) &Slrn_Header_Window, 0, sizeof (SLscroll_Window_Type));
   Total_Num_Headers = 0;
   
   Current_Group = NULL;
   Last_Read_Header = NULL;
   *Output_Filename = 0;

   Same_Subject_Start_Header = NULL;
   Slrn_Current_Group_Name = NULL;

   /* Since this function may get called before the mode is pushed, 
    * only pop it if the mode is really article mode.
    */
   if ((Slrn_Current_Mode != NULL)
       && (Slrn_Current_Mode->mode == SLRN_ARTICLE_MODE))
     slrn_pop_mode ();

   slrn_init_hangup_signals (1);
}

/*}}}*/

/*}}}*/

static void skip_to_next_group (void) /*{{{*/
{
   art_quit ();
   slrn_select_next_group ();
}

/*}}}*/

static void skip_to_prev_group (void) /*{{{*/
{
   art_quit ();
   slrn_select_prev_group ();
}

/*}}}*/

static void fast_quit (void) /*{{{*/
{
   art_quit ();
   slrn_group_quit ();
}

/*}}}*/

static void art_suspend_cmd (void) /*{{{*/
{
   int rows = SLtt_Screen_Rows;
   slrn_suspend_cmd ();
   if (rows != SLtt_Screen_Rows)
     art_winch_sig (rows, -1);
}

/*}}}*/

/*}}}*/
/*{{{ art_xpunge */
static void art_xpunge (void) /*{{{*/
{
   Slrn_Header_Type *save, *next, *h;
   
   free_article ();
   free_kill_lists_and_update ();
   
   save = Headers;
   if (Headers != NULL)
     {
	update_ranges ();
     }
   
   while (Headers != NULL)
     {
	if (0 == (Headers->flags & HEADER_READ))
	  break;
	Headers = Headers->next;
     }
   
   if (Headers == NULL)
     {
	Headers = save;
	art_quit ();
	return;
     }
   
   if ((Num_Tag_List.len != 0)
       && (Num_Tag_List.headers != NULL))
     {
	unsigned int i, j;
	Slrn_Header_Type *th;
	
	j = 0;
	for (i = 0; i < Num_Tag_List.len; i++)
	  {
	     th = Num_Tag_List.headers[i];
	     if (th->flags & HEADER_READ)
	       {
		  th->tag_number = 0;
		  th->flags &= ~HEADER_NTAGGED;
		  continue;
	       }
	     
	     Num_Tag_List.headers [j] = th;
	     j++;
	     th->tag_number = j;
	  }
	Num_Tag_List.len = j;
     }
   
   next = Slrn_Current_Header;
   while (next != NULL)
     {
	if (0 == (next->flags & HEADER_READ))
	  break;
	next = next->next;
     }
   
   if (next == NULL)
     {
	next = Slrn_Current_Header;
	while (next != NULL)
	  {
	     if (0 == (next->flags & HEADER_READ))
	       break;
	     next = next->prev;
	  }
     }
   
   Slrn_Current_Header = next;	       /* cannot be NULL */
   
   h = Slrn_First_Header;
   /* h cannot be NULL here*/
   while (1)
     {
	next = h->real_next;
	if (0 == (h->flags & HEADER_READ))
	  break;
	free_header (h);
	h = next;
     }
   Slrn_First_Header = h;
   h->real_prev = NULL;
   
   while (h != NULL)
     {	
	Slrn_Header_Type *next_next;

	next = h->real_next;
	while (next != NULL)
	  {
	     next_next = next->real_next;
	     if (0 == (next->flags & HEADER_READ))
	       break;
	     free_header (next);
	     next = next_next;
	  }
	h->real_next = next;
	if (next != NULL)
	  next->real_prev = h;
	h = next;
     }
   
   Last_Read_Header = NULL;
   h = Headers = Slrn_First_Header;
   Headers->prev = NULL;
   
   while (h != NULL)
     {
	h->next = next = h->real_next;
	if (next != NULL)
	  {
	     next->prev = h;
	  }
	h = next;
     }
   
   sort_by_sorting_mode ();
   
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/


/*}}}*/
/*{{{ cancel_article */

static void cancel_article (void) /*{{{*/
{
   char *from, *msgid, *newsgroups, *dist;
   char me[256];
   
   if (-1 == slrn_check_batch ())
     return;
   
   if (-1 == select_article (0)) return;

   slrn_update_screen ();
   
   if (slrn_get_yesno (0, "Are you sure that you want to cancel this article") <= 0)
     return;
   
   slrn_message_now ("Cancelling...");
   
   /* TO cancel, we post a cancel message with a 'control' header.  First, check to
    * see if this is really the owner of the message.
    */
   
   from = slrn_extract_header ("From: ", 6);
   if (from != NULL) from = parse_from (from);
   if (from == NULL) from = "";
   sprintf (me, "%s@%s", Slrn_User_Info.username, Slrn_User_Info.host);
   
   if (slrn_case_strcmp ((unsigned char *) from, (unsigned char *) me))
     {
	slrn_error ("Failed: Your name: '%s' is not '%s'", me, from);
	return;
     }
   
   if (NULL == (newsgroups = slrn_extract_header ("Newsgroups: ", 12)))
     newsgroups = "";
   
   if (NULL == (msgid = slrn_extract_header ("Message-ID: ", 12)))
     {
	slrn_error ("No message id.");
	return;
     }
   
   
   dist = slrn_extract_header("Distribution: ", 14);
   
   if (Slrn_Post_Obj->po_start () < 0) return;
   
   Slrn_Post_Obj->po_printf ("From: %s\nNewsgroups: %s\nSubject: cancel %s\nControl: cancel %s\n",
		from, newsgroups, msgid, msgid);
   
   if (dist != NULL)
     {
	Slrn_Post_Obj->po_printf ("Distribution: %s\n", dist);
     }
   
   Slrn_Post_Obj->po_printf("\nignore\nArticle canceled by slrn %s\n", Slrn_Version);
   
   if (0 == Slrn_Post_Obj->po_end ())
     {
	slrn_message ("Done.");
     }
}

/*}}}*/


/*}}}*/

/*{{{ header/thread (un)deletion/(un)catchup */
static void delete_header (Slrn_Header_Type *h) /*{{{*/
{
   if (h->flags & HEADER_TAGGED) return;
   if (0 == (h->flags & HEADER_READ))
     {
	kill_cross_references (h);
	h->flags |= HEADER_READ;
     }
}

/*}}}*/

static void undelete_header (Slrn_Header_Type *h) /*{{{*/
{
   if (h->flags & HEADER_TAGGED) return;
   h->flags &= ~HEADER_READ;
}

/*}}}*/

static void catch_up_all (void) /*{{{*/
{
   for_all_headers (delete_header, 1);
}

/*}}}*/

static void un_catch_up_all (void) /*{{{*/
{
   for_all_headers (undelete_header, 1);
}

/*}}}*/

static void catch_up_to_here (void) /*{{{*/
{
   for_all_headers (delete_header, 0);
}

/*}}}*/

static void un_catch_up_to_here (void) /*{{{*/
{
   for_all_headers (undelete_header, 0);
}

/*}}}*/


static void undelete_header_cmd (void) /*{{{*/
{
   if ((Slrn_Current_Header->parent != NULL)/* in middle of thread */
       || (Slrn_Current_Header->child == NULL)/* At top with no child */
       /* or at top with child showing */
       || (0 == (Slrn_Current_Header->child->flags & HEADER_HIDDEN)))
     {
	undelete_header (Slrn_Current_Header);
     }
   else
     {
	for_this_tree (Slrn_Current_Header, undelete_header);
     }
   slrn_header_down_n (1, 0);
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void delete_header_cmd (void) /*{{{*/
{
   if ((Slrn_Current_Header->parent != NULL)/* in middle of thread */
       || (Slrn_Current_Header->child == NULL)/* At top with no child */
       /* or at top with child showing */
       || (0 == (Slrn_Current_Header->child->flags & HEADER_HIDDEN)))
     {
	delete_header (Slrn_Current_Header);
     }
   else
     {
	for_this_tree (Slrn_Current_Header, delete_header);
     }
   slrn_next_unread_header ();
   Slrn_Full_Screen_Update = 1;
}

/*}}}*/

static void thread_delete_cmd (void) /*{{{*/
{
   for_this_tree (Slrn_Current_Header, delete_header);
   delete_header_cmd ();
}

/*}}}*/


/*}}}*/

/*{{{ group_lens functions */
#if SLRN_HAS_GROUPLENS
static void grouplens_rate_article (void) /*{{{*/
{
   int ch;
   
   if ((Slrn_Current_Header == NULL)
       || (Num_GroupLens_Rated == -1))
     return;
   
   slrn_message_now ("Rate article (1-5):");
   
   ch = SLang_getkey ();
   if ((ch < '1') || (ch > '5'))
     {
	slrn_error ("Rating must be in range 1 to 5.");
	return;
     }
   
   slrn_group_lens_rate_article (Slrn_Current_Header, ch - '0',
				 (Article_Visible && (Header_Showing == Slrn_Current_Header)));
}

/*}}}*/

#endif
/*}}}*/
/*{{{ mouse commands */

/* actions for different regions:
 *	- top status line (help)
 *	- header status line
 *	- above header status line
 *	- below header status line
 *	- bottom status line
 */
static void art_mouse (void (*top_status)(void), /*{{{*/
		       void (*header_status)(void),
		       void (*bot_status)(void),
		       void (*normal_region)(void)
		       )
{
   int r, c;
   slrn_get_mouse_rc (&r, &c);
   
   /* take top status line into account */
   if (r == 1)
     {
	if (Slrn_Use_Mouse)
	  (void) slrn_execute_menu (c);
	else if (NULL != top_status) (*top_status) ();
 	return;
     }
   
   if (r >= SLtt_Screen_Rows)
     return;
   
   /* On header status line */
   if (r - 2 == Header_Window_Nrows)
     {
	if (NULL != header_status) (*header_status) ();
 	return;
     }
   
   /* bottom status line */
   if (r == SLtt_Screen_Rows - 1)
     {
	if (NULL != bot_status) (*bot_status) ();
	return;
     }
   
   if (r - 2 > Header_Window_Nrows)
     {
	if (NULL != normal_region) (*normal_region) ();
 	return;
     }
   
   r -= (1 + Last_Cursor_Row);
   if (r < 0)
     {
	r = -r;
	if (r != (int) slrn_header_up_n (r, 0)) return;
     }
   else if (r != (int) slrn_header_down_n (r, 0)) return;
   
   select_article (1);
   /* if (NULL != normal_region) (*normal_region) (); */
   
}

/*}}}*/


static void art_mouse_left (void) /*{{{*/
{
   art_mouse (slrn_article_help, header_pagedn,
	      art_next_unread, art_pagedn);
}

/*}}}*/

static void art_mouse_middle (void) /*{{{*/
{
   art_mouse (toggle_header_formats, hide_article,
	      toggle_quotes, hide_article);
#if 1
   /* Make up for buggy rxvt which have problems with the middle key. */
   if (NULL != getenv ("COLORTERM"))
     {
	if (SLang_input_pending (7))
	  {
	     while (SLang_input_pending (0)) SLang_getkey ();
	  }
     }
#endif
}

/*}}}*/


static void art_mouse_right (void) /*{{{*/
{
   art_mouse (slrn_article_help, header_pageup,
	      art_prev_unread, art_pageup);
}

/*}}}*/

/*}}}*/

/*{{{ slrn_init_article_mode */

#define A_KEY(s, f)  {s, (int (*)(void)) f}

static SLKeymap_Function_Type Art_Functions [] = /*{{{*/
{
#if SLRN_HAS_GROUPLENS
   A_KEY("grouplens_rate_article", grouplens_rate_article),
#endif
   A_KEY("print_article", print_article_cmd),
   A_KEY("digit_arg", slrn_digit_arg),
   A_KEY("browse_url", browse_url),
   A_KEY("art_xpunge", art_xpunge),
   A_KEY("wrap_article", toggle_wrap_article),
   A_KEY("goto_last_read", goto_last_read),
#if SLRN_HAS_DECODE
   A_KEY("decode", decode_article),
#endif
#if 1
#if SLRN_HAS_SPOILERS
     A_KEY("show_spoilers", show_spoilers),
#endif
#endif
     A_KEY("create_score", create_score),
     A_KEY("toggle_collapse_threads", toggle_collapse_threads),
     A_KEY("toggle_header_tag", toggle_header_tag),
     A_KEY("tag_header", num_tag_header),
     A_KEY("untag_headers", num_untag_headers),
     A_KEY("repeat_last_key", slrn_repeat_last_key),
     A_KEY("art_bob", art_bob),
     A_KEY("art_eob", art_eob),
     A_KEY("goto_beginning", art_bob),
     A_KEY("goto_end", art_eob),
     A_KEY("forward_digest", skip_digest_forward),
     A_KEY("locate_article", locate_header_by_msgid),
     A_KEY("delete_thread", thread_delete_cmd),
     A_KEY("post", slrn_post_cmd),
     A_KEY("get_children_headers", get_children_headers),
     A_KEY("get_parent_header", get_parent_header),
     A_KEY("skip_to_next_group", skip_to_next_group),
     A_KEY("skip_to_prev_group", skip_to_prev_group),
     A_KEY("fast_quit", fast_quit),
     A_KEY("catchup_all", catch_up_all),
     A_KEY("uncatchup_all", un_catch_up_all),
     A_KEY("catchup", catch_up_to_here),
     A_KEY("uncatchup", un_catch_up_to_here),
     A_KEY("pipe_article", pipe_article),
     A_KEY("toggle_rot13", toggle_rot13),
     A_KEY("toggle_show_author", toggle_header_formats),
     A_KEY("toggle_header_formats", toggle_header_formats),
     A_KEY("toggle_sort", toggle_sort),
     A_KEY("skip_quotes", skip_quoted_text),
     A_KEY("header_bob", header_bob),
     A_KEY("header_eob", header_eob),
     A_KEY("goto_article", goto_article),
     A_KEY("shrink_window", shrink_window),
     A_KEY("enlarge_window", enlarge_window),
     A_KEY("scroll_dn", art_pagedn),
     A_KEY("scroll_up", art_pageup),
     A_KEY("article_pagedn", art_pagedn),
     A_KEY("article_pageup", art_pageup),
     A_KEY("article_linedn", art_linedn),
     A_KEY("article_lineup", art_lineup),
     A_KEY("article_search", article_search),
     A_KEY("author_search_backward", author_search_backward),
     A_KEY("author_search_forward", author_search_forward),
     A_KEY("cancel", cancel_article),
     A_KEY("delete", delete_header_cmd),
     A_KEY("down", header_down),
     A_KEY("exchange_mark", exchange_mark),
     A_KEY("followup", followup),
     A_KEY("forward", forward_article),
     A_KEY("help", slrn_article_help),
     A_KEY("hide_article", hide_article),
     A_KEY("left", art_left),
     A_KEY("mark_spot", mark_spot),
     A_KEY("next", art_next_unread),
     A_KEY("prev", art_prev_unread),
     A_KEY("quit", art_quit),
     A_KEY("redraw", slrn_redraw),
     A_KEY("reply", reply_cmd),
     A_KEY("right", art_right),
     A_KEY("save", save_article),
     A_KEY("subject_search_backward", subject_search_backward),
     A_KEY("subject_search_forward", subject_search_forward),
     A_KEY("suspend", art_suspend_cmd),
     A_KEY("toggle_headers", toggle_headers),
     A_KEY("toggle_quotes", toggle_quotes),
     A_KEY("undelete", undelete_header_cmd),
     A_KEY("up", header_up),
     A_KEY("pageup", header_pageup),
     A_KEY("pagedn", header_pagedn),
     A_KEY("next_same_subject", next_header_same_subject),
     A_KEY("next_high_score", next_high_score),
     A_KEY("next_high_score", next_high_score),
     A_KEY("locate_header_by_msgid", locate_header_by_msgid),
     A_KEY("post_postponed", slrn_post_postponed),
     A_KEY("zoom_article_window", zoom_article_window),
     A_KEY(NULL, NULL)
};

/*}}}*/


static Slrn_Mode_Type Art_Mode_Cap = /*{{{*/
{
   NULL,			       /* keymap */
   art_update_screen,
   art_winch_sig,		       /* sigwinch_fun */
   slrn_art_hangup,
   NULL,			       /* enter_mode_hook */
   SLRN_ARTICLE_MODE,
};

/*}}}*/



void slrn_init_article_mode (void) /*{{{*/
{
   char  *err = "Unable to create Article keymap!";
   char numbuf[2];
   char ch;
   
   if (NULL == (Slrn_Article_Keymap = SLang_create_keymap ("article", NULL)))
     slrn_exit_error (err);
   
   Art_Mode_Cap.keymap = Slrn_Article_Keymap;
   
   Slrn_Article_Keymap->functions = Art_Functions;
   Art_Functions_Ptr = Art_Functions;
   
   numbuf[1] = 0;
   
   for (ch = '0'; ch <= '9'; ch++)
     {
	numbuf[0] = ch;
	SLkm_define_key (numbuf, (FVOID_STAR) goto_header_number, Slrn_Article_Keymap);
     }
#if SLRN_HAS_GROUPLENS
   numbuf[0] = '0';
   /* Steal '0' for use as a prefix for rating. */
   SLkm_define_key  (numbuf, (FVOID_STAR) grouplens_rate_article, Slrn_Article_Keymap);
#endif
   
   SLkm_define_key  ("\033l", (FVOID_STAR) locate_header_by_msgid, Slrn_Article_Keymap);
   SLkm_define_key ("\0331", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0332", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0333", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0334", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0335", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0336", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0337", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0338", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0339", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key ("\0330", (FVOID_STAR) slrn_digit_arg, Slrn_Article_Keymap);
   SLkm_define_key  ("*", (FVOID_STAR) toggle_header_tag, Slrn_Article_Keymap);
   SLkm_define_key  ("#", (FVOID_STAR) num_tag_header, Slrn_Article_Keymap);
   SLkm_define_key  ("\033#", (FVOID_STAR) num_untag_headers, Slrn_Article_Keymap);
#if SLRN_HAS_DECODE
   SLkm_define_key  (":", (FVOID_STAR) decode_article, Slrn_Article_Keymap);
#endif
   SLkm_define_key  (" ", (FVOID_STAR) art_pagedn, Slrn_Article_Keymap);
   SLkm_define_key  ("!", (FVOID_STAR) next_high_score, Slrn_Article_Keymap);
   SLkm_define_key  (",", (FVOID_STAR) exchange_mark, Slrn_Article_Keymap);
   SLkm_define_key  (".", (FVOID_STAR) slrn_repeat_last_key, Slrn_Article_Keymap);
   SLkm_define_key  ("/", (FVOID_STAR) article_search, Slrn_Article_Keymap);
   SLkm_define_key  (";", (FVOID_STAR) mark_spot, Slrn_Article_Keymap);
   SLkm_define_key  ("<", (FVOID_STAR) art_bob, Slrn_Article_Keymap);
   SLkm_define_key  ("=", (FVOID_STAR) next_header_same_subject, Slrn_Article_Keymap);
   SLkm_define_key  (">", (FVOID_STAR) art_eob, Slrn_Article_Keymap);
   SLkm_define_key  ("?", (FVOID_STAR) slrn_article_help, Slrn_Article_Keymap);
   SLkm_define_key  ("A", (FVOID_STAR) author_search_backward, Slrn_Article_Keymap);
   SLkm_define_key  ("F", (FVOID_STAR) forward_article, Slrn_Article_Keymap);
   SLkm_define_key  ("H", (FVOID_STAR) hide_article, Slrn_Article_Keymap);
   SLkm_define_key  ("K", (FVOID_STAR) create_score, Slrn_Article_Keymap);
   SLkm_define_key  ("L", (FVOID_STAR) goto_last_read, Slrn_Article_Keymap);
   SLkm_define_key  ("N", (FVOID_STAR) skip_to_next_group, Slrn_Article_Keymap);
   SLkm_define_key  ("P", (FVOID_STAR) slrn_post_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("S", (FVOID_STAR) subject_search_backward, Slrn_Article_Keymap);
   SLkm_define_key  ("T", (FVOID_STAR) toggle_quotes, Slrn_Article_Keymap);
   SLkm_define_key  ("U", (FVOID_STAR) browse_url, Slrn_Article_Keymap);
   SLkm_define_key  ("W", (FVOID_STAR) toggle_wrap_article, Slrn_Article_Keymap);
   SLkm_define_key  ("\033^C", (FVOID_STAR) cancel_article, Slrn_Article_Keymap);
   SLkm_define_key  ("\033^P", (FVOID_STAR) get_children_headers, Slrn_Article_Keymap);
   SLkm_define_key  ("\033a", (FVOID_STAR) toggle_header_formats, Slrn_Article_Keymap);
   SLkm_define_key  ("\033d", (FVOID_STAR) thread_delete_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("\033p", (FVOID_STAR) get_parent_header, Slrn_Article_Keymap);
   SLkm_define_key  ("\033t", (FVOID_STAR) toggle_collapse_threads, Slrn_Article_Keymap);
   SLkm_define_key  ("\r", (FVOID_STAR) art_pagedn, Slrn_Article_Keymap);
   SLkm_define_key  ("\t", (FVOID_STAR) skip_quoted_text, Slrn_Article_Keymap);
   SLkm_define_key  ("^L", (FVOID_STAR) slrn_redraw, Slrn_Article_Keymap);
   SLkm_define_key  ("^M", (FVOID_STAR) art_linedn, Slrn_Article_Keymap);
   SLkm_define_key  ("^P", (FVOID_STAR) header_up, Slrn_Article_Keymap);
   SLkm_define_key  ("^R", (FVOID_STAR) slrn_redraw, Slrn_Article_Keymap);
   SLkm_define_key  ("^Z", (FVOID_STAR) art_suspend_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("a", (FVOID_STAR) author_search_forward, Slrn_Article_Keymap);
   SLkm_define_key  ("b", (FVOID_STAR) art_pageup, Slrn_Article_Keymap);
   SLkm_define_key  ("d", (FVOID_STAR) delete_header_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("f", (FVOID_STAR) followup, Slrn_Article_Keymap);
   SLkm_define_key  ("g", (FVOID_STAR) skip_digest_forward, Slrn_Article_Keymap);
   SLkm_define_key  ("j", (FVOID_STAR) goto_article, Slrn_Article_Keymap);
   SLkm_define_key  ("n", (FVOID_STAR) art_next_unread, Slrn_Article_Keymap);
   SLkm_define_key  ("o", (FVOID_STAR) save_article, Slrn_Article_Keymap);
   SLkm_define_key  ("p", (FVOID_STAR) art_prev_unread, Slrn_Article_Keymap);
   SLkm_define_key  ("q", (FVOID_STAR) art_quit, Slrn_Article_Keymap);
   SLkm_define_key  ("r", (FVOID_STAR) reply_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("s", (FVOID_STAR) subject_search_forward, Slrn_Article_Keymap);
   SLkm_define_key  ("t", (FVOID_STAR) toggle_headers, Slrn_Article_Keymap);
   SLkm_define_key  ("u", (FVOID_STAR) undelete_header_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("x", (FVOID_STAR) art_xpunge, Slrn_Article_Keymap);
   SLkm_define_key  ("y", (FVOID_STAR) print_article_cmd, Slrn_Article_Keymap);
   SLkm_define_key  ("|", (FVOID_STAR) pipe_article, Slrn_Article_Keymap);
#if defined(IBMPC_SYSTEM)
   SLkm_define_key  ("^@S", (FVOID_STAR) art_pageup, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0S", (FVOID_STAR) art_pageup, Slrn_Article_Keymap);
#else
   SLkm_define_key  ("^?", (FVOID_STAR) art_pageup, Slrn_Article_Keymap);
#endif
#if defined(IBMPC_SYSTEM)
   SLkm_define_key  ("\033^@H", (FVOID_STAR) art_lineup, Slrn_Article_Keymap);
   SLkm_define_key  ("\033\xE0H", (FVOID_STAR) art_lineup, Slrn_Article_Keymap);
   SLkm_define_key  ("\033^@P", (FVOID_STAR) art_linedn, Slrn_Article_Keymap);
   SLkm_define_key  ("\033\xE0P", (FVOID_STAR) art_linedn, Slrn_Article_Keymap);
   SLkm_define_key  ("^@H", (FVOID_STAR) header_up, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0H", (FVOID_STAR) header_up, Slrn_Article_Keymap);
   SLkm_define_key  ("^@P", (FVOID_STAR) header_down, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0P", (FVOID_STAR) header_down, Slrn_Article_Keymap);
   SLkm_define_key  ("^@M", (FVOID_STAR) art_right, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0M", (FVOID_STAR) art_right, Slrn_Article_Keymap);
   SLkm_define_key  ("^@K", (FVOID_STAR) art_left, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0K", (FVOID_STAR) art_left, Slrn_Article_Keymap);
#else
   SLkm_define_key  ("\033\033[A", (FVOID_STAR) art_lineup, Slrn_Article_Keymap);
   SLkm_define_key  ("\033\033OA", (FVOID_STAR) art_lineup, Slrn_Article_Keymap);
   SLkm_define_key  ("\033\033[B", (FVOID_STAR) art_linedn, Slrn_Article_Keymap);
   SLkm_define_key  ("\033\033OB", (FVOID_STAR) art_linedn, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[A", (FVOID_STAR) header_up, Slrn_Article_Keymap);
   SLkm_define_key  ("\033OA", (FVOID_STAR) header_up, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[B", (FVOID_STAR) header_down, Slrn_Article_Keymap);
   SLkm_define_key  ("\033OB", (FVOID_STAR) header_down, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[C", (FVOID_STAR) art_right, Slrn_Article_Keymap);
   SLkm_define_key  ("\033OC", (FVOID_STAR) art_right, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[D", (FVOID_STAR) art_left, Slrn_Article_Keymap);
   SLkm_define_key  ("\033OD", (FVOID_STAR) art_left, Slrn_Article_Keymap);
#endif
   SLkm_define_key  ("\033S", (FVOID_STAR) toggle_sort, Slrn_Article_Keymap);
   SLkm_define_key  ("^U", (FVOID_STAR) header_pageup, Slrn_Article_Keymap);
   SLkm_define_key  ("\033V", (FVOID_STAR) header_pageup, Slrn_Article_Keymap);
#if defined(IBMPC_SYSTEM)
   SLkm_define_key  ("^@I", (FVOID_STAR) header_pageup, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0I", (FVOID_STAR) header_pageup, Slrn_Article_Keymap);
   SLkm_define_key  ("^@Q", (FVOID_STAR) header_pagedn, Slrn_Article_Keymap);
   SLkm_define_key  ("\xE0Q", (FVOID_STAR) header_pagedn, Slrn_Article_Keymap);
#else
   SLkm_define_key  ("\033[5~", (FVOID_STAR) header_pageup, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[6~", (FVOID_STAR) header_pagedn, Slrn_Article_Keymap);
#endif
   SLkm_define_key  ("^D", (FVOID_STAR) header_pagedn, Slrn_Article_Keymap);
   SLkm_define_key  ("^V", (FVOID_STAR) header_pagedn, Slrn_Article_Keymap);
   SLkm_define_key  ("\033>", (FVOID_STAR) header_eob, Slrn_Article_Keymap);
   SLkm_define_key  ("\033<", (FVOID_STAR) header_bob, Slrn_Article_Keymap);
   SLkm_define_key  ("c", (FVOID_STAR) catch_up_all, Slrn_Article_Keymap);
   SLkm_define_key  ("\033c", (FVOID_STAR) catch_up_all, Slrn_Article_Keymap);
   SLkm_define_key  ("\033u", (FVOID_STAR) un_catch_up_all, Slrn_Article_Keymap);
   SLkm_define_key  ("\033C", (FVOID_STAR) catch_up_to_here, Slrn_Article_Keymap);
   SLkm_define_key  ("C", (FVOID_STAR) catch_up_to_here, Slrn_Article_Keymap);
   SLkm_define_key  ("\033U", (FVOID_STAR) un_catch_up_to_here, Slrn_Article_Keymap);
   SLkm_define_key  ("\033R", (FVOID_STAR) toggle_rot13, Slrn_Article_Keymap);
#if 1
#if SLRN_HAS_SPOILERS
   SLkm_define_key  ("\033?", (FVOID_STAR) show_spoilers, Slrn_Article_Keymap);
#endif
#endif
   SLkm_define_key  ("^N", (FVOID_STAR) header_down, Slrn_Article_Keymap);
   SLkm_define_key  ("^", (FVOID_STAR) enlarge_window, Slrn_Article_Keymap);
   SLkm_define_key  ("^^", (FVOID_STAR) shrink_window, Slrn_Article_Keymap);
   SLkm_define_key  ("\033P", (FVOID_STAR) slrn_post_postponed, Slrn_Article_Keymap);
   
   /* mouse (left/middle/right) */
   SLkm_define_key  ("\033[M\040", (FVOID_STAR) art_mouse_left, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[M\041", (FVOID_STAR) art_mouse_middle, Slrn_Article_Keymap);
   SLkm_define_key  ("\033[M\042", (FVOID_STAR) art_mouse_right, Slrn_Article_Keymap);

   SLkm_define_key  ("z", (FVOID_STAR) zoom_article_window, Slrn_Article_Keymap);
   
   if (SLang_Error) slrn_exit_error (err);
}

/*}}}*/


/*}}}*/
/*{{{ slrn_article_mode and support functions */


static void slrn_art_hangup (int sig) /*{{{*/
{
   (void) sig;
   if (Slrn_Current_Header != NULL)
     undelete_header_cmd ();		       /* in case we are reading one */
   art_quit ();
}

/*}}}*/

static void mark_ranges_read (Slrn_Range_Type *r) /*{{{*/
{
   Slrn_Header_Type *h = Slrn_First_Header;
   int min, max;
   
   while ((r != NULL) && (h != NULL))
     {
	min = r->min;
	max = r->max;
	while (h != NULL)
	  {
	     if (h->number < min)
	       {
		  h = h->real_next;
		  continue;
	       }
	     
	     if (h->number > max)
	       {
		  break;
	       }
	     
	     h->flags |= HEADER_READ;
	     h = h->real_next;
	  }
	r = r->next;
     }
}

/*}}}*/

static void init_graphic_chars (void)
{
#ifndef IBMPC_SYSTEM
   if (SLtt_Has_Alt_Charset == 0)
     Slrn_Simulate_Graphic_Chars = 1;
#endif

   if (Slrn_Simulate_Graphic_Chars)
     {
	Graphic_LTee_Char = '+';
	Graphic_UTee_Char = '+';
	Graphic_LLCorn_Char = '\\';
	Graphic_HLine_Char = '-';
	Graphic_VLine_Char = '|';
	Graphic_ULCorn_Char = '/';
	
	Graphic_Chars_Mode = SIMULATED_CHAR_SET_MODE;
	return;
     }
   
   Graphic_Chars_Mode = ALT_CHAR_SET_MODE;
   Graphic_LTee_Char = SLSMG_LTEE_CHAR;
   Graphic_UTee_Char = SLSMG_UTEE_CHAR;
   Graphic_LLCorn_Char = SLSMG_LLCORN_CHAR;
   Graphic_HLine_Char = SLSMG_HLINE_CHAR;
   Graphic_VLine_Char = SLSMG_VLINE_CHAR;
   Graphic_ULCorn_Char = SLSMG_ULCORN_CHAR;

   Graphic_Chars_Mode = ALT_CHAR_SET_MODE;
}

/* If all > 0, get last 'all' headers from server independent of whether
 *             they have been read or not.
 * If all < 0, and this is not the first time this group has been accessed,
 *             either during this session or during previous sessions, get
 *             that last 'all' UNREAD articles.
 * Otherwise,  fetch ALL UNREAD headers from the server.
 */
int slrn_select_article_mode (Slrn_Group_Type *g, int all, int score) /*{{{*/
{
   int min, max;
   int smin, smax;
   Slrn_Range_Type *r;
   int status;

   init_graphic_chars ();

   Header_Window_HScroll = 0;
   User_Aborted_Group_Read = 0;
   Headers = Slrn_First_Header = NULL;
   Threads_Collapsed = 0;
   Same_Subject_Start_Header = NULL;
   Number_Killed = Number_High_Scored = Number_Low_Scored = 0;
   
   Current_Group = g;
   r = &g->range;
   Slrn_Current_Group_Name = g->name;
   
   if (Slrn_Server_Obj->sv_reset_has_xover)
     Slrn_Server_Obj->sv_has_xover = 1;
   
   Slrn_Score_After_XOver = Slrn_Server_Obj->sv_has_xover;

#if SLRN_HAS_SLANG
   SLang_run_hooks ("pre_article_mode_hook", 0);
   if (SLang_Error)
     return -1;
#endif
   
   if (score && (1 == slrn_open_score (Slrn_Current_Group_Name)))
     Perform_Scoring = 1;
   else Perform_Scoring = 0;
   
   Slrn_Server_Min = r->min;
   Slrn_Server_Max = r->max;
   r = r->next;
   /* Now r points to ranges already read.  */
   
   status = 0;
   
   if (all > 0)
     {
	min = Slrn_Server_Max - all + 1;
	if (min < Slrn_Server_Min) min = Slrn_Server_Min;
	status = get_headers (min, Slrn_Server_Max, &all);
	if (status != -1)
	  mark_ranges_read (r);
     }
   else
     {
	if ((all < 0) && (r != NULL))
	  {
	     int unread;

	     /* This condition will occur when the user wants to read unread
	      * articles that occur in a gap, i.e., RRRUUUUURRRUUUUUUU and
	      * we need to dig back far enough below the last group of read
	      * ones until we have retrieved abs(all) articles.
	      * 
	      * The problem with this is that some articles may not be 
	      * available on the server which means that the number to
	      * go back will be under estimated.
	      */
	     all = -all;
	     
	     while (r->next != NULL) r = r->next;
	     /* Go back through previously read articles counting unread.
	      * If number unread becomes greater than the number that we
	      * intend to read, then we know where to start querying 
	      * the server.
	      */
	     unread = 0;
	     max = Slrn_Server_Max;
	     while (r->prev != NULL)
	       {
		  unread += max - r->max;
		  if (unread >= all) break;
		  max = r->min - 1;
		  r = r->prev;
	       }
	     
	     if (unread >= all)
	       {
		  /* This may be problematic if some articles are missing on 
		   * the server.  If that is the case, smin will be to high
		   * and we will fall short of the goal.
		   */
		  smin = r->max + (unread - all) + 1;
	       }
	     else smin = Slrn_Server_Min;
	     smax = Slrn_Server_Max;
	     r = r->next;
	  }
	else
	  {
	     /* all == 0, or no previously read articles. */
	     smin = Slrn_Server_Min;
	     smax = Slrn_Server_Max;
	     if (r != NULL)
	       {
		  Slrn_Range_Type *r1;
		  
		  /* Estimate how many are available to read */
		  all = smax - r->max;
#if 0				       /* is this correct?? */
		  all++;
#endif
		  
		  /* Now subtract the ones that we have already read. */
		  r1 = r->next;
		  while (r1 != NULL)
		    {
		       all -= (r1->max - r1->min) + 1;
		       r1 = r1->next;
		    }
		  /* This condition should never arise */
		  if (all == 0) all = smax - smin + 1;
	       }
	     else all = smax - smin + 1;
	  }
	
	while (r != NULL)
	  {
	     if (r->min > smin)
	       {
		  min = smin;
		  max = r->min - 1;

		  status = get_headers (min, max, &all);

		  if (status == -1)
		    break;
		  
		  if (status == 0)
		    {
		       Slrn_Groups_Dirty = 1;
		       r->min = min;
		    }
		  
		  smin = r->max + 1;
	       }
	     else
	       {
		  smin = r->max + 1;
	       }
	     r = r->next;
	  }
	
	if (smin <= smax)
	  {
	     status = get_headers (smin, smax, &all);
	  }
     }
   
   if ((status == -1) || SLKeyBoard_Quit)
     {
	if (SLang_Error == USER_BREAK)
	  slrn_error_now (0, "Group transfer aborted.");
	else
	  slrn_error_now (0, "Server read failed.");
	
	/* This means that we cannot update ranges for this group because 
	 * the user aborted and update_ranges assumes that all articles 
	 * upto server max are present.
	 */
	User_Aborted_Group_Read = 1;
	art_quit ();
     }
   else if (Slrn_Score_After_XOver 
       && Perform_Scoring)
     score_headers ();
   
   if (Headers == NULL)
     {
	slrn_close_score ();
	if (Number_Killed)
	  slrn_error ("No unread articles found. (%d killed)", Number_Killed);
	else
	  slrn_error ("No unread articles found.");

	free_kill_lists_and_update ();
	Slrn_Current_Group_Name = NULL;
	if (SLang_Error == USER_BREAK) return -1;
	else return -2;
     }
   
   make_hash_table ();
   
   /* This must go here to fix up the next/prev pointers */
   sort_by_server_number ();

   extract_real_names ();

   slrn_chmap_fix_headers ();

   slrn_push_mode (&Art_Mode_Cap);
   
   Slrn_Current_Header = Headers;
   Last_Cursor_Row = 0;
   Mark_Header = NULL;
   init_header_window_struct ();
   
   Article_Current_Line = Slrn_Article_Lines = NULL;
   At_End_Of_Article = NULL;
   Header_Showing = NULL;
   SLMEMSET ((char *) &Slrn_Article_Window, 0, sizeof (SLscroll_Window_Type));
   
   set_article_visibility (0);

#if SLRN_HAS_GROUPLENS
   /* slrn_set_suspension (1); */
   Num_GroupLens_Rated = slrn_get_grouplens_scores ();
   /* slrn_set_suspension (0); */
#endif

#if SLRN_HAS_SLANG
   (void) SLang_run_hooks ("article_mode_hook", 0);
#endif   
   sort_by_sorting_mode ();
   header_bob ();

   quick_help ();
   
   if (Slrn_Startup_With_Article) art_pagedn ();
   
   if (SLang_Error == 0)
     {
	if (Perform_Scoring 
	    /* && (Number_Killed || Number_High_Scored) */
	    )
	  {
#if SLRN_HAS_GROUPLENS
	     if (Num_GroupLens_Rated != -1)
	       {
		  slrn_message ("Num Killed: %u, Num High: %u, Num Low: %u, Num GroupLens Rated: %d",
				Number_Killed, Number_High_Scored, Number_Low_Scored,
				Num_GroupLens_Rated);
	       }
	     else
#endif
	       slrn_message ("Num Killed: %u, Num High: %u, Num Low: %u",
			     Number_Killed, Number_High_Scored, Number_Low_Scored);
	  }
	
	else slrn_clear_message ();
     }
   
   Number_Low_Scored = Number_Killed = Number_High_Scored = 0;
   return 0;
}

/*}}}*/

/*}}}*/

/*{{{ screen update functions */

static char *Header_Display_Formats [SLRN_MAX_HEADER_FORMATS];
static unsigned int Header_Format_Number;

int slrn_set_header_format (unsigned int num, char *fmt)
{
   if (num >= SLRN_MAX_HEADER_FORMATS)
     return -1;
   
   if (Header_Display_Formats[num] != NULL)
     SLang_free_slstring (Header_Display_Formats[num]);

   if ((fmt == NULL) || (*fmt == 0))
     {
	Header_Display_Formats[num] = NULL;
	return 0;
     }

   if (NULL == (Header_Display_Formats[num] = SLang_create_slstring (fmt)))
     return -1;
   
   return 0;
}
     
static void toggle_header_formats (void)
{
   if (Slrn_Prefix_Arg_Ptr != NULL)
     {
	Header_Format_Number = (unsigned int) *Slrn_Prefix_Arg_Ptr;
	Slrn_Prefix_Arg_Ptr = NULL;
     }
   else Header_Format_Number++;

   Header_Format_Number = Header_Format_Number % SLRN_MAX_HEADER_FORMATS;
   
   if (Header_Display_Formats[Header_Format_Number] == NULL)
     Header_Format_Number = 0;

   Slrn_Full_Screen_Update = 1;
}



/* Wrappers around SLsmg routines. */
static void smg_write_nchars (char *s, unsigned int n)
{
   unsigned char *s1, *smax;
   unsigned int eight_bit;

   s1 = (unsigned char *) s;
   smax = s1 + n;
   eight_bit = SLsmg_Display_Eight_Bit;
   
   while (s1 < smax)
     {
	if ((*s1 & 0x80) && (eight_bit > (unsigned int) *s1))
	  {
	     if (s != (char *) s1)
	       SLsmg_write_nchars (s, (unsigned int) ((char *)s1 - s));
	     SLsmg_write_char ('?');
	     s1++;
	     s = (char *) s1;
	  }
	else s1++;
     }
   
   if (s != (char *)s1)
     SLsmg_write_nchars (s, (unsigned int) ((char *)s1 - s));
}

static void smg_write_string (char *s)
{
   smg_write_nchars (s, strlen (s));
}

static void smg_write_char (char c)
{
   smg_write_nchars (&c, 1);
}

static void quick_help (void) /*{{{*/
{
   char *msg;
   
   if (Slrn_Batch) return;

   if (Article_Visible == 0)
     {
	msg = "SPC:Select  Ctrl-D:PgDn  Ctrl-U:PgUp  d:Mark-as-Read  n:Next  p:Prev  q:Quit";
	if (Slrn_Header_Help_Line != NULL) msg = Slrn_Header_Help_Line;
     }
   else
     {
	msg = "SPC:Pgdn  B:PgUp  u:Un-Mark-as-Read  f:Followup  n:Next  p:Prev  q:Quit";
	if (Slrn_Art_Help_Line != NULL) msg = Slrn_Art_Help_Line;
     }

   if (0 == slrn_message (msg))
     Slrn_Message_Present = 0;
}

/*}}}*/

static void write_rot13 (unsigned char *buf) /*{{{*/
{
   static int init_rot13;
   static char rot13buf[256];
   unsigned char ch;
   int i;
   
   if (init_rot13 == 0)
     {
	init_rot13 = 1;
	for (i = 0; i < 256; i++)
	  {
	     rot13buf[i] = i;
	  }
	
	for (i = 'A'; i <= 'M'; i++)
	  {
	     rot13buf[i] = i + 13;
	     /* Now take care of lower case ones */
	     rot13buf[i + 32] =  i + 32 + 13;
	  }
	
	for (i = 'N'; i <= 'Z'; i++)
	  {
	     rot13buf[i] = i - 13;
	     /* Now take care of lower case ones */
	     rot13buf[i + 32] =  i + 32 - 13;
	  }
     }
   
   while ((ch = *buf++) != 0)
     {
	ch = rot13buf[ch];
	smg_write_nchars ((char *) &ch, 1);
     }
}

/*}}}*/

/*{{{ utility routines */

#if SLRN_HAS_SPOILERS
/* write out the line, replacing all printable chars with '*' */
static void write_spoiler (unsigned char *buf) /*{{{*/
{
   unsigned char ch;
   
   Spoilers_Visible = Slrn_Current_Header;
   
   if (Slrn_Spoiler_Char == ' ')
     return;
   
   while ((ch = *buf++) != 0)
     {
	if (!isspace(ch)) ch = Slrn_Spoiler_Char;
	smg_write_nchars ((char *) &ch, 1);
     }
}
/*}}}*/
#endif

static void draw_tree (Slrn_Header_Type *h) /*{{{*/
{
   unsigned char buf[2];

#if !defined(IBMPC_SYSTEM)
   if (Graphic_Chars_Mode == 0)
     {
	smg_write_string ((char *) h->tree);
	smg_write_string ("  ");
	return;
     }
   if (Graphic_Chars_Mode == ALT_CHAR_SET_MODE)
     SLsmg_set_char_set (1);
#endif
   
   slrn_set_color (TREE_COLOR);
   
   if (*h->tree) smg_write_string ((char *) h->tree);
   
   if (h->flags & FAKE_CHILDREN)
     {
	buf[0] = Graphic_UTee_Char;
	buf[1] = Graphic_HLine_Char;
	SLsmg_forward (-1);
	smg_write_char (Graphic_ULCorn_Char);
     }
   else if ((h->sister == NULL) ||
	    ((h->sister->flags & FAKE_PARENT) && ((h->flags & FAKE_PARENT) == 0)))
     {
	buf[0] = Graphic_LLCorn_Char;
	buf[1] = Graphic_HLine_Char;
     }
   else
     {
	buf[0] = Graphic_LTee_Char;
	buf[1] = Graphic_HLine_Char;
     }
   smg_write_nchars ((char *) buf, 2);

#if !defined(IBMPC_SYSTEM)
   if (Graphic_Chars_Mode == ALT_CHAR_SET_MODE) SLsmg_set_char_set (0);
#endif
}

/*}}}*/

/*{{{ check_subject */

/*
 * This checks if subjects should be printed (correctly I hope)
 * hacked: articles in a tree shouldn't display their subject if the
 *          subject is already displayed (i.e. at top)
 * To add: take more Re:'s into account (currently, one is allowed)
 */
int Slrn_Show_Thread_Subject = 0;
static int check_subject (Slrn_Header_Type *h) /*{{{*/
{
   char *psubj, *subj;
   
   subj = h->subject;
   psubj = h->prev->subject;	       /* used to be: h->parent->subject */
   if ((subj == NULL) || (psubj == NULL)) return 1;

   return subject_cmp ((unsigned char *)subj, (unsigned char *)psubj);
}

/*}}}*/

/*}}}*/

#if SLRN_HAS_END_OF_THREAD
static int display_end_of_thread (Slrn_Header_Type *h) /*{{{*/
{
   Slrn_Header_Type *parent, *next_parent;
   
   if ((h == NULL)
       || (h->parent == NULL)
       || (h->child != NULL)
       || (h->next == NULL))
     return -1;
   
   parent = h->parent;
   while (parent->parent != NULL) parent = parent->parent;
	     
   next_parent = h->next;
   while (next_parent->parent != NULL) 
     next_parent = next_parent->parent;
   
   if (next_parent != parent)
     {
	if ((Header_Window_Nrows == 0)
	    && (next_parent != NULL)
	    && (next_parent->subject != NULL))
	  slrn_message ("End of Thread.  Next: %s", next_parent->subject);
	else
	  slrn_message ("End of Thread.");
	return 0;
     }
   
   if ((h->sister == NULL) 
       || (h->parent != h->next->parent)
       || (FAKE_PARENT & (h->next->flags ^ h->flags)))
     {
	/* The last test involving ^ is necessary because the two can be
	 * sisters except that one can have a fake parent.  If this is the
	 * case, we are at the end of a subthread.
	 */
	slrn_message ("End of Sub-Thread");
	return 0;
     }
   
   return -1;
}
/*}}}*/
#endif


static void disp_write_flags (Slrn_Header_Type *h, int row)
{
   unsigned int flags = h->flags;

   /* Do not write header numbers if we are displaying at bottom of the
    * screen in the display area.  When this happens, the header window is
    * not visible.
    */
   if (row + 1 != SLtt_Screen_Rows)
     {
	if (Slrn_Use_Header_Numbers)
	  {
	     slrn_set_color (HEADER_NUMBER_COLOR);
	     SLsmg_printf ("%2d", row);
	     if (row > Largest_Header_Number) Largest_Header_Number = row;
	  }
	else smg_write_string ("  ");
     }
   

   if (flags & HEADER_NTAGGED)
     {
	slrn_set_color (HIGH_SCORE_COLOR);
	SLsmg_printf ("%2d",
		      h->tag_number);
     }
   else
     {
	if ((flags & HEADER_HIGH_SCORE)
	    || ((flags & FAKE_HEADER_HIGH_SCORE)
		&& (h->child != NULL)
		&& (h->child->flags & HEADER_HIDDEN)))
	  {
	     slrn_set_color (HIGH_SCORE_COLOR);
	     SLsmg_printf ("!%c",
			   ((flags & HEADER_TAGGED) ? '*': ' '));
	  }
	else
	  {
	     slrn_set_color (0);
	     SLsmg_printf (" %c",
			   ((flags & HEADER_TAGGED) ? '*': ' '));
	  }
     }
   slrn_set_color (0);
   SLsmg_write_char ((flags & HEADER_READ) ? 'D': '-');
}
   
#if SLRN_HAS_GROUPLENS
# define SLRN_GROUPLENS_DISPLAY_WIDTH 5
static void disp_write_grplens (Slrn_Header_Type *h)
{
   char buf [SLRN_GROUPLENS_DISPLAY_WIDTH], *b, *bmax;

   if (Num_GroupLens_Rated == -1)
     return;

   b = buf;
   bmax = b + SLRN_GROUPLENS_DISPLAY_WIDTH;   
   while (b < bmax) *b++ = ' ';
     {
	int pred = h->gl_pred;

	if (pred < 0)
	  buf [SLRN_GROUPLENS_DISPLAY_WIDTH / 2] = '?';
	else
	  {
	     b = buf;
	     while ((pred > 0) && (b < bmax))
	       {
		  pred--;
		  *b++ = '*';
	       }
	  }
     }
   slrn_set_color (GROUPLENS_DISPLAY_COLOR);
   smg_write_nchars (buf, SLRN_GROUPLENS_DISPLAY_WIDTH);
   slrn_set_color (0);
}
#endif

/*}}}*/

/*}}}*/

static char *disp_get_header_subject (Slrn_Header_Type *h, int row)
{
   if ((Slrn_Show_Thread_Subject)
       /* || (0 == h->num_children) */
       || (h->parent == NULL)
       || (row == 1)
       || (row + 1 == SLtt_Screen_Rows)/* at bottom of screen */
       || check_subject (h))
     return h->subject;

   return ">";
}

static void disp_draw_thread_tree (Slrn_Header_Type *h)
{
   if (0 == (Slrn_Sorting_Mode & SORT_BY_THREADS))
     return;

   if ((h->next != NULL)
       && (h->next->flags & HEADER_HIDDEN))
     {
	slrn_set_color (THREAD_NUM_COLOR);
	SLsmg_printf (" %2d ",
		      1 + h->num_children);
     }
   else
     {
	smg_write_string ("    ");
	if ((h->parent != NULL) || (h->flags & FAKE_CHILDREN))
	  draw_tree (h);
     }
}

static int display_header_line (Slrn_Header_Type *h, int row)
{
   char *fmt;
   char ch;

   SLsmg_gotorc (row, 0);
   slrn_set_color (0);

   fmt = Header_Display_Formats[Header_Format_Number];

   if (fmt == NULL)
     fmt = "%F%-5S%G%-5l:[%12r]%t%s";

   while ((ch = *fmt) != 0)
     {
	char *s;
	int len;
	int color;
	int field_len;
	char buf[32];
	int right_justify;
	int spaces = 0;

	fmt++;
	if (ch != '%')
	  {
	     SLsmg_write_nchars (&ch, 1);
	     continue;
	  }

	ch = *fmt++;

	s = NULL;
	len = -1;
	color = 0;

	field_len = -1;
	if (ch == '-')
	  {
	     right_justify = 1;
	     ch = *fmt++;
	  }
	else
	  right_justify = 0;

	if (isdigit (ch))
	  {
	     field_len = 0;
	     do
	       {
		  field_len = 10 * field_len + (int) (ch - '0');
		  ch = *fmt++;
	       }
	     while (isdigit (ch));
	  }

	switch (ch)
	  {
	   case 0:
	     slrn_set_color (0);
	     SLsmg_erase_eol ();
	     return 0;
	       
	   default:
	     break;

	   case 'F':
	     disp_write_flags (h, row);
	     break;
	     
	     /* score */	     
#if SLRN_HAS_SORT_BY_SCORE
	   case 'S':
	     if (Slrn_Display_Score)
	       {
		  int score;
		  
		  score = ((h->child != NULL) && (h->child->flags & HEADER_HIDDEN)
			   ? h->thread_score : h->score);
		  
		  s = buf;
		  if (score)
		    sprintf (s, "%d", score);
		  else *s = 0;
	       }
	     break;
#endif
	   case 'l':
	     s = buf;
	     sprintf (s, "%d", h->lines);
	     break;

	   case '%':
	     s = "%";
	     len = 1;
	     break;

	   case 't':
	     disp_draw_thread_tree (h);
	     break;

	   case 'f':
	     color = AUTHOR_COLOR;
	     s = h->from;
	     if (s == NULL) s = "";
	     break;
	   case 'r':
	     color = AUTHOR_COLOR;
	     s = h->realname;
	     len = h->realname_len;
	     if (s == NULL) s = "";
	     break;
	   case 's':
	     color = SUBJECT_COLOR;
	     s = disp_get_header_subject (h, row);
	     break;
#if SLRN_HAS_GROUPLENS
	   case 'G':
	     disp_write_grplens (h);
	     break;
#endif
	   case 'd':
	     if (NULL == (s = h->date))
	       s = "";
	     break;
	     
	   case 'n':
	     s = buf;
	     sprintf (s, "%d", h->number);
	     break;

           case 'g':
             SLsmg_erase_eol ();
             if (right_justify)
               field_len = SLtt_Screen_Cols - field_len;
             SLsmg_gotorc (row, field_len);
             break;
	  }

	slrn_set_color (color);
	if (s == NULL)
	  continue;

	if (len == -1) len = strlen (s);
	if (field_len != -1)
	  {
	     if (field_len > len)
	       spaces = field_len - len;
	     else 
	       len = field_len;
	  }
	if (right_justify)
	  {
	     while (spaces)
	       {
		  SLsmg_write_nchars (" ", 1);
		  spaces--;
	       }
	  }
	SLsmg_write_nchars (s, len);
	while (spaces)
	  {
	     spaces--;
	     SLsmg_write_nchars (" ", 1);
	  }
	if (color) slrn_set_color (0);
     }

   slrn_set_color (0);
   SLsmg_erase_eol ();
   return 0;
}

static void display_article_line (Slrn_Article_Line_Type *l)
{
   char *lbuf = l->buf;
   
   if (l->flags & HEADER_LINE)
     {
	if ((unsigned char)*lbuf > (unsigned char)' ')
	  {
	     lbuf = slrn_strchr (lbuf, ':');
	     if (lbuf != NULL)
	       {
		  lbuf++;
		  slrn_set_color (SLRN_HEADER_KEYWORD_COLOR);
		  smg_write_nchars (l->buf, lbuf - l->buf);
	       }
	     else lbuf = l->buf;
	  }
	slrn_set_color (HEADER_COLOR);
	smg_write_string (lbuf);
     }
   else
     {
	if (l->flags & QUOTE_LINE)
	  slrn_set_color (QUOTE_COLOR);
	else if (l->flags & SIGNATURE_LINE)
	  slrn_set_color (SIGNATURE_COLOR);
	else slrn_set_color (ARTICLE_COLOR);
#if SLRN_HAS_SPOILERS
	if (l->flags & SPOILER_LINE)
	  write_spoiler ((unsigned char *) lbuf);
	else
#endif
	  if (Do_Rot13) write_rot13 ((unsigned char *) lbuf);
	else smg_write_string (lbuf);
     }
}


static void update_header_window (void)
{
   Slrn_Header_Type *h;
   int height;
   int row;
   int last_cursor_row;
   int c0;

   height = Slrn_Header_Window.nrows;

   h = (Slrn_Header_Type *) Slrn_Header_Window.top_window_line;
   SLscroll_find_top (&Slrn_Header_Window);
   if (h != (Slrn_Header_Type *) Slrn_Header_Window.top_window_line)
     {
	Slrn_Full_Screen_Update = 1;
	h = (Slrn_Header_Type *) Slrn_Header_Window.top_window_line;
     }
   
   last_cursor_row = Last_Cursor_Row;

   SLsmg_gotorc (height + 1, 0);
   slrn_set_color (STATUS_COLOR);
   SLsmg_printf ("News Group: %s", Slrn_Current_Group_Name);
   slrn_print_percent (height + 1, 60, 
		       &Slrn_Header_Window);

   c0 = Header_Window_HScroll;
   SLsmg_set_screen_start (NULL, &c0);

   for (row = 1; row <= height; row++)
     {
	while ((h != NULL) && (h->flags & HEADER_HIDDEN))
	  h = h->next;
	
	if (Slrn_Full_Screen_Update
	    || (row == last_cursor_row))
	  {
	     if (h == NULL)
	       {
		  SLsmg_gotorc (row, 0);
		  slrn_set_color (0);
		  SLsmg_erase_eol ();
		  continue;
	       }

	     display_header_line (h, row);
	  }
	h = h->next;
     }
   SLsmg_set_screen_start (NULL, NULL);
}

static void update_article_window (void)
{
   Slrn_Article_Line_Type *l;
 
   if (Article_Visible == 0)
     return;

   l = (Slrn_Article_Line_Type *) Slrn_Article_Window.top_window_line;

   SLscroll_find_top (&Slrn_Article_Window);

   if (Slrn_Full_Screen_Update 
       || (l != Article_Current_Line)
       || (l != (Slrn_Article_Line_Type *) Slrn_Article_Window.top_window_line))
     {
	int row;
	int c0;
	int height;

	height = SLtt_Screen_Rows - 2;

	SLsmg_gotorc (height, 0);
	slrn_set_color (STATUS_COLOR);
	if (Article_Window_HScroll) smg_write_char ('<'); else smg_write_char (' ');
	SLsmg_printf (" %d : ", Header_Showing->number);
	SLsmg_write_string (Header_Showing->subject);
	slrn_print_percent (height, 60, &Slrn_Article_Window);
	slrn_set_color (0);
	
	c0 = Article_Window_HScroll;
	SLsmg_set_screen_start (NULL, &c0);
  	l = Article_Current_Line;
	
	row = Slrn_Header_Window.nrows + 2;
	if (row == 2) row--;	       /* header window not visible */

	while (row < height)
	  {
	     SLsmg_gotorc (row, 0);
	     
	     if (l != NULL) 
	       {
		  if (l->flags & HIDDEN_LINE)
		    {
		       l = l->next;
		       continue;
		    }
		  
		  display_article_line (l);
		  l = l->next;
	       }
#if SLRN_HAS_TILDE_FEATURE
	     else if (Slrn_Use_Tildes)
	       {
		  slrn_set_color (SLRN_TILDE_COLOR);
		  smg_write_char ('~');
	       }
#endif

	     slrn_set_color (0);
	     SLsmg_erase_eol ();
	     
	     row++;
	  }
	
	if (((l == NULL) 
	     || ((l->flags & SIGNATURE_LINE) && Slrn_Sig_Is_End_Of_Article))
	    && (Slrn_Current_Header == Header_Showing))
	  At_End_Of_Article = Slrn_Current_Header;
	
	SLsmg_set_screen_start (NULL, NULL);
     }
}

static void art_update_screen (void) /*{{{*/
{
   At_End_Of_Article = NULL;
#if SLRN_HAS_SPOILERS
   Spoilers_Visible = NULL;
#endif
   if (Slrn_Full_Screen_Update) Largest_Header_Number = 0;

   update_header_window ();
   update_article_window ();

   if (Slrn_Use_Mouse) slrn_update_article_menu ();
   else
     slrn_update_top_status_line ();

   if (Slrn_Message_Present == 0) 
     {
#if SLRN_HAS_SPOILERS
	if (Spoilers_Visible != NULL)
	  slrn_message ("Spoilers visible!");
	else
#endif
#if SLRN_HAS_END_OF_THREAD
	  if (Article_Visible
	      && (-1 != display_end_of_thread (Slrn_Current_Header)))
	    /* do nothing */ ;
	else
#endif
	  quick_help ();
     }

   Last_Cursor_Row = 1 + (int) Slrn_Header_Window.window_row;

   if ((Header_Window_Nrows == 0)
       && Article_Visible)
     {
	if (Slrn_Message_Present == 0)
	  display_header_line (Slrn_Current_Header, SLtt_Screen_Rows - 1);

	SLsmg_gotorc (SLtt_Screen_Rows - 1, 0);
	Slrn_Full_Screen_Update = 0;
	return;
     }

   if (Header_Window_HScroll)
     {
	int c0 = Header_Window_HScroll;
	SLsmg_set_screen_start (NULL, &c0);
     }
   display_header_line (Slrn_Current_Header, Last_Cursor_Row);
   SLsmg_set_screen_start (NULL, NULL);

   SLsmg_gotorc (Last_Cursor_Row, 0);

   slrn_set_color (CURSOR_COLOR);
#if SLANG_VERSION > 10003
   if (Slrn_Display_Cursor_Bar)
     SLsmg_set_color_in_region (CURSOR_COLOR, Last_Cursor_Row, 0, 1, SLtt_Screen_Cols);
   else
#endif
   smg_write_string ("->");

   slrn_set_color (0);
   
   Slrn_Full_Screen_Update = 0;
}

/*}}}*/


/*}}}*/

