/*$Copyright:
 * Copyright (C) 1992.5.22. Recruit Co.,Ltd. 
 * Institute for Supercomputing Research
 * All rights reserved.
 * NewsBase  by ISR, Kazuto MIYAI, Gary ARAKAKI, Katsunori SUZUKI, Kok-meng Lue
 *
 * You may freely copy, distribute and reuse the code in this program under 
 * following conditions.
 * - to include this notice in the source code, if it is to be distributed 
 *   with source code.
 * - to add the file named "COPYING" within the code, which shall include 
 *   GNU GENERAL PUBLIC LICENSE(*).
 * - to display an acknowledgement in binary code as follows: "This product
 *   includes software developed by Recruit Co.,Ltd., ISR."
 * - to display a notice which shall state that the users may freely copy,
 *   distribute and reuse the code in this program under GNU GENERAL PUBLIC
 *   LICENSE(*)
 * - to indicate the way to access the copy of GNU GENERAL PUBLIC LICENSE(*)
 *
 *   (*)GNU GENERAL PUBLIC LICENSE is stored in the file named COPYING
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
$*/

#import "IOrderedListD.h"
#import "InfoD.h"
#import "INewsgroupInfoD.h"
#import "IOrderedListD.h"
#import "INntpIO.h"
#import "ITreeNodeD.h"
#import "errdebug.h"
#import "data_types.h"
#import "IUifNode.h"

#import <appkit/appkit.h>
#import <stdio.h>
#import <ctype.h>
#import <libc.h>
#import <string.h>
#import <mach/mach.h>
#import <objc/zone.h>
#import "Localization.h"

#define LoStr(key)      doLocalString(NULL,key,NULL)

static HashTable *tableOfNoiseWords;
static const char *noiseWords[] = {
#include "noise_words.h"
};

#define		SIZEMARKONE	"sizemarkOne"
#define		SIZEMARKTWO	"sizemarkTwo"
#define		SIZEMARKTHREE	"sizemarkThree"

static const char *ikey[MAX_NO_OF_TOKENS];

@implementation INntpIO

+ initialize
{
    char filename[512];
    NXStream *stream;
    char *buffer;
    int len, maxlen;
    char *ptr;
    const char **word;

    tableOfNoiseWords = [[HashTable alloc] initKeyDesc:"*"];
    sprintf(filename, "%.491s/.NewsBaseNoiseWords", getenv("HOME"));
    if ((stream = NXMapFile(filename, NX_READWRITE)) != NULL) {
        NXSeek(stream, (long)0, NX_FROMEND);
        NXPutc(stream, '\0');
        NXGetMemoryBuffer(stream, &buffer, &len, &maxlen);
        for (ptr = strtok(buffer, " \n\r"); ptr != NULL;
            ptr = strtok(NULL, " \n\r")) {
            [tableOfNoiseWords insertKey:ptr value:nil];
        }
        NXCloseMemory(stream, NX_SAVEBUFFER);
        return(self);
    } else {
        for (word = noiseWords; *word != NULL; ++word) {
            [tableOfNoiseWords insertKey:*word value:nil];
        }
        return(self);
    }
}

- initServer:(char *)nntpHost allNews:(BOOL)allNewsFlag
			newsGroupMode:(BrowserMode)g_mode
			articleMode:(BrowserMode)a_mode
{
    const char	*buffer;

    iNewsGroupMode = g_mode;
    iArticleMode = a_mode;
    irFlag = UNMARKEDONLY;

    // set default
    iBoundaryForTwo = 100; iBoundaryForThree = 300;
    // read default database and set boundary Line # for line mark
    if ((buffer = NXGetDefaultValue(OWNER, ARTICLESIZEMARK)) != 0) {
        sscanf(buffer, "0:%d:%d", &iBoundaryForTwo, &iBoundaryForThree);
    }
    DBG(1, fprintf(stderr,"boundaryTwo = %d\t Tree = %d\n", 
    					iBoundaryForTwo, iBoundaryForThree));
    return [super initServer:nntpHost allNews:allNewsFlag];
}

- subDirectoryOf:knode
{
    /* directory structure of newsgroup tree				*/
    /*									*/
    /*  [iNewsGroupTreeRoot]-+-[tree node]-+-[newsgroupInfo]		*/
    /*			     |             +-[tree node]-[newsgroupInfo]*/
    /*					   +-[tree node]+[tree node]	*/
    
    if (iNewsGroupMode == Tree) {
	return [self _subDirectoryOf:knode];
    } else {
	return [self _flatDirectoryOf:knode];
    }
}

- _subDirectoryOf:knode
{
    id		dirNewsGroup, childNewsGroup;
    id		listnode;
    id		node;
    id		n;
    int		i, j;
    int		count;
    char	*subscribe;
    id		groupInfo;
    char	*newsgroupName;
    NXTreeState	state;
    id		tnode;

    /* set newsgroup Tree node */
    if (knode == nil) {
	/* dirNode is rootNode */
	dirNewsGroup = iNewsGroupTreeRoot;
    } else {
	dirNewsGroup = [knode linkedData];
    }
    
    listnode = [[IOrderedListD allocFromZone:[self zone]]
						initWithKey:"listnode"];
    
    for (i=0; childNewsGroup=[dirNewsGroup objectAt:i]; ++i) {
	if ([childNewsGroup isMemberOf:[ITreeNodeD class]]==NO) {
	    /* childNewsGroup is not a real newsgroup Tree node */
	    continue;
	}
	
	// set leaf or not
	count = 0;
	for (j=0; n=[childNewsGroup objectAt:j]; ++j) {
	    if ([n isMemberOf:[ITreeNodeD class]]==NO) {
		continue;
            }
            ++count;
	}
	
	// a childNewsGroup is classified into 3 types
	//   1. directory
	//   2. leaf and directory
	//        childNewsGroup has tree node and newsgroup info
	//   3. leaf
	
	// 1. directory only
	if ((count > 0) && [childNewsGroup dataForKey:GROUPINFO] == nil) {
	    node = [[IUifNode allocFromZone:[self zone]] 
					    initWithKey:[childNewsGroup key]];
	    [listnode addObject:node];
	    [node setTitleForCell:[childNewsGroup key]];
	    [node setLinkedData:childNewsGroup];
            [node setNodeType:DirOfSubDirs];
	    [node setLeaf:NO];
	    
	    // check newsgroup is subscribed or not here to 
	    // set directory active or notactive
	    [node setActive:NO];	// default is not active

	    [childNewsGroup initState:&state];
	    while (tnode = [childNewsGroup nextState:&state]) {
		if ((strcmp([tnode key], GROUPINFO) == 0)
		    && (strcmp([tnode infoForKey:SUBSCRIBE], "yes") == 0 )) {
		    // found a active newsgroup under me
		    [node setActive:YES];
		    break;
		}
	    }
	} else if (count > 0) {
	// 2. leaf and directory
	
	    // insert leaf cell
	    // 
	    node = [[IUifNode allocFromZone:[self zone]] 
				    initWithKey:[childNewsGroup key]];
	    [listnode addObject:node];
	    [node setTitleForCell:[childNewsGroup key]];
	    [node setLinkedData:childNewsGroup nodeType:DirOfItems];
	    [node setLeaf:YES];
	    // set image
	    if ((groupInfo=[childNewsGroup dataForKey:GROUPINFO]) != nil) {
		if ([groupInfo isAllArticleMarked] == NO) {
		    [node setImageForCell:
				    [NXImage findImageNamed:UNREADMARK]];
		}
	    }
	    /* set newsgroup name for dataGroupName */
	    [node setDataGroupName:[[childNewsGroup dataForKey:GROUPINFO]
		infoForKey:GROUPNAME]];

	    DBG(10,fprintf(stderr,"newsgroupName=%s",newsgroupName));
	    /* set node active for subscribe, no active for unsubscribe */
	    subscribe = [[childNewsGroup dataForKey:GROUPINFO]
						    infoForKey:SUBSCRIBE];
	    if(strncmp(subscribe,"y",1)==0) {
		[node setActive:YES];
	    } else {
		[node setActive:NO];
	    }

	    // insert directory cell
	    //
	    node = [[IUifNode allocFromZone:[self zone]] 
					    initWithKey:[childNewsGroup key]];
	    [listnode addObject:node];
	    [node setTitleForCell:[childNewsGroup key]];
	    [node setLinkedData:childNewsGroup];
            [node setNodeType:DirOfSubDirs];
	    [node setLeaf:NO];

	    // check newsgroup is subscribed or not here to 
	    // set directory active or notactive
	    [node setActive:NO];	// default is not active

	    [childNewsGroup initState:&state];
	    while (tnode = [childNewsGroup nextState:&state]) {
		if ((strcmp([tnode key], GROUPINFO) == 0)
		    && (strcmp([tnode infoForKey:SUBSCRIBE], "yes") == 0 )) {
		    [node setActive:YES];
		    break;
		}
	    }
	} else {
	// 3. leaf

	    node = [[IUifNode allocFromZone:[self zone]] 
					    initWithKey:[childNewsGroup key]];
	    [listnode addObject:node];
	    [node setTitleForCell:[childNewsGroup key]];
	    [node setLinkedData:childNewsGroup];
            [node setNodeType:DirOfItems];
	    [node setLeaf:YES];
	    if ((groupInfo=[childNewsGroup dataForKey:GROUPINFO]) != nil) {
		if ([groupInfo isAllArticleMarked] == NO) {
		    [node setImageForCell:[NXImage findImageNamed:UNREADMARK]];
		}
	    }
	    [node setDataGroupName:[[childNewsGroup dataForKey:GROUPINFO] 
                infoForKey:GROUPNAME]];

	    subscribe = [[childNewsGroup dataForKey:GROUPINFO]
	    					 	infoForKey:SUBSCRIBE];
            if(strncmp(subscribe,"y",1)==0) {
		[node setActive:YES];
	    } else {
		[node setActive:NO];
	    }
	}
    }
    return listnode;
}

- _flatDirectoryOf:knode
{
    id		newsGroup;
    id		listnode, node;
    NXTreeState state;
//    const char	*key;
    char	*subscribe;
    id		treenode;
    char	*newsgroupName;
    id		groupInfo;

    /* set newsgroup Tree node */
    /* only rebuild from root(==nil) for now */
    if (knode == nil) {
	/* dirNode is rootNode */
	newsGroup = iNewsGroupTreeRoot;
    } else {
	newsGroup = [knode linkedData];
    }
    
    listnode = [[IOrderedListD allocFromZone:[self zone]]
						initWithKey:"listnode"];
    [newsGroup initState:&state];
    while (treenode=[newsGroup nextState:&state]) {
	if ([treenode isMemberOf:[ITreeNodeD class]]==NO) {
	    /* newsGroup is not a real newsgroup Tree node */
	    continue;
	}
	if ([treenode dataForKey:GROUPINFO]==nil) {
	    /* this node is only for grouping node, not have newsgroup inof */
	    continue;
	}
        newsGroup = treenode;

	newsgroupName = [[newsGroup dataForKey:GROUPINFO] infoForKey:GROUPNAME];
	node = [[IUifNode allocFromZone:[self zone]] initWithKey:newsgroupName];
	[node setTitleForCell:newsgroupName];
	[node setLinkedData:newsGroup nodeType:DirOfItems];
	[node setDataGroupName:newsgroupName];
	[node setLeaf:YES];		/* flat's cell is always leaf */
	// set image
	if ((groupInfo=[newsGroup dataForKey:GROUPINFO]) != nil) {
	    if ([groupInfo isAllArticleMarked] == NO) {
		[node setImageForCell:[NXImage findImageNamed:UNREADMARK]];
	    }
	}
	subscribe = [[newsGroup dataForKey:GROUPINFO] infoForKey:SUBSCRIBE];
	if(strncmp(subscribe,"y",1)==0) {
	    [node setActive:YES];
	} else {
	    [node setActive:NO];
	}
	[listnode addObject:node];
    }
    return listnode;
}

- itemHeadersOf:knode
{
    /* itemHeadersOf 							*/
    /*   from newsgroup browser: click on leaf cell of newsgroup browser */
    /*   from article browser: click on article directory */
    
    id		linkdata;

    if ((linkdata = [knode linkedData]) == nil) {
	return 0;
    }

    switch ([knode nodeType]) {
    case DirOfItems:
	/* linkdata has groupInfo, so coming from NewsGroup browser */
	switch(iArticleMode) {
        case Flat:
	    /* get headers from newsgroup info */
	    return [self _itemHeadersOf:knode];
        case Tree:
	    /* Tree mode */
	    /* clicked on newsgroup browser, create article reference tree */

	    /* make iArticleDB with article headers */
	    [self _itemHeadersOf:knode];
	    /* make articleTree from iArticleDB */
	    [self _makeArticleTree];
	    /* return listnode for column-0 for ReferenceMode */
	    return [self _itemTreeHeadersOf:knode];
        case BySubject:
	    /* return listnode for column-0 for BySubjectMode */
	    return([self _subjectHeadersOf:knode]);
        case ByKeyword:
	    /* return listnode for column-0 for ByKeywordMode */
	    return([self _keywordHeadersOf:knode]);
	}
    case ReferenceGroup:
	/* Article Browser should be in Tree Mode */
        return [self _itemSubTreeHeadersOf:knode];
    case SubjectGroup:
    case KeywordGroup:
	/* Article Browser should be in BySubject or ByKeyword Mode */
        return([self _itemHeadersOfSubject:knode]);
    default:
        return(0);
    }
}

- _itemTreeHeadersOf:knode
{
    id		listnode, node;
    int		i, j, k;
    id		childArticle, articleItem, g_child, gg_child;
    id		last_articleGroup;
    char 	*subject;
    int		tree_node_count;
    id		groupInfo, headerInfo;
    const char	*newsgroupName;
    char 	*groupnameTmp;
    char	*cline;
    id		image;
    BOOL	isUnreadArticle;

    /* _itemTreeHeadersOf is for filling cell in colum-0 of article browser */
    /* _itemSubTreeHeadersOf is for column-1 */

    /* return listnode for column-0 */
    listnode = [[IOrderedListD allocFromZone:[self zone]]
						initWithKey:"listnode"];
						
    // search groupInfo
    groupnameTmp = NXCopyStringBuffer([knode dataGroupName]);
    makeTreeKey (groupnameTmp, ikey, ".");
    groupInfo=[[iNewsGroupTreeRoot nodeForKey:ikey] 
					    dataForKey:GROUPINFO];
    free(groupnameTmp);
    
    /* iArticleReferenceTreeRoot sould be made befor here */
    for (i=0;childArticle=[iArticleReferenceTreeRoot objectAt:i];++i) {
	/*  	iArticleReferenceTreeRoot strucrture			*/
	/* (1) case 1							*/
	/*[root]--+-[tree_node]---[articleItem]-[header]		*/
	/*        |(childArticle)					*/
	/* (2) case 2							*/
	/*        +-[tree_node]-+-[articleItem]-[header]	     	*/
	/*	  |		|				     	*/
	/*	  |		+-[tree_node]-[articleItem]-[header]	*/
	/*	  |		+-[tree_node]-[articleItem]-[header]	*/
	/* (3) case 3						     	*/
	/*	(original article is already removed in server)     	*/
	/*        +-[tree_node]-+				    	*/
	/*			+-[tree_node]-[articleItem]-[header]	*/
	/*			+-[tree_node]-[articleItem]-[header]	*/

	if ([childArticle isMemberOf:[ITreeNodeD class]]==NO) {
	    /* childArticle must be ITreeNodeD */
	    /* if you go into here, something wrong */
	    fprintf (stderr,"INntpIO: error: childArticle is not a member of ITreeNodeD\n");
	    continue;
	}
						
	/* search articleItem, count tree_node, set title for cell */
	/* tree_node does not 	*/
	/* have articleItem, use articleItem of last child of 	*/
	/* this node as articleItem for tree_node		*/
	
	articleItem = nil;		/* articleItem */
	tree_node_count = 0;
	isUnreadArticle = NO;
	for (j=0; g_child=[childArticle objectAt:j]; ++j) {
	    if ([g_child isMemberOf:[IOrderedListD class]] &&
			([g_child dataForKey:HEADER_INFO]!=nil)) {
		/* articleItem have header.tbl */
		/* case (1) or (2) */
		articleItem = g_child;
		node = [[IUifNode allocFromZone:[self zone]] 
				    		initWithKey:[articleItem key]];
		[listnode addObject:node];
		[node setLeaf:YES];
		[node setLinkedData:articleItem nodeType:Header];
                newsgroupName = [knode dataGroupName];
                [node setDataGroupName:newsgroupName];
		headerInfo = [g_child dataForKey:HEADER_INFO];
		subject = [headerInfo infoForKey:SUBJECT];
		// set title and image
		[node setTitleForCell:subject];
		if ((cline=(char *)[headerInfo infoForKey:LINES]) != NULL) {
		    // article header has "Lines:", so set mark to cell
		    image = [self getArticleMarkFor:(int)atoi(cline)];
		    [node setImageForCell:image];
		}

		/* check article is already read or not */
		if ([groupInfo checkArticleIsRead:
				(int)[headerInfo infoForKey:ARTICLE_NUM]]) {
		    [node setActive:NO];
		} else {
		    [node setActive:YES];
		}
	    } else if ([g_child isMemberOf:[ITreeNodeD class]]==YES) {
		/* g_child is tree_node */
		/* case (2) or (3) */
                last_articleGroup = g_child;
		++ tree_node_count;
		// check if this g_child's article is already read
		for (k=0; gg_child=[last_articleGroup objectAt:k]; ++k) {
		    if ([gg_child isMemberOf:[IOrderedListD class]] &&
		      ((headerInfo=[gg_child dataForKey:HEADER_INFO])!=nil)) {
			// gg_child is articleItem
			if ([groupInfo checkArticleIsRead: 
			    (int)[headerInfo infoForKey:ARTICLE_NUM]] == NO) {
			    // there is an unread article 
			    isUnreadArticle = YES;
			    break;
			}
		    }
		}		
	    }
	}
	if (articleItem == nil) {
	    /* case (3), can't find right article */
	    /* g_child is a last article of this article group */
	    for (j=0; gg_child=[last_articleGroup objectAt:j]; ++j) {
		if ([gg_child isMemberOf:[IOrderedListD class]] &&
			([gg_child dataForKey:HEADER_INFO]!=nil)) {
		    articleItem = gg_child;
		    break;
		}
	    }
	}
	subject = [[articleItem dataForKey:HEADER_INFO] infoForKey:SUBJECT];
	    
	if (tree_node_count >0 ) {
	    node = [[IUifNode allocFromZone:[self zone]] 
					initWithKey:[childArticle key]];
	    [listnode addObject:node];
	    [node setTitleForCell:subject];
	    [node setLeaf:NO];
	    [node setLinkedData:childArticle nodeType:ReferenceGroup];
	    [node setDataGroupName:[knode dataGroupName]];
	    // check if all article is read or not to set directory's active
	    if (isUnreadArticle == NO) {
		// child node is all not active, so directory is not active
		[node setActive:NO];
	    } else {
		[node setActive:YES];
	    }
	}
    }
    DBG(20, {int i; id child; for(i=0;child=[listnode objectAt:i];++i) {
		fprintf(stderr,"child key=%s\n",[child key]);} });
    return listnode;
}
		
- _itemSubTreeHeadersOf:knode
{
    id		listnode, node;
    id		articleGroup;
    id		articleItem, headerInfo, groupInfo;
    id		childArticle, g_child;
    int		i, j;
    const char	*newsgroupName;
//    char	groupnameTmp[256];
    char	*groupnameTmp;
    char	*cline;
    id		image;
    
    articleGroup = [knode linkedData];
    /* articleGroup is [tree_node], represent article group node  */
    
    listnode = [[IOrderedListD allocFromZone:[self zone]]
    						 initWithKey:"listnode"];
    for (i=0; childArticle=[articleGroup objectAt:i]; ++i) {
	/* 			   (articleItem sould be displayed in	*/
	/*						column-0)	*/
	/*        +-[tree_node]-+-([articleItem]-[header])	     	*/
	/*	  		|				     	*/
	/*	  		+-[tree_node]-[articleItem]-[header]	*/
	/*	  		+-[tree_node]-[articleItem]-[header]	*/

        if ([childArticle isMemberOf:[ITreeNodeD class]]==NO) {
	    /* only [tree_node] are under article group node */
	    continue;
	}	
	for (j=0; g_child=[childArticle objectAt:j]; ++j) {
	    if ([g_child isMemberOf:[IOrderedListD class]] && 
			((headerInfo=[g_child dataForKey:HEADER_INFO])!=nil)) {
		/* g_child is article */
		articleItem = g_child;
		node = [[IUifNode allocFromZone:[self zone]] 
				    		initWithKey:[articleItem key]];
		[listnode addObject:node];
		[node setTitleForCell:[headerInfo infoForKey:MESSAGE_ID]];
		if ((cline=(char *)[headerInfo infoForKey:LINES]) != NULL) {
		    // article header has "Lines:", so set mark to cell
		    image = [self getArticleMarkFor:(int)atoi(cline)];
		    [node setImageForCell:image];
		}
		[node setLinkedData:articleItem nodeType:Header];
                newsgroupName = [knode dataGroupName];
                [node setDataGroupName:newsgroupName];
		[node setLeaf:YES];
		/* check article is already read or not */
		//strncpy(groupnameTmp, newsgroupName, sizeof(groupnameTmp)-1);
		groupnameTmp = NXCopyStringBuffer(newsgroupName);
		makeTreeKey(groupnameTmp, ikey, ".");
		groupInfo=[[iNewsGroupTreeRoot nodeForKey:ikey] 
							dataForKey:GROUPINFO];
		free(groupnameTmp);
		if ([groupInfo checkArticleIsRead:
				(int)[headerInfo infoForKey:ARTICLE_NUM]]) {
		    [node setActive:NO];
		} else {
		    [node setActive:YES];
		}
	    }
	}
    }
    return listnode;
}

- _itemHeadersOf:knode
{
    id		newsGroup;
    id		listnode;
    id		node;
    id		article;
    id		headerInfo, groupInfo;
    int		i;
    const char	*newsgroupName;
//    char	groupnameTmp[256];
    char	*groupnameTmp;
    char	*message_id;
    id		image;
    char	*cline;

    newsGroup = [knode linkedData];    
    iArticleDB = [self initNewsGroup:newsGroup readFlag:(ReadFlag)irFlag];
    
    listnode = [[IOrderedListD allocFromZone:[self zone]]
					    initWithKey:"listnode"];	
    for (i=0; article=[iArticleDB objectAt:i]; ++i) {
	headerInfo = [article dataForKey:HEADER_INFO];
	message_id = [headerInfo infoForKey:MESSAGE_ID];
	/* if no message_id, bring up alert panel and skip it */
	if (message_id == NULL) {
	    NXRunAlertPanel(LoStr("NewsBase"),LoStr("no Message-ID field")
		,LoStr("OK"),NULL,NULL);
	    continue;
	}
	node = [[IUifNode allocFromZone:[self zone]] initWithKey:message_id];
	[listnode addObject:node];
	[node setTitleForCell:[headerInfo infoForKey:SUBJECT]];
	if ((cline=(char *)[headerInfo infoForKey:LINES]) != NULL) {
	    // article header has "Lines:", so set mark to cell
	    image = [self getArticleMarkFor:(int)atoi(cline)];
	    [node setImageForCell:image];
	}
	[node setLinkedData:article nodeType:Header];
        newsgroupName = [knode dataGroupName];
        [node setDataGroupName:newsgroupName];
        [node setLeaf:YES];
	
	/* check article is already read or not */
	//strncpy(groupnameTmp, newsgroupName, sizeof(groupnameTmp)-1);
	groupnameTmp = NXCopyStringBuffer(newsgroupName);
	makeTreeKey(groupnameTmp, ikey, ".");
	groupInfo=[[iNewsGroupTreeRoot nodeForKey:ikey] dataForKey:GROUPINFO];
	free(groupnameTmp);
	if ([groupInfo checkArticleIsRead:
				(int)[headerInfo infoForKey:ARTICLE_NUM]]) {
	    [node setActive:NO];
	} else {
	    [node setActive:YES];
	}
    }

    return listnode;
}

- getArticleMarkFor:(int)linenum
{
    id	image;
    
    if (linenum < iBoundaryForTwo) {
	image = [NXImage findImageNamed:SIZEMARKONE];
    } else if (iBoundaryForTwo <= linenum  && linenum < iBoundaryForThree) {
	image = [NXImage findImageNamed:SIZEMARKTWO];
    } else if (iBoundaryForThree <= linenum ) {
	image = [NXImage findImageNamed:SIZEMARKTHREE];
    } else {
	// linenum <= 0, and other
	image = NULL;
    }
    return image;
}
	
- itemOf:knode
{
    IOrderedListD *article;
    InfoD *header;
    const char *messageId;
    
    if ((article = [knode linkedData]) == nil) {
	return(nil);
    }
    if ((header = [article dataForKey:HEADER_INFO]) == nil) {
        return(nil);
    }
    messageId = (const char *)[header infoForKey:MESSAGE_ID];
    if ([self sendArticle:messageId] == YES) {
        return(self);
    } else {
        return(nil);
    }
}

- (BOOL)toggleActive:knode
{
    id		newsGroup, groupInfo;
    id		headerInfo;
    int		art_num;
    const char	*newsgroupName;
    char	*groupnameTmp;
    enum {ACTIVATE, NOTACTIVATE} activateAction;
    char	*xref, buf[MAXPATHLEN], *bufptr[MAX_NO_OF_TOKENS];
    char	groupNameBuf[MAXPATHLEN];
    int		i;
    NXTreeState	state;
    id		rootnode, tnode;

    DBG(1, fprintf(stderr,"INntpIO: -- toggleActive fired\n"));
    
    switch ([knode nodeType]) {
    /* message is coming from newsgroup browser */
    /* toggle subscribe */
    case DirOfItems:
	// selected cell is a newsgroup leaf cell
	DBG(10,fprintf(stderr,"-- toggle SUBSCRIBE"));
	
	groupInfo = [[knode linkedData] dataForKey:GROUPINFO];
	if ([knode active]) {
	    /* knode is active, subscribe -> unsubscribe */
	    [groupInfo addInfoString:"no" key:SUBSCRIBE];
	} else {
	    /* knode is not active, unsubscribe -> subscribe */
	    [groupInfo addInfoString:"yes" key:SUBSCRIBE];
	}
	return YES;
	break;
	
    case DirOfSubDirs:
	// selected cell is a directory of newsgroup
	// subscribe or unsubscribe all newsgroup under this directory

	activateAction = ([knode active]) ? NOTACTIVATE : ACTIVATE;

	rootnode = [knode linkedData];
	[rootnode initState:&state];
	while (tnode=[rootnode nextState:&state]) {
	    if (strcmp([tnode key], GROUPINFO) == 0) {
		// tnode is groupInfo for newsgroup 
		if (activateAction == NOTACTIVATE) {
		    [tnode addInfoString:"no" key:SUBSCRIBE];
		} else {
		    [tnode addInfoString:"yes" key:SUBSCRIBE];
		}
	    }
	}
	return YES;
	break;
	
    /* message is coming from article header browser */
    /* toggle article mark */
    case ReferenceGroup:
        // selected cell is a directory of article
	// knode is obj. of "ITreeNodeD"
	activateAction = ([knode active]) ? NOTACTIVATE : ACTIVATE;

	rootnode = [knode linkedData];
	[rootnode initState:&state];
	while (tnode=[rootnode nextState:&state]) {
	    if ([tnode isMemberOf:[IOrderedListD class]] &&
			((headerInfo=[tnode dataForKey:HEADER_INFO]) != nil)) {
		// tnode is article
		art_num = (int)[headerInfo infoForKey:ARTICLE_NUM];
		
		groupnameTmp = NXCopyStringBuffer([knode dataGroupName]);
		makeTreeKey(groupnameTmp, ikey, ".");
		newsGroup = [iNewsGroupTreeRoot nodeForKey:ikey];
		free(groupnameTmp);
		groupInfo = [newsGroup dataForKey:GROUPINFO];
		
		if (activateAction == NOTACTIVATE) {
		    // this directory cell is active so mark all article 
		    // as read
		    [groupInfo markReadArticle:art_num];
		} else {
		    [groupInfo unmarkReadArticle:art_num];
		}
	    }
	}
	return YES;
	break;
	
    case SubjectGroup:
    case KeywordGroup:
	// selected cell is a directory of article
	// knode is obj. of "IOrderedListD"
	
	activateAction = ([knode active]) ? NOTACTIVATE : ACTIVATE;
	rootnode = [knode linkedData];
	for (i=0; tnode=[rootnode objectAt:i]; ++i) {
	    if ([tnode isMemberOf:[IOrderedListD class]] &&
			((headerInfo=[tnode dataForKey:HEADER_INFO]) != nil)) {
		// tnode is article
		art_num = (int)[headerInfo infoForKey:ARTICLE_NUM];
		
		groupnameTmp = NXCopyStringBuffer([knode dataGroupName]);
		makeTreeKey(groupnameTmp, ikey, ".");
		newsGroup = [iNewsGroupTreeRoot nodeForKey:ikey];
		free(groupnameTmp);
		groupInfo = [newsGroup dataForKey:GROUPINFO];
		
		if (activateAction == NOTACTIVATE) {
		    // selected cell is active
		    // mark all article as read under this directory
		    [groupInfo markReadArticle:art_num];
		} else {
		    [groupInfo unmarkReadArticle:art_num];
		}
	    }
	}
	return YES;
	break;
	
    case Header:
	// selected cell is a leaf cell of article	
	DBG(1,fprintf(stderr,"-- toggle article mark"));
	
	headerInfo = [[knode linkedData] dataForKey:HEADER_INFO];
	// mark this article first
	newsgroupName = [knode dataGroupName];
	groupnameTmp = NXCopyStringBuffer(newsgroupName);
	makeTreeKey(groupnameTmp, ikey, ".");
	newsGroup = [iNewsGroupTreeRoot nodeForKey:ikey];
	free(groupnameTmp);

	groupInfo = [newsGroup dataForKey:GROUPINFO];

	// set this article's article number and mark or unmark it
	// activateAction will be used for Xref: things
	art_num = (int)[headerInfo infoForKey:ARTICLE_NUM];
	if ([groupInfo checkArticleIsRead:art_num]) {
	    /* article is already read, mark -> unmark */
	    [groupInfo unmarkReadArticle:art_num];
	    activateAction = NOTACTIVATE;
	} else {
	    [groupInfo markReadArticle:art_num];
	    activateAction = ACTIVATE;
	}
	
	// do Xref: things
	if ((xref=(char *)[headerInfo infoForKey:"Xref"]) == NULL) {
	    // this article does not have cross reference
	    return YES;
	} else {
	    // article has Xref:
	    strncpy(buf, xref, sizeof(buf));
	    makeTreeKey(buf, bufptr, " ");
	    
	    for (i=1; bufptr[i] != NULL; ++i) {
		// skip first one (xxxxx)
		//   assume that Xref: field is like
		//        xxxxx aaa.aaa.aaa:nnn bbb.bbb.bbb:mmm .....
		//        xxxxx: e.g. ISRNEWS
		//        aaa.aaa.aaa, bbb.bbb.bbb: newsgroup
		//        nnn, mmm: article number
		
		if (sscanf(bufptr[i],"%1023[^:]:%d",groupNameBuf,&art_num)==2){
		    // buf is newsgroup name
		    DBG(1, fprintf(stderr,"newsgroup=%s\t art_num=%d\n",
						    groupNameBuf, art_num));
		    makeTreeKey(groupNameBuf, ikey, ".");
		    newsGroup = [iNewsGroupTreeRoot nodeForKey:ikey];
		    groupInfo = [newsGroup dataForKey:GROUPINFO];
		    if (activateAction == NOTACTIVATE) {
			[groupInfo unmarkReadArticle:art_num];
		    } else {
			[groupInfo markReadArticle:art_num];
		    }
		}
	    }
	    return YES;
	}
	break;
    default:
	// should never coming here
	return NO;
	break;
    }
    return NO;
}

- (BrowserMode)toggleNewsGroupMode:sender
{
    switch ((BrowserMode)[[sender selectedCell] tag]) {
    case Flat:
	iNewsGroupMode = Flat;
	break;
    case Tree:
    default:
	iNewsGroupMode = Tree;
	break;
    }
    return iNewsGroupMode;
}

- (BrowserMode)toggleArticleMode:sender
{
    switch ((BrowserMode)[[sender selectedCell] tag]) {
    case Flat:
	iArticleMode = Flat;
        break;
    case Tree:
	iArticleMode = Tree;
        break;
    case BySubject:
	iArticleMode = BySubject;
        break;
    case ByKeyword:
	iArticleMode = ByKeyword;
        break;
    }
    return(iArticleMode);
}

- setArticleMode:(BrowserMode)articleMode
{
    iArticleMode = articleMode;
    return(self);
}

- setReadFlag:(ReadFlag)rflag
{
    irFlag = rflag;
    return self;
}

- _makeArticleTree
{
    int		i;
    id		articleItem;

    iArticleHashTable = [[HashTable allocFromZone:headersZone] 
					    initKeyDesc:"*" valueDesc:"@"];
    iArticleReferenceTreeRoot = [[ITreeNodeD allocFromZone:headersZone] 
    							initWithKey:"root"];
    
    for (i=0; articleItem=[iArticleDB objectAt:i]; ++i) {
        [iArticleHashTable insertKey:[[articleItem dataForKey:HEADER_INFO]
				    infoForKey:MESSAGE_ID] value:articleItem];
    }

    [self _addArticleToTree];
    return iArticleReferenceTreeRoot;
}

- (void)_addArticleToTree
{
#define TREELISTSIZE 128
    char	treeKeyList[TREELISTSIZE];
    char	treeKeyListTmp[TREELISTSIZE];
    const char 	*mes_id;
    id		node;
    NXHashState	state;
    id		articleItem;
    id		org_article;
    char	*org_art_messageID;
    int		org_art_num;
    char	*key[MAX_NO_OF_TOKENS];

    state = [iArticleHashTable initState];
    
    while ([iArticleHashTable nextState:&state 
    		key:(const void **)&mes_id value:(void **)&articleItem]) {
	/* look for original article's message_id */
	if ((org_art_messageID=[self findOriginalArticle:articleItem])==NULL) {
	    /* original article */
	    sprintf(treeKeyList, "%010d", 
    	  (int)[[articleItem dataForKey:HEADER_INFO] infoForKey:ARTICLE_NUM]);
	} else {
	    /* article has reference */
	    if ((org_article = [iArticleHashTable 
    			valueForKey:(char *)org_art_messageID]) == NULL) {
		/* if original aritlce is not in server, 
				key = "0000000000_messageID 0000000yyy" */
		sprintf(treeKeyList, "0000000000_%.105s %010d",
		    (char *)org_art_messageID, 
		    (int)[[articleItem dataForKey:HEADER_INFO] 
	  					infoForKey:ARTICLE_NUM]);
	    } else {
		/* if original article is in server, 
		    			key= "0000000xxx 0000000yyy" */
		org_art_num = (int)[[org_article dataForKey:HEADER_INFO]
	    					infoForKey:ARTICLE_NUM];
		sprintf(treeKeyList, "%010d %010d", org_art_num,
    	  			(int)[[articleItem dataForKey:HEADER_INFO] 
	  			infoForKey:ARTICLE_NUM]);
	    }
	    DBG(10,fprintf(stderr,"treeKeyList = %s\n org_subject = %s\n"
			"Subject = %s", treeKeyList, 
	   (char *)[[org_article dataForKey:HEADER_INFO] infoForKey:SUBJECT],
	   (char *)[[articleItem dataForKey:HEADER_INFO] infoForKey:SUBJECT]));
	}
	strncpy(treeKeyListTmp, treeKeyList, sizeof(treeKeyListTmp)-1);
	makeTreeKey(treeKeyListTmp, key, " ");
        node = [iArticleReferenceTreeRoot addNodeForKey:key];
	[node insertKeyedObject:articleItem];
    }
}

- (char *)findOriginalArticle:articleItem
{
    char	*headRef, *references;
    char	*ch;
    
    if (([[articleItem dataForKey:HEADER_INFO] 
    					infoForKey:REFERENCES]) == NULL) {
	/* if not responsed article */
	return NULL;
    }

    do {
	if((references=[[articleItem dataForKey:HEADER_INFO] 
				infoForKey:REFERENCES]) == NULL) {
	    return headRef;
	}
	headRef = references;
        if ((ch=strchr(headRef,' ')) != NULL) {
		*ch = '\0';
	} else {
	    /* if not ' ' in headRef, only one message id
	       is in references field , and that's original article */
	    return headRef;
	}
    } while (articleItem=[iArticleHashTable valueForKey:headRef]);
    
    return headRef;
}

- (BrowserMode)newsGroupMode
{
    return iNewsGroupMode;
}

- (BrowserMode)articleMode
{
    return(iArticleMode);
}

- (IOrderedListD *)_subjectHeadersOf:(IUifNode *)newsgroupNode
{
    int 		i, k;
    IOrderedListD	*articleItem;
    InfoD		*header;
    const char		*subject;
    int			articleNo;
    char		articleNoString[16];
    IOrderedListD	*listnode;
    HashTable		*tableOfSubjectGroups;
    NXHashState 	tableState;
    IOrderedListD	*subjectGroup;
    IUifNode		*subjectNode;
    IUifNode		*articleNode;
    const char		*newsgroupName;
    char		*groupnameTmp;
    char		*cline;
    id			image;
    BOOL		isUnreadArticle;
    INewsgroupInfoD	*groupInfo;
    id			c_article, c_header;

    iArticleDB = [self initNewsGroup:[newsgroupNode linkedData]
        readFlag:(ReadFlag)irFlag];
    // use a temporary hashtable to map subjects to subject groups
    tableOfSubjectGroups = [[HashTable allocFromZone:headersZone]
        initKeyDesc:"*" valueDesc:"@"];
    for (i = 0; (articleItem = [iArticleDB objectAt:i]) != nil; ++i) {
        header = [articleItem objectWithKey:HEADER_INFO];
        subject = [header infoForKey:SUBJECT];
        // strip Re: from subject if present
        if (strncmp(subject, "Re:", 3) == 0 || strncmp(subject, "re:", 3)
            == 0 || strncmp(subject, "RE:", 3) == 0) {
            subject += 3;
        }
        if (subject[0] == ' ') {
            ++subject;
        }
        articleNo = (int)[header infoForKey:ARTICLE_NUM];
        sprintf(articleNoString, "%010d", articleNo);
        if ((subjectGroup = [tableOfSubjectGroups valueForKey:subject])
            == nil) {
            // create a subject group for this subject
            subjectGroup = [[IOrderedListD allocFromZone:headersZone]
                initWithKey:subject];
            [tableOfSubjectGroups insertKey:[subjectGroup key]
                value:subjectGroup];
        }
        // add article to this subject group
        [subjectGroup insertKeyedObject:(IKeyedObject *)articleItem];
    }

    newsgroupName = [newsgroupNode dataGroupName];
    groupnameTmp = NXCopyStringBuffer(newsgroupName);
    makeTreeKey(groupnameTmp, ikey, ".");
    groupInfo = [[iNewsGroupTreeRoot nodeForKey:ikey] dataForKey:GROUPINFO];
    free (groupnameTmp);
    
    tableState = [tableOfSubjectGroups initState];
    listnode = [[IOrderedListD allocFromZone:[self zone]] initWithKey:""];
    while ([tableOfSubjectGroups nextState:&tableState
        key:(const void **)&subject value:(void **)&subjectGroup]) {
        articleItem = [subjectGroup objectAt:0];
        header = [articleItem objectWithKey:HEADER_INFO];
        articleNo = (int)[header infoForKey:ARTICLE_NUM];
        sprintf(articleNoString, "%010d", articleNo);
        if ([subjectGroup count] == 1) {
            // create articleNode and link to article
            articleNode = [[IUifNode allocFromZone:[self zone]]
                initWithKey:articleNoString];
            [articleNode setLinkedData:articleItem nodeType:Header];
            // use subject as title
            [articleNode setTitleForCell:[header infoForKey:SUBJECT]];
	    // set image for line num
	    if ((cline=(char *)[header infoForKey:LINES]) != NULL) {
	        // article header has "Lines:", so set mark to cell
	        image = [self getArticleMarkFor:(int)atoi(cline)];
	        [articleNode setImageForCell:image];
	    }
            [articleNode setDataGroupName:newsgroupName];
            [articleNode setLeaf:YES];
            if ([groupInfo checkArticleIsRead:articleNo] == YES) {
                [articleNode setActive:NO];
            } else {
                [articleNode setActive:YES];
            }
            [listnode insertKeyedObject:articleNode];
            [subjectGroup removeObject:articleItem];
            [subjectGroup free];
        } else {
            // More than one article in this subject group so create a subject
            // node for this subject and link to subject group
            subjectNode = [[IUifNode allocFromZone:[self zone]]
                initWithKey:articleNoString];
            [subjectNode setLinkedData:subjectGroup nodeType:SubjectGroup];
            [subjectNode setTitleForCell:subject];
            [subjectNode setDataGroupName:newsgroupName];
	    [subjectNode setLeaf:NO];
	    
	    // check if there is unread article 
	    isUnreadArticle = NO;
	    for (k=0; c_article=[subjectGroup objectAt:k]; ++k) {
		if ([c_article isMemberOf:[IOrderedListD class]] &&
		    ((c_header=[c_article dataForKey:HEADER_INFO]) != nil)) {
		    if ([groupInfo checkArticleIsRead: 
			(int)[c_header infoForKey:ARTICLE_NUM]] == NO) {
			// there is an unread article 
			isUnreadArticle = YES;
			break;
		    }
		}
	    }
	    if (isUnreadArticle == YES) {
		[subjectNode setActive:YES];
	    } else {
		[subjectNode setActive:NO];
	    }
            [listnode insertKeyedObject:subjectNode];
        }
    }
    [tableOfSubjectGroups free];
    return(listnode);
}

- _itemHeadersOfSubject:(IUifNode *)subjectNode
{
    IOrderedListD       *subjectGroup;
    int                 i;
    IOrderedListD       *articleItem;
    InfoD               *header;
    int                 articleNo;
    char                articleNoString[16];
    IOrderedListD       *listnode;
    IUifNode            *articleNode;
    const char          *newsgroupName;
//    char		groupnameTmp[256];
    char		*groupnameTmp;
    char		*cline;
    id			image;

    subjectGroup = [subjectNode linkedData];
    listnode = [[IOrderedListD allocFromZone:[self zone]] initWithKey:""];
    for (i = 0; (articleItem = [subjectGroup objectAt:i]) != nil; ++i) {
        header = [articleItem objectWithKey:HEADER_INFO];
        articleNo = (int)[header infoForKey:ARTICLE_NUM];
        sprintf(articleNoString, "%010d", articleNo);
        // create articleNode and link to article
        articleNode = [[IUifNode allocFromZone:[self zone]]
            initWithKey:articleNoString];
        [articleNode setLinkedData:articleItem nodeType:Header];
        // use sender as title
        if ([subjectNode nodeType] == KeywordGroup) {
            [articleNode setTitleForCell:[header infoForKey:SUBJECT]];
        } else {
            [articleNode setTitleForCell:[header infoForKey:FROM]];
        }
	// set image by line num
	if ((cline=(char *)[header infoForKey:LINES]) != NULL) {
	    // article header has "Lines:", so set mark to cell
	    image = [self getArticleMarkFor:(int)atoi(cline)];
	    [articleNode setImageForCell:image];
	}
        newsgroupName = [subjectNode dataGroupName];
        [articleNode setDataGroupName:newsgroupName];
        [articleNode setLeaf:YES];
        // add article node to subject group
	//strncpy(groupnameTmp, newsgroupName, sizeof(groupnameTmp)-1);
	groupnameTmp = NXCopyStringBuffer(newsgroupName);
        makeTreeKey(groupnameTmp, ikey, ".");
        if ([[[iNewsGroupTreeRoot nodeForKey:ikey] dataForKey:GROUPINFO]
            checkArticleIsRead:articleNo] == YES) {
            [articleNode setActive:NO];
        } else {
            [articleNode setActive:YES];
            // also make the subject group active
            [subjectNode setActive:YES];
        }
	free(groupnameTmp);
        [listnode insertKeyedObject:articleNode];
    }
    return(listnode);
}

#define KEYWORD_LIST_SIZE 4096

- (IOrderedListD *)_keywordHeadersOf:(IUifNode *)newsgroupNode
{
    const char		*newsgroupName;
    int 		i;
    IOrderedListD	*articleItem;
    InfoD		*header;
    const char 		*keywords;
    char		keywordList[KEYWORD_LIST_SIZE], *ptr;
    const char 		*keyword;
    HashTable		*tableOfKeywordGroups;
    IOrderedListD	*keywordGroup;
    IOrderedListD	*listnode;
    IUifNode            *keywordNode;

    iArticleDB = [self initNewsGroup:[newsgroupNode linkedData]
        readFlag:(ReadFlag)irFlag];
    newsgroupName = [newsgroupNode dataGroupName];
    // use a temporary hashtable to map subjects to subject groups
    tableOfKeywordGroups = [[HashTable allocFromZone:headersZone]
        initKeyDesc:"*" valueDesc:"@"];
    listnode = [[IOrderedListD allocFromZone:[self zone]] initWithKey:""];
    for (i = 0; (articleItem = [iArticleDB objectAt:i]) != nil; ++i) {
        header = [articleItem objectWithKey:HEADER_INFO];
        strncpy(keywordList, [header infoForKey:SUBJECT], sizeof(keywordList));
        keywordList[sizeof(keywordList) - 1] = '\0';
        if ((keywords = [header infoForKey:KEYWORDS]) != NULL) {
            ptr = index(keywordList, '\0');
            *ptr++ = ' ';
            strncpy(ptr, keywords, &keywordList[sizeof(keywordList)]
                - ptr);
            keywordList[sizeof(keywordList) - 1] = '\0';
        }
        for (ptr = keywordList; *ptr != '\0'; ++ptr) {
            if (isalnum(*ptr) == 0) {
                *ptr = ' ';
            } else if (isupper(*ptr) != 0) {
                *ptr = tolower(*ptr);
            }
        }
        for (keyword = strtok(keywordList, " "); keyword != NULL;
            keyword = strtok(NULL, " ")) {
            if (keyword[1] == '\0') {
                continue;
            }
            if ([tableOfNoiseWords isKey:keyword] == YES) {
                continue;
            }
            if ((keywordGroup = [tableOfKeywordGroups valueForKey:keyword])
                == nil) {
                // create a keyword group for this keyword
                keywordGroup = [[IOrderedListD allocFromZone:headersZone]
                    initWithKey:keyword];
                [tableOfKeywordGroups insertKey:[keywordGroup key]
                    value:keywordGroup];
                keywordNode = [[IUifNode allocFromZone:[self zone]]
                    initWithKey:keyword];
                [keywordNode setLinkedData:keywordGroup nodeType:KeywordGroup];
                [keywordNode setTitleForCell:keyword];
                [keywordNode setDataGroupName:newsgroupName];
                [keywordNode setActive:YES];
                [listnode insertKeyedObject:keywordNode];
            }
            // add article to this keyword group
            [keywordGroup insertKeyedObject:(IKeyedObject *)articleItem];
        }
    }
    [tableOfKeywordGroups free];
    return(listnode);
}

@end

