/*
 * Copyright 1998, Dave Cridland + Stan Brooks
 * This file is part of SLirc.
 * Redistributable under GPL or PAL, or both, at your discretion.
 * See COPYING.
 */

/*
 * This was all based on a series of discussions between me and Stan.
 * I just happened to write the code out before Stan could. :-)
 */

#include <ctype.h>
#include <stdlib.h>
#include <errno.h>
#include <stdio.h>
#include <slang.h>
#include <time.h>
#include <string.h>
#include "slirc.h"

typedef struct _LineData_Type {
   char * from;
   char * to;
   char * server;
   char * text;
   unsigned char indent;
   unsigned char bold;
   unsigned char type;
   unsigned char htyp;
   /* int allwin; */
   unsigned int rxtime;
   unsigned int msgno;  /* increasing, unique message number */
   /* int real_line; */
   struct _LineData_Type * lprev;
   struct _LineData_Type * lnext;
} LineData_Type;

typedef struct _Line_Type {
   LineData_Type * line;
   struct _Line_Type * prev;
   struct _Line_Type * next;
} Line_Type;

typedef struct _WindowMatch_Type {
   char * from;
   char * to;
   char * server;
   struct _WindowMatch_Type * prev;
   struct _WindowMatch_Type * next;
} WindowMatch_Type;

typedef struct _Window_Type {
   char * name;
   WindowMatch_Type * start_match; /* association of line to window is OR of linked-list of things */
   WindowMatch_Type * end_match;
   Line_Type * startline;
   Line_Type * currentline;
   Line_Type * auxline;    /* cached location from last PutIntoWindow() */
   Line_Type * endline;
   int dirty;
   unsigned int lastseen;
   struct _Window_Type * prev;
   struct _Window_Type * next;
} Window_Type;

typedef struct _WindowList_Type {
   Window_Type * start;
   Window_Type * end;
   Window_Type * current;
   Window_Type * mini;
} WindowList_Type;

/*
 typedef struct _Transcript_Type {
    LineData_Type * start;
    LineData_Type * end;
 } Transcript_Type;
 * 
 static Transcript_Type Transcript;
*/

/* might later want to subdivide this master list by servers */
static LineData_Type * LD_first=NULL;
static LineData_Type * LD_last=NULL;

static WindowList_Type WindowList;

/* sjb: IsVisible() may be resurrected -- i still want regexp selection on windows */
static int IsVisible(LineData_Type *line)
{
  return 1;
}

/*
 * String drawing stuff...
 */

static int HeadCol(int type)
{
   if (!UseColours) return 0;
   switch(type) {
    case IRC_TYPE_LOG:    return ColLogText;
    case IRC_TYPE_INFO:   return ColInfoText;
    case IRC_TYPE_ACTION: return ColSayNormText;
    case IRC_TYPE_SAY:    return ColSayNormText;
    default:              return ColDefault;
   }
}

static char *WordBreak(char **cp, char *txth, int tw)
/*
 * a helper function for breaking input text into pieces
 * of width no more than tw chars, trying to break on a word-boundary
 * It skips the start-pointer *cp past initial whitespace,
 * and returns a pointer to the 'best' place to line-break.
 * Note: txth should point to the terminating '\0' of the
 * string (*cp), this is just to avoid re-computing it each time.
*/
{
   char *p,*q;
   p = *cp;
   /* skip p forward to non-whitespace */
   while (*p && isspace(*p)) p++ ;
   q = p;
   if (*p) {
      q += tw; if (q > txth) q = txth;
      /* go back up to 1/3 of text-width looking for a word-break */
      if (*q && !isspace(*q)) {
	char *r,*rlow;
	rlow = p+(2*tw)/3; /* don't backup any lower than this to find a word-break */
	for (r = q; --r > rlow; )
	   if (isspace(*r)) {
		q = r;
		break;
	   }
      }
   }
   *cp = p;
   return q;
}
 
/*
 * Preformat_Lines(bottom,nrows)
 * This parses out nrow lines working backwards from bottom
 * doing line-breaks, etc, for displaying on visible screen
 * It necessary to reformat each time at actual
 * time of display, because the screen may have been resized,
 * which would change (1) the location of line-breaks,
 * and (2) the number of screen-lines taken to display
 * each .text field.
 * Also, keeping each .text field un-broken in the main
 * lists(s) will considerably simplify regexp selection
 * on the .text field.
*/
static LineData_Type *Preformat_Lines(Line_Type *bottom,int nrows)
{
   Line_Type *lt;
   LineData_Type *line,*l0,*last;
   int lct = 0;

   l0 = NULL; last = NULL;

   for (lt = bottom; lct<nrows && lt ; lt = lt->prev) {
	LineData_Type *l,*l1,*l2;
	char *from, *to;
	char *p, *q, *text, *txth;
	int indent, bold, tw, ltyp, type;
   
	line = lt->line;
	if (!IsVisible(line)) continue;
	bold = 0;
	type = line->type;
	ltyp = type;

	/* Calculate indent for any continued lines */
	indent = 3;
	if ((from = line->from))
	   indent += strlen(from);
   
	if ((to = line->to))
	   if (IrcCmp(to, Target, -1) && IrcCmp(to, NickName, -1))
		indent += strlen(to) + 2;
   
	/* tw is text-width for any contin'd lines */
	tw = SLtt_Screen_Cols - indent;
   
	text = line->text;
	txth = text + strlen(text);
	while (--txth >= text && isspace(*txth)); txth++;
	if (txth == text) continue; /* line was nothing but whitespace */
	l2 = l0;
	l = l0 = l1 = NULL;
	for (p = text; p < txth; p = q) {
	   q = WordBreak(&p,txth,tw);
	   if (q == p) break;  /* nothing left but whitespace */
   
	   l = (LineData_Type *) SLmalloc(sizeof(LineData_Type));
	   if (!l) Fatal("malloc()");
	   memset((char *) l, 0, sizeof(LineData_Type)); /* this inits type=0 */
	   
	   l->text = SLmake_nstring(p,q - p);
	   if (!l->text) Fatal("malloc()");
	   l->lprev = l1;
	   l->lnext = l2;
	   if (l2) l2->lprev = l;
	   if (l1) l1->lnext = l;
	   l1 = l;
	   if (!l0) l0 = l;
   
	   l->from = from;
	   l->to = to;
	   l->indent = indent;
	   l->bold = bold;
	   l->htyp = type; /* info for scrolls that break line */
	   l->type = ltyp;
   
	   /* set bold for cont-line */
   
	   while(p<q) {
		char ch;
		ch = *p++;
		if (ch == CTRL_B) bold ^= 1;
		else if (ch == CTRL_O) bold = 0;
	   }
	   ltyp = IRC_TYPE_CONT;
   
	   lct++;
	}
	if (!last) last = l;
	if (!l0) l0 = l2; /* restore l0 if no lines resulted */
   }
   /* now free any unneeded rows at the top, leaving precisely nrows */
   while (lct>nrows) {
	line = l0;
	l0 = l0->lnext;
	l0->lprev = NULL;
	SLfree(line->text);
	SLfree((char*)line);
	lct--;
   }
   return last;
}

static void DrawString(char* text, int bold, int ColNorm, int ColBold)
{
   char* cp = text;
   
   SLsmg_set_color(ColNorm);
   for (cp = text; *cp; cp++) {
      if (*cp == CTRL_B) {
	 if ((bold ^= 1))
	   SLsmg_set_color(ColBold);
	 else
	   SLsmg_set_color(ColNorm);
      } else if (*cp == CTRL_O) {
	 bold = 0;
	 SLsmg_set_color(ColNorm);
      } else if (*cp != '')
	   SLsmg_write_char(*cp);
   }
}

static void DrawLineData(int row, LineData_Type * line)
{
    int i;
		
    SLsmg_gotorc(row, 0);

    if(UseColours)
	SLsmg_set_color(ColDefault);

    if (line) {
      switch (line->type) {

      case IRC_TYPE_INFO:

        if (UseColours) SLsmg_set_color(ColLogTag);
        SLsmg_write_string("++ ");
        DrawString(line->text, line->bold, HeadCol(line->htyp), ColBufferTextBold);
        break;

      case IRC_TYPE_LOG:
        if (UseColours) SLsmg_set_color(ColLogTag);
        SLsmg_write_string("// ");
        DrawString(line->text, line->bold, HeadCol(line->htyp), ColBufferTextBold);
        break;

      case IRC_TYPE_ACTION:
        if (UseColours) SLsmg_set_color(ColSayNormDec);
        SLsmg_write_string(">>");
        if (UseColours) SLsmg_set_color(ColSayNormSend);
        SLsmg_write_string(line->from);
        {
          int ColNorm = HeadCol(line->htyp);
          SLsmg_set_color(ColNorm);
          SLsmg_write_char(' ');
          DrawString(line->text, line->bold, ColNorm, ColBufferTextBold);
        }
        break;

      case IRC_TYPE_SAY:
        if (UseColours) SLsmg_set_color(ColSayNormDec);
        SLsmg_write_string("<");
        if (UseColours) {
          if (!IrcCmp(line->to, NickName, -1))
            SLsmg_set_color(ColSayTgtoSend);
          else if (!IrcCmp(line->text, NickName, strlen(NickName))
                || !IrcCmp(line->from, NickName, -1) )
            SLsmg_set_color(ColAutoResp);
          else
            SLsmg_set_color(ColSayNormSend);
        }
        SLsmg_write_string(line->from);
        if (IrcCmp(line->to, Target, -1) && IrcCmp(line->to, NickName, -1)) {
          if (UseColours) SLsmg_set_color(ColSayNormDec);
          SLsmg_write_string("->");
          if (UseColours) SLsmg_set_color(ColSayTgtoSend);
          SLsmg_write_string(line->to);
        }
        if (UseColours) SLsmg_set_color(ColSayNormDec);
        SLsmg_write_string("> ");
        DrawString(line->text, line->bold, HeadCol(line->htyp), ColBufferTextBold);
        break;

      case IRC_TYPE_CONT:
        for (i = 0; i < line->indent; i++) SLsmg_write_char(' ');
        DrawString(line->text, line->bold, HeadCol(line->htyp), ColBufferTextBold);
        break;

      default:
        SLsmg_write_string("!! ");
        SLsmg_write_string("Unknown line type.");
      }                          /* switch */
      if (UseColours) SLsmg_set_color(ColDefault);
    }  /* if (line) */
    SLsmg_erase_eol();
}

static int DrawWindow(Window_Type * window, int startrow, int length)
{
   int n=0;
   if(window && window->dirty)
     {
	Line_Type * lt;
	LineData_Type *line;
	
	n=1;
	/* if(UseColours) SLsmg_set_color(ColDefault);
	   SLsmg_fill_region(startrow, 0, length, SLtt_Screen_Cols, ' ');
	*/
	
	if(!(lt=window->currentline))
	  lt=window->endline;
	
	if (lt) window->lastseen=lt->line->msgno;
	
	/* Preformat with appropriate line-breaks and indentation,
	 * returning a temporary list of formatted lines ready to
	 * be displayed by DrawLineData()     */
	line=Preformat_Lines(lt,length);
	
	for( ; length>0; length--)
	  {
		LineData_Type *l0;

		DrawLineData(startrow+length-1, line);
		if (line==NULL) continue;

		l0 = line->lprev;
	   	SLfree(line->text);   /* free that trash */
	   	SLfree((char*)line);
		line = l0;
	  }

	window->dirty=0;
     }
     return n;
}

int DrawAllWindows(int start_row, int lines)
{
   int n=0;
   if(WindowList.mini && (WindowList.mini != WindowList.current))
     {
	n += DrawWindow(WindowList.mini, start_row, 4);
	SLsmg_gotorc(start_row + 4, 0);
	SLsmg_set_color(UseColours? ColStatusSay : 1);
	SLsmg_erase_eol();
	SLsmg_write_string(" [");
	SLsmg_write_string(WindowList.mini->name);
	SLsmg_write_string("]");
	start_row+=5;
	lines-=5;
	SLsmg_set_color(ColDefault);
     }
   n += DrawWindow(WindowList.current, start_row, lines);
   return n;
}

static void ChangeWindow(Window_Type * window)
{
   WindowList.current=window;
   window->dirty=1;
   window->currentline=NULL;
   if(WindowList.mini)
     WindowList.mini->dirty=1;
}

static Window_Type * FindWindow(char * name)
{
   Window_Type * window;
   
   for(window=WindowList.end; window; window=window->prev) {
     if(!IrcCmp(name, window->name, -1))
       break;
	 }
   return(window);
}

/*
 * New matching driver thing.
 * The old way was to have multiple window defs, but that was a right old hack.
 */
static int WindowAddMatch(Window_Type * window, char * to_match, char * from_match, char * server_match)
{
   WindowMatch_Type * match;
   
   if(!window)
      return(1);
   
   match = (WindowMatch_Type *)SLmalloc(sizeof(WindowMatch_Type));
   memset(match, 0, sizeof(WindowMatch_Type));
   
   if(!match)
      return(2);
   
   if(from_match[0])
     match->from = SLmake_string(from_match);
   
   if(to_match[0])
     match->to = SLmake_string(to_match);
   
   if(server_match[0])
     match->server = SLmake_string(server_match);
   
   if(window->start_match==NULL)
     window->start_match = window->end_match = match;
   else
     {
	window->end_match->next = match;
	match->prev = window->end_match;
	window->end_match = match;
     }
   
   return(0);
}

static Window_Type * NewWindow(char * name, char * to_match, char * from_match, char * server_match)
{
   Window_Type * window;
   
   if(!(window=FindWindow(name)))
     {
	window=(Window_Type *)SLmalloc(sizeof(Window_Type));
	memset((char *)window, 0, sizeof(Window_Type));
	/* silent assumption that this initializes pointer fields to NULL */
	window->name=SLmake_string(name);
     }
   
   WindowAddMatch(window, to_match, from_match, server_match);

   return(window);
}

/*
 * Which window should get this line?
 */
static Window_Type * WhichWindow(LineData_Type * line)
{
   Window_Type * window;
   WindowMatch_Type * match;
   
   for(window=WindowList.end; window; window=window->prev)
     for(match=window->end_match; match; match=match->prev)
       if(!match->server || (line->server && !IrcCmp(match->server, line->server, -1)))
	 if(!match->from || (line->from && !IrcCmp(match->from, line->from, -1)))
	   if(!match->to || (line->to && !IrcCmp(match->to, line->to, -1)))
	     return(window);
   
   return NULL;
}

/*
 * Puts a LineData_Type object into a window.
 * Note that most of this code is handling lines added
 * out of order, which should only happen when a window is
 * deleted.
 * sjb: this looked cpu-intensive to search whole list
 * to find successive msgno's... So, modified it to cache
 * last added line as window->auxline, and starting successive
 * searches from that point, since additions (after a window delete)
 * will generally be in order.
 */
static Window_Type * PutIntoWindow(LineData_Type * line)
{
   Window_Type * window = WhichWindow(line);
   Line_Type *lt,*xlt;
   
   /* create a Line_Type *lt for this line */
   lt=(Line_Type *)SLmalloc(sizeof(Line_Type));
   memset((char *)lt, 0, sizeof(Line_Type));
   lt->line=line;
   
   if((xlt=window->endline))
     {
	unsigned int msgno = line->msgno;
	/* sjb: next loop is (should be) redundant */
	/* while(window->endline->next)            */
	/*  window->endline=window->endline->next; */
	
	if(xlt->line->msgno>msgno)
	  {  /* doesn't go last in list, so start search from window->auxline if set */
	     if(window->auxline) xlt = window->auxline;
	
	     while(xlt->line->msgno<msgno && xlt->next) xlt=xlt->next;
	     while(xlt->line->msgno>msgno && xlt->prev) xlt=xlt->prev;
	  }

	if(xlt->line->msgno<msgno)
	  { /* insert after xlt */
	    if(!(lt->next = xlt->next))
	      window->endline=lt;
	    else
	      xlt->next->prev=lt;

	    lt->prev = xlt;
	    xlt->next = lt;
	  }
	else
	  { /* insert before xlt */
	    if(!(lt->prev = xlt->prev))
	      window->startline=lt;
	    else
	      xlt->prev->next=lt;

	    lt->next = xlt;
	    xlt->prev = lt;
	  }

     }
   else
     window->startline=window->endline=lt;

   window->auxline=lt; /* cache last insertion point */
   window->dirty=1;
   return(window);
}

static Window_Type * AddNewWindow(char * name, char * to, char * from, char * server)
{
   Window_Type * old_window;
   Window_Type * new_window;
   LineData_Type ltmp;
   
   /*
    * Setup dummy line ltmp with suitable data.
    */
   
   memset((char *)&ltmp, 0, sizeof(LineData_Type));

   ltmp.from=from[0]? from:"*";
   ltmp.to=to[0]? to:"*";
   ltmp.server=server[0]? server:"*";
   
   /*
    * Okay, that's a dummy line setup.
    * Note that we don't actually need to malloc those strings properly,
    * because ltmp is just used locally and forgotten...
    */

   old_window=WhichWindow(&ltmp);  /* this can be NULL? */
   new_window=NewWindow(name, to, from, server);

   /*
    * If this was a ruleset addition, rather than a window, then we already
    * have the window in the list...
    */
   
   if(!FindWindow(name))
     {
	new_window->prev=old_window;
	new_window->next=old_window->next;
	
	old_window->next=new_window;
	if(new_window->next)
	  new_window->next->prev=new_window;
	
	if(WindowList.end==old_window)
	  WindowList.end=new_window;
     }
   /*
    * Okay. Now we need to reshuffle the old window's lines, because some of those
    * lines might be better placed in the new window.
    */
   
   if(old_window->startline)
     {  /* take care, because lines can go right back into old_window */
        Line_Type *lt,*nlt;
	lt=old_window->startline;
	old_window->startline=old_window->endline=old_window->auxline=NULL;
	for( ; lt; lt=nlt)
	  {
	     nlt=lt->next;
	     PutIntoWindow(lt->line);
	     SLfree((char *)lt);
	  }
	old_window->currentline=NULL;
     }

   return(new_window);
}

/*
 * Stan! Stan! Stan!
 * This is probably all wrong.
 * But then, I wrote it.
 *   -  dwd
 */
static char * SLirc_WhichWindow(char * to, char * from, char * server)
{
   Window_Type * win;
   LineData_Type ltmp;

   memset((char *)&ltmp, 0, sizeof(LineData_Type));
   
   ltmp.from=from[0]? from:"*";
   ltmp.to=to[0]? to:"*";
   ltmp.server=server[0]? server:"*";
   
   win = WhichWindow(&ltmp);
   
   return(win->name);
}

static void DeleteWindow(char * name)
{
   Window_Type * old_window=FindWindow(name);
   
   if(!old_window || !old_window->prev) return; /* Don't delete the default window. */
   old_window->prev->next=old_window->next;
   
   if(old_window->next)
     old_window->next->prev=old_window->prev;
   else
     WindowList.end=old_window->prev;
   
   /*
    * Now resort this window into the windows that are left...
    */
   if((old_window->startline))
     {
	Line_Type *lt, *nlt;
	
	for(lt=old_window->startline; lt; lt=nlt)
	  {
	     nlt=lt->next;
	     PutIntoWindow(lt->line);
	     SLfree((char *)lt);
	  }
	old_window->startline=old_window->endline=NULL;
     }
   SLfree((char *)old_window); /* sjb: memory leak -- need to free .name */
   while((old_window=FindWindow(name)))
     {
	if(!old_window->prev) return; /* Don't delete the default window. */
	old_window->prev->next=old_window->next;
	
	if(old_window->next)
	  old_window->next->prev=old_window->prev;
	else
	  WindowList.end=old_window->prev;
	SLfree((char *)old_window);
     }
}

static void scroll_n(Window_Type * window, int n)
{
   Line_Type * l;

   l=window->currentline;
   if(!l) l=window->endline;

   if(!n || !l) return;

   if(n>0)
	while(n && l->next) {
	  l = l->next;
	  if (IsVisible(l->line)) n--;
	}
   else
	while(n && l->prev) {
	  l = l->prev;
	  if (IsVisible(l->line)) n++;
	}
   
   if (window->currentline != l) {
     window->currentline=l;
     window->dirty=1;
   }
}

static void scroll_x(int *x)
{
   scroll_n(WindowList.current, *x);
}

static void scroll_eob(void)
{
   Window_Type *w;
   w=WindowList.current;
   if (w->currentline != w->endline) {
     w->currentline=w->endline;
     w->dirty=1;
   }
}

static void scroll_bob(void)
{
   Window_Type *w;
   w=WindowList.current;
   if (w->currentline != w->startline) {
     w->currentline=w->startline;
     w->dirty=1;
   }
}

void DirtyWindow(void)
{
   WindowList.current->dirty=1;
   if(WindowList.mini)
     WindowList.mini->dirty=1;
}

/*
 * this next is the place where all things enter the windoze lists,
 * that is, become LineData_Type objects
*/
void * SLirc_Multn(char * text, char * from, char * to, unsigned int type)
{
   static int MsgNo=0;
   LineData_Type *line;
   Window_Type *w;
   
   line = (LineData_Type *) SLmalloc(sizeof(LineData_Type));
   if (!line) Fatal("malloc()");
   memset((char *) line, 0, sizeof(LineData_Type));
   
   /*
    * now put this new line in the master list
    * BTW, the 'master list' doesn't currently do much
    * of anything, but since there's an lprev,lnext field
    * it may as well be used... we might want to free things
    * one day, or dump it all to a file or sth
   */
   line->lprev = LD_last;
   line->lnext = NULL;
   if (!LD_first)
      LD_first = line;
   else
      LD_last->lnext = line;
   LD_last = line;

   /* fill in line's data fields */
   if ((line->from = from))
     if (!(line->from = SLmake_string(from))) Fatal("malloc()");
   if ((line->to = to))
     if (!(line->to = SLmake_string(to))) Fatal("malloc()");
   if ((line->text = text))
     if (!(line->text = SLmake_string(text)))	Fatal("malloc()");
   if ((line->server = ServerName))
     if (!(line->server = SLmake_string(ServerName)))	Fatal("malloc()");
   line->type = type;
   line->msgno = MsgNo++;
   line->rxtime=time(NULL);

   w=PutIntoWindow(line);
   scroll_n(w, 1);
   w->dirty=1;

   return(line);
}

static void SLirc_ChangeMini(char * name)
{
   WindowList.mini=FindWindow(name);
   WindowList.current->dirty=1;
   if(WindowList.mini)
     WindowList.mini->dirty=1;
}

static void SLirc_ChangeWindow(char * name)
{
   Window_Type *w;
	 w=FindWindow(name);
   if(w) ChangeWindow(w);
}

static int IsUnread(Window_Type * w)
{
   return (w->endline && w->endline->line->msgno > w->lastseen);
}

static char * SLirc_GetWindowList(void)
{
   static char buf[1024]; /* buffer overflow */
   char * cp = buf;
   Window_Type * w;
   
   *cp=0;
   
   for(w=WindowList.start; w; w=w->next) {
     sprintf(cp, "%c%s ", IsUnread(w)?'*':'-', w->name);
     cp += strlen(cp);
   }
   return(buf);
}


static SLang_Intrin_Fun_Type WindowsFunctions[] =
{
   MAKE_INTRINSIC_I("irc_scroll", scroll_x, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_0("irc_eob", scroll_eob, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_0("irc_bob", scroll_bob, SLANG_VOID_TYPE),
   
   MAKE_INTRINSIC_S("irc_change_window", SLirc_ChangeWindow, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_N("irc_create_window", AddNewWindow, SLANG_VOID_TYPE, 4, SLANG_STRING_TYPE, SLANG_STRING_TYPE, SLANG_STRING_TYPE, SLANG_STRING_TYPE, 0, 0, 0),
   MAKE_INTRINSIC_S("irc_destroy_window", DeleteWindow, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_S("irc_change_mini", SLirc_ChangeMini, SLANG_VOID_TYPE),
   MAKE_INTRINSIC_SSS("irc_which_window", SLirc_WhichWindow, SLANG_STRING_TYPE),
   MAKE_INTRINSIC_0("irc_get_winlist", SLirc_GetWindowList, SLANG_STRING_TYPE),
   
   SLANG_END_TABLE
};

void Init_Windows()
{
   SLadd_intrin_fun_table(WindowsFunctions, "_Windoze");
   WindowList.start=WindowList.end=WindowList.current=NewWindow("Default", "", "", "");
   WindowList.mini=NULL;
}


syntax highlighted by Code2HTML, v. 0.9.1