/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * The contents of this file are subject to the Netscape Public License
 * Version 1.0 (the "NPL"); you may not use this file except in
 * compliance with the NPL.  You may obtain a copy of the NPL at
 * http://www.mozilla.org/NPL/
 *
 * Software distributed under the NPL is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the NPL
 * for the specific language governing rights and limitations under the
 * NPL.
 *
 * The Initial Developer of this code under the NPL is Netscape
 * Communications Corporation.  Portions created by Netscape are
 * Copyright (C) 1998 Netscape Communications Corporation.  All Rights
 * Reserved.
 */

#include "bkmks.h"
#include "net.h"
#include "xp_mcom.h"
#include "client.h"
#include "msgcom.h"
#include "undo.h"
#include "xp_hash.h"
#include "xpgetstr.h"
#include "glhist.h"
#include "libi18n.h"
#include "xp_qsort.h"
#include "intl_csi.h"

/* N.B. If you add code to this file, make sure it still builds on win16 debug - its 
   code segment is too big.
*/

#define INTL_SORT 1
#ifdef INTL_SORT	/* Added by ftang */
#include "xplocale.h"
#endif

extern int MK_OUT_OF_MEMORY;

extern int XP_BKMKS_BOOKMARKS_CHANGED;
extern int XP_BKMKS_ADDRESSBOOK_CHANGED;
extern int XP_BKMKS_BOOKMARKS_CONFLICT;
extern int XP_BKMKS_ADDRESSBOOK_CONFLICT;
extern int XP_BKMKS_CANT_WRITE_ADDRESSBOOK;
extern int XP_BKMKS_CANT_WRITE_BOOKMARKS;

/* added by L10N team */
extern int XP_BKMKS_HOURS_AGO;
extern int XP_BKMKS_DAYS_AGO;
extern int XP_BKMKS_COUNTALIASES_MANY;
extern int XP_BKMKS_COUNTALIASES_ONE;
extern int XP_BKMKS_COUNTALIASES_NONE;
extern int XP_BKMKS_INVALID_NICKNAME;
extern int XP_BKMKS_NICKNAME_ALREADY_EXISTS;
extern int XP_BKMKS_REMOVE_THIS_ITEMS_ALIASES;
extern int XP_BKMKS_REMOVE_SOME_ITEMS_ALIASES;
extern int XP_BKMKS_AUTOGENERATED_FILE;
extern int XP_BKMKS_READ_AND_OVERWRITE;
extern int XP_BKMKS_DO_NOT_EDIT;
extern int XP_BKMKS_NEW_HEADER;
extern int XP_BKMKS_NEW_BOOKMARK;
extern int XP_BKMKS_NOT_FOUND;
extern int XP_BKMKS_OPEN_BKMKS_FILE;
extern int XP_BKMKS_IMPORT_BKMKS_FILE; 
extern int XP_BKMKS_IMPORT_ADDRBOOK;
extern int XP_BKMKS_SAVE_BKMKS_FILE;
extern int XP_BKMKS_SAVE_ADDRBOOK;
extern int XP_BKMKS_LESS_THAN_ONE_HOUR_AGO;


extern int XP_BKMKS_SOMEONE_S_BOOKMARKS;	/* "%s%s's Bookmarks%s" */
extern int XP_BKMKS_PERSONAL_BOOKMARKS;		/* "%sPersonal Bookmarks%s" */
extern int XP_BKMKS_SOMEONE_S_ADDRESSBOOK;	/* "%s%s's Addressbook%s" */
extern int XP_BKMKS_PERSONAL_ADDRESSBOOK;	/* "%sPersonal Addressbook%s" */

extern int XP_BKMKS_BOOKMARK;	
extern int XP_BKMKS_ENTRY;	
extern int XP_BKMKS_SECONDS;	
extern int XP_BKMKS_MINUTES;	
extern int XP_BKMKS_HOURS_MINUTES;	

extern int XP_BKMKS_HEADER;
extern int XP_ADDRBOOK_HEADER;


#define SECONDS_PER_DAY		86400L
#define BMLIST_COOKIE			"<!DOCTYPE NETSCAPE-Bookmark-file-1>"
#define BM_ADDR_LIST_COOKIE		"<!DOCTYPE NETSCAPE-Addressbook-file-1>"
#define READ_BUFFER_SIZE		2048
#define DEF_NAME				"Personal Bookmarks" /* L10N? This doesn't seem to be used. */

#ifdef FREEIF
#undef FREEIF
#endif
#define FREEIF(obj) do { if (obj) { XP_FREE (obj); obj = 0; }} while (0)

#define BM_ATTR_FOLDED		0x0001
#define BM_ATTR_SELECTED	0x0002
#define BM_ATTR_ISNEW		0x0004
#define BM_ATTR_FINDAFF		0x0008 /* This entry is temporarily unfolded only
									  as a result of the last find
									  operation. */
#define BM_ATTR_HASALIASES	0x0010 /* Actually, being set only means that
									  this entry *might* have aliases.  But
									  if an entry has aliases, this bit is
									  definitely set. */
#define BM_ATTR_MARKED		0x0020 /* Random bit to temporarily mark a bunch of
									  items. */
#define BM_ATTR_CHECKING	0x0040 /* In the midst of checking whether this
									  entry has changed. */


#define BM_ISHEADER(bmStruct) \
	((bmStruct) && ((bmStruct)->type == BM_TYPE_HEADER))

#define BM_ISURL(bmStruct) \
	((bmStruct) && ((bmStruct)->type == BM_TYPE_URL))

#define BM_ISADDRESS(bmStruct) \
	((bmStruct) && ((bmStruct)->type == BM_TYPE_ADDRESS))

#define BM_ISSEPARATOR(bmStruct) \
	((bmStruct) && ((bmStruct)->type == BM_TYPE_SEPARATOR))

#define BM_ISALIAS(bmStruct) \
	((bmStruct) && ((bmStruct)->type == BM_TYPE_ALIAS))

#define BM_ISFOLDED(bmStruct) \
	((bmStruct) && ((bmStruct)->flags & BM_ATTR_FOLDED))

#define BM_ISSELECTED(bmStruct) \
	((bmStruct) && ((bmStruct)->flags & BM_ATTR_SELECTED))

#define BM_SETFLAG(bmStruct, flag) \
	((bmStruct) ? ((bmStruct)->flags |= (flag)) : 0)

#define BM_CLEARFLAG(bmStruct, flag) \
	((bmStruct) ? ((bmStruct)->flags &= ~(flag)) : 0)

static int32 g_iNaturalIndexPool = 0;

struct BM_Entry_struct
{
  BM_Type type;

  uint16 flags;
  BM_Entry*	next;
  BM_Entry*	parent;
  char* name;
  char* description;
  BM_Date addition_date;
  int32 iNaturalIndex;          /* Index for user arranged "sort" */
  char* nickname;				/* Used only by address book, alas. */

  union {
	struct BM_Header {
	  char* target;			/* target */
	  BM_Entry*	children;			/* a linked list of my children */
	  uint32 childCount;			/* the number of "children" */
	  BM_Entry*	lastChild;			/* the last child in "children" */
	} header;
	struct BM_Url {
	  char* address;			/* address */
	  char* target;			/* target */
	  char* content_type;		/* content-type */
	  BM_Date last_visit;		/* when last visited */
	  BM_Date last_modified;	/* when the contents of the URL was last
								   modified. */
	} url;
	struct BM_Address {
	  char* address;			/* e-mail address */
	} address;
	struct BM_Alias {
	  BM_Entry*	original;		/* the original bm */
	} alias;
  } d;
};


typedef struct BM_Frame {
	BM_Entry*		gBookmarks;			/* root of the tree */
	XP_Bool			gBookmarksModified; /* TRUE if the tree is modified */
	int32			gCount;				/* number of entries in tree.  If
										   negative, then need to be
										   recalculated. */
	int32			gVisCount;			/* number of visible entries.  If
										   negative, then need to be
										   recalculated. */
	int32			gSelectionCount;	/* number of selected items.  If
										   negative, then we're out of sync,
										   call bm_SyncSelection to
										   synchronize.  */
	uint32			gSelectionMask;		/* what types of items are selected
										   (invalid if gSelectionCount is
										   negative.)*/
	BM_FindInfo*	gFindInfo;			/* information for the find dialog */
	char*			gFile;				/* the file this data is from */
	void*			gTemporary;
	void*			feData;
    int32			max_depth;	/* The number of levels in the heirarchy that
								   we currently display.  (If zero, then we
								   don't know. */
    UndoState*		undo;
    MWContext* next;			/* Next bookmarks context in master linked list. */
	XP_Bool unfoldedForFind;	/* TRUE if some headers have been unfolded
								   as the result of a find operation.  The
								   headers in question will have the
								   BM_ATTR_FINDAFF flag set.*/

    BM_SortType     enSortType;         /* the sort field (a column or natural) */
    XP_Bool         bSorting;
	XP_HashTable aliasTable;
	XP_HashTable nicknameTable;
    int aliasID;
	XP_Bool errorSavingBookmarks;

  int32 batch_depth;
  int32 first_update_line;
  int32 last_update_line;

  void* savetimer;

  BM_Entry* lastSelectedItem;

  BM_Entry* menuheader;			/* Which header to use as the menu bar. */
  BM_Entry* addheader;			/* Which entry to add new items into. */

  XP_StatStruct laststat;		/* Stat of the file when we loaded it in. */



  struct BM_WhatsChangedInfo {
	int32 total;
	int32 numreached;
	int32 numchanged;
	time_t starttime;
  } whatschanged;

} BM_Frame;


#define CHKCONTEXT(context)											   \
XP_ASSERT(context != NULL && (context->type == MWContextAddressBook || \
							  context->type == MWContextBookmarks) &&  \
		  context->bmframe != NULL);								   \
if (!context || (context->type != MWContextAddressBook &&			   \
				 context->type != MWContextBookmarks) ||			   \
	!context->bmframe) return 0;

#define CHKCONTEXTVOID(context)										   \
XP_ASSERT(context != NULL && (context->type == MWContextAddressBook || \
							  context->type == MWContextBookmarks) &&  \
		  context->bmframe != NULL);								   \
if (!context || (context->type != MWContextAddressBook &&			   \
				 context->type != MWContextBookmarks) ||			   \
	!context->bmframe) return;


/* Should probably be a macro, but putting the assert in the macro and
   making this still be easy to use is just too painful. */
static BM_Frame*
GETFRAME(MWContext* context)
{
  XP_ASSERT(context &&
			(context->type == MWContextBookmarks ||
			 context->type == MWContextAddressBook) &&
			context->bmframe != NULL);
  return (context && (context->type == MWContextBookmarks ||
					  context->type == MWContextAddressBook))
	? context->bmframe : NULL;
}


static void bm_CancelLastFind(MWContext* context);
static void bm_InsertItemAfter(MWContext* context, BM_Entry* insert_after,
							   BM_Entry* insertee, XP_Bool sync);
static void bm_SortSelected(MWContext* context, BM_SortType enSortType);
static void bm_SyncCount(MWContext* context);
static void bm_AddChildToHeaderSorted(MWContext* context, BM_Entry* parent,
									  BM_Entry* child);
static void bm_SortSilent(MWContext* context, BM_SortType enSortType );



XP_Bool
BM_IsHeader(BM_Entry* entry)
{
  return BM_ISHEADER(entry);
}

XP_Bool
BM_IsUrl(BM_Entry* entry)
{
  return BM_ISURL(entry);
}

XP_Bool
BM_IsAddress(BM_Entry* entry)
{
  return BM_ISADDRESS(entry);
}

XP_Bool
BM_IsSeparator(BM_Entry* entry)
{
  return BM_ISSEPARATOR(entry);
}

XP_Bool
BM_IsAlias(BM_Entry* entry)
{
  return BM_ISALIAS(entry);
}

XP_Bool
BM_IsFolded(BM_Entry* entry)
{
  return BM_ISFOLDED(entry);
}

XP_Bool
BM_IsSelected(BM_Entry* entry)
{
  return BM_ISSELECTED(entry);
}


int32
BM_GetChangedState(BM_Entry* entry)
{
  if (BM_ISALIAS(entry)) {
	entry = entry->d.alias.original;
  }
  if (BM_ISURL(entry)) {
	if (entry->d.url.last_modified == 0 || entry->flags & BM_ATTR_CHECKING) {
	  return BM_CHANGED_UNKNOWN;
	}
	if (entry->d.url.last_visit < entry->d.url.last_modified) {
	  return BM_CHANGED_YES;
	}
  }
  return BM_CHANGED_NO;
}

/* globals */
static MWContext* ContextList = NULL;


void
BM_SetFEData(MWContext* context, void* data)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXTVOID(context);
  f->feData = data;
}

void*
BM_GetFEData(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXT(context);
  return f->feData;
}


/* creates a new empty bookmark entry */
static BM_Entry*
bm_NewEntry(int16 type)
{
  BM_Entry* new_entry = XP_NEW_ZAP(BM_Entry);
  if (!new_entry) return NULL;
  XP_MEMSET(new_entry, 0, sizeof(BM_Entry));
  new_entry->type = type;
  return new_entry;
}

/* creates a new header bookmarks entry */
BM_Entry*
BM_NewHeader(const char* name)
{
  BM_Entry* header;
  header = bm_NewEntry(BM_TYPE_HEADER);
  if (!header) return NULL;
  StrAllocCopy(header->name, name);
  return header;
}

/* creates a new URL bookmarks entry */
BM_Entry*
BM_NewUrl(const char* name, const char* address, const char* content_type,
		   BM_Date last_visit)
{
  BM_Entry* url;

  url = bm_NewEntry(BM_TYPE_URL);

  if (!url) return NULL;

  StrAllocCopy(url->name, name);
  url->description = NULL;
  StrAllocCopy(url->d.url.address, address);
  StrAllocCopy(url->d.url.content_type, content_type);
  url->d.url.last_visit = last_visit;
  url->d.url.last_modified = last_visit; /* ### Is this right? */
  url->addition_date = 0;

  return url;
}

/* creates a new URL bookmarks entry */
static BM_Entry*
bm_NewAddress(const char* name, const char* address)
{
    BM_Entry*     add_struct;

    add_struct = bm_NewEntry(BM_TYPE_ADDRESS);

    if (!add_struct) return NULL;

    StrAllocCopy(add_struct->name, name ? name : "");
    StrAllocCopy(add_struct->d.address.address, address ? address : "");

    return add_struct;
}

/* creates a new separator bookmarks entry */
static BM_Entry*
bm_NewSeparator(void)
{
  return bm_NewEntry(BM_TYPE_SEPARATOR);
}

/* creates a new alias bookmarks entry */
static BM_Entry*
bm_NewAlias(BM_Entry* original)
{
  BM_Entry* alias;
  alias = bm_NewEntry(BM_TYPE_ALIAS);
  alias->d.alias.original = original;
  BM_SETFLAG(original, BM_ATTR_HASALIASES);
  return alias;
}

extern BM_Entry* BM_CopyBookmark(MWContext* context, BM_Entry* original)
{
	BM_Entry* copy;
	BM_Entry* child;
	BM_Entry* childCopy;

	copy = NULL;

	if(!original)
		return NULL;

	switch (original->type) {
		case BM_TYPE_URL:
			copy = BM_NewUrl(original->name, original->d.url.address,
							 original->d.url.content_type, original->d.url.last_visit);
			break;
		case BM_TYPE_ADDRESS:
			copy = bm_NewAddress(original->name, original->d.address.address);
			break;
		case BM_TYPE_SEPARATOR:
			copy = bm_NewSeparator();
			break;
		case BM_TYPE_ALIAS:
			copy = bm_NewAlias(original->d.alias.original);
			break;
		case BM_TYPE_HEADER:
			copy = BM_NewHeader(original->name);

			child = original->d.header.children;

			while (child) {
				childCopy = BM_CopyBookmark(context, child);
				if(childCopy)
					BM_AppendToHeader(context, copy, childCopy);
				child = child->next;
			}
			break;
	}

	if(copy)
	{
			StrAllocCopy(copy->description, original->description);
			StrAllocCopy(copy->nickname, original->nickname);
	}
	return copy;
}

/* gets the top node of the bmlist */
BM_Entry*
BM_GetRoot(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXT(context);
  if (!f) return NULL;
  if (!f->gBookmarks) {
	f->gBookmarks = BM_NewHeader(context->type == MWContextBookmarks ?
		XP_GetString(XP_BKMKS_HEADER) : XP_GetString(XP_ADDRBOOK_HEADER));
	if (context->type == MWContextBookmarks) {
	  f->menuheader = f->addheader = f->gBookmarks;
	}
	bm_SyncCount(context);
  } else {
	if (f->gBookmarks->parent != NULL)
	  XP_ASSERT(f->gBookmarks->next == NULL); /* Stupid consistancy test; 
											     dunno what action to take if
											     this isn't so. */
  }
  return f->gBookmarks;
}

static void
bm_EachEntryDo_1(MWContext* context, BM_Entry* at, EntryFunc func, void* closure)
{
  BM_Entry* nextChild;
  BM_Entry* children;

  while (at) {
	nextChild = at->next;
	if (BM_ISHEADER(at)) {
	  children = at->d.header.children;
	} else {
	  children = NULL;
	}

	(*func)(context, at, closure);

	if (children) {
	  bm_EachEntryDo_1(context, children, func, closure);
	}

	at = nextChild;
  }
}

static void
bm_EachSelectedEntryDo_1(MWContext* context, BM_Entry* at, EntryFunc func,
						 void* closure)
{
  BM_Entry* nextChild;
  BM_Entry* children;

  while (at) {
	nextChild = at->next;
	if (BM_ISHEADER(at)) {
	  children = at->d.header.children;
	} else {
	  children = NULL;
	}

	if (BM_ISSELECTED(at)) (*func)(context, at, closure);

	if (children) {
	  bm_EachSelectedEntryDo_1(context, children, func, closure);
	}

	at = nextChild;
  }
}

static void
bm_EachProperSelectedEntryDo_1(MWContext* context, BM_Entry* at, EntryFunc func,
							   void* closure, struct BM_Entry_Focus* bmFocus)
{
  BM_Entry* child;
  BM_Entry* nextChild;

  XP_ASSERT(at);
  if (!at) return;

  XP_ASSERT(BM_ISHEADER(at));
  child = at->d.header.children;

  while (child) {
	nextChild = child->next;
	switch (child->type) {
	case BM_TYPE_URL:
	case BM_TYPE_ADDRESS:
	case BM_TYPE_SEPARATOR:
	case BM_TYPE_ALIAS:
	  if (BM_ISSELECTED(child)) {
		if ((bmFocus != NULL) && !bmFocus->foundSelection)
  		  bmFocus->foundSelection = TRUE;
		(*func)(context, child, closure);
	  }
	  else if (bmFocus && !bmFocus->foundSelection) {
		bmFocus->saveFocus = child;
	  }
	  break;
	case BM_TYPE_HEADER:
	  if (BM_ISSELECTED(child)) {
		if ((bmFocus != NULL) && !bmFocus->foundSelection)
  		  bmFocus->foundSelection = TRUE;
		(*func)(context, child, closure);
	  } 
	  else {
		if (bmFocus && !bmFocus->foundSelection)
		  bmFocus->saveFocus = child;
		if (! BM_ISFOLDED(child)) {
		  bm_EachProperSelectedEntryDo_1(context, child, func, closure, bmFocus);
		}
	  }
	  break;
	}
	child = nextChild;
  }
}



void
BM_EachEntryDo(MWContext* context, EntryFunc func, void* closure)
{
  CHKCONTEXTVOID(context);
  bm_EachEntryDo_1(context, BM_GetRoot(context), func, closure);
}

void
BM_EachSelectedEntryDo(MWContext* context, EntryFunc func, void* closure)
{
  CHKCONTEXTVOID(context);
  bm_EachSelectedEntryDo_1(context, BM_GetRoot(context), func, closure);
}

void
BM_EachProperSelectedEntryDo(MWContext* context, EntryFunc func, void* closure,
  struct BM_Entry_Focus* bmFocus)
{
  CHKCONTEXTVOID(context);
  bm_EachProperSelectedEntryDo_1(context, BM_GetRoot(context), func, closure,
	bmFocus);
}



static void
bm_ClearMarkEverywhere_1(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_CLEARFLAG(entry, BM_ATTR_MARKED);
}


static void 
bm_ClearMarkEverywhere(MWContext* context)
{
  BM_EachEntryDo(context, bm_ClearMarkEverywhere_1, NULL);
}


static MWContext*
bm_GetContextForEntry(BM_Entry* entry)
{
  MWContext* result;
  BM_Frame* f;
  while (entry->parent) entry = entry->parent;
  for (result = ContextList ; result ; result = f->next) {
	f = GETFRAME(result);
	if (f->gBookmarks == entry) return result;
  }
  return NULL;
}


BM_Type
BM_GetType(BM_Entry* entry)
{
  XP_ASSERT(entry);
  return entry ? entry->type : 0;
}


/* return the "name" for item -- may vary depending on its type */
char*
BM_GetName(BM_Entry* entry)
{
  XP_ASSERT(entry);
  if (!entry) return NULL;

  switch (entry->type) {
  case BM_TYPE_URL:
  case BM_TYPE_HEADER:
  case BM_TYPE_ADDRESS:
	return entry->name;
  case BM_TYPE_ALIAS:
	if (entry->d.alias.original)
	  return BM_GetName(entry->d.alias.original);
	else
	  return entry->name;
  default:
	return NULL;
  }
}

/* return the "address" for item -- may vary depending
	on its type */
char*
BM_GetAddress(BM_Entry* entry)
{
  XP_ASSERT(entry);
  if (!entry) return NULL;

  switch (entry->type) {
  case BM_TYPE_URL:
	return entry->d.url.address;
  case BM_TYPE_ADDRESS:
	return entry->d.address.address;
  case BM_TYPE_HEADER:
  case BM_TYPE_SEPARATOR:
  case BM_TYPE_ALIAS:
	if (entry->d.alias.original) return BM_GetAddress(entry->d.alias.original);
	return NULL;
  default:
	return NULL;
  }
}

/* return the "target" for item -- may vary depending
	on its type */
char*
BM_GetTarget(BM_Entry* entry, XP_Bool recurse)
{
  XP_ASSERT(entry);
  if (!entry) return NULL;

  switch (entry->type) {
  case BM_TYPE_URL:
	if ((recurse)&&(entry->d.url.target == NULL)&&(entry->parent != NULL))
	{
		return BM_GetTarget(entry->parent, recurse);
	}
	else
	{
		return entry->d.url.target;
	}
  case BM_TYPE_HEADER:
	if ((recurse)&&(entry->d.header.target == NULL)&&(entry->parent != NULL))
	{
		return BM_GetTarget(entry->parent, recurse);
	}
	else
	{
		return entry->d.header.target;
	}
  case BM_TYPE_ADDRESS:
  case BM_TYPE_SEPARATOR:
	return NULL;
  case BM_TYPE_ALIAS:
	if (entry->d.alias.original) return BM_GetTarget(entry->d.alias.original, recurse);
	return NULL;
  default:
	return NULL;
  }
}

/* return the "description" for item -- may vary depending on its type */
char*
BM_GetDescription(BM_Entry* entry)
{
  XP_ASSERT(entry);
  if (!entry) return NULL;

  switch (entry->type) {
  case BM_TYPE_URL:
  case BM_TYPE_HEADER:
  case BM_TYPE_ADDRESS:
	return entry->description;
  default:
	return NULL;
  }
}

char*
BM_GetNickName(BM_Entry* entry)
{
  XP_ASSERT(bm_GetContextForEntry(entry)->type == MWContextAddressBook);
  if (BM_ISALIAS(entry)) return BM_GetNickName(entry->d.alias.original);
  else return entry->nickname;
}


PRIVATE int32
bm_CountAliases_1(BM_Entry* at, BM_Entry* forEntry)
{
  int32 count = 0;
  for ( ; at ; at = at->next) {
	if (BM_ISHEADER(at)) {
	  count += bm_CountAliases_1(at->d.header.children, forEntry);
	} else if (BM_ISALIAS(at)) {
	  if (at->d.alias.original == forEntry) count++;
	}
  }
  return count;
}

PUBLIC int32
BM_CountAliases(MWContext* context, BM_Entry* entry)
{
  int32 result;
  CHKCONTEXT(context);
  result = bm_CountAliases_1(BM_GetRoot(context), entry);
  if (result) {
	XP_ASSERT(entry->flags & BM_ATTR_HASALIASES);
	BM_SETFLAG(entry, BM_ATTR_HASALIASES);
  } else {
	BM_CLEARFLAG(entry, BM_ATTR_HASALIASES);
  }
  return result;
}

BM_Date 
BM_GetLastVisited(BM_Entry *entry)
{
  XP_ASSERT(entry);
  if (!entry || (entry->type != BM_TYPE_URL)) return 0;

  return entry->d.url.last_visit;
}

BM_Date 
BM_GetAdditionDate(BM_Entry *entry)
{
  XP_ASSERT(entry);

  if (!entry) return 0;
  
  return entry->addition_date;
}


/* pretty print the last visited date         ### fix i18n */
char*
BM_PrettyLastVisitedDate(BM_Entry* entry)
{
  static char buffer[200];

  buffer[0] = 0;

  XP_ASSERT(entry);
  if (!entry) return NULL;

  if (entry->type == BM_TYPE_URL) {
	time_t lastVisited;
	time_t today;
	time_t elapsed;

	lastVisited = entry->d.url.last_visit;
	if (lastVisited == 0) return "";
	today = XP_TIME();

	elapsed = today - lastVisited;

	if (elapsed < SECONDS_PER_DAY) {
	  int32 hours = (elapsed + 1800L) / 3600L;
	  if (hours < 1) {
		return XP_GetString(XP_BKMKS_LESS_THAN_ONE_HOUR_AGO);
	  }
	  sprintf(buffer, XP_GetString(XP_BKMKS_HOURS_AGO), hours);
	} else if (elapsed < (SECONDS_PER_DAY * 31)) {
	  sprintf(buffer, XP_GetString(XP_BKMKS_DAYS_AGO),
			  (elapsed + (SECONDS_PER_DAY / 2)) / SECONDS_PER_DAY);
	} else {
	  struct tm* tmp;
	  tmp = localtime(&lastVisited);

	  sprintf(buffer, asctime(tmp));
	}
	return buffer;
  }
  return NULL;
}

/* pretty print the added on date */
char*
BM_PrettyAddedOnDate(BM_Entry* entry)
{
  static char buffer[200];
  struct tm* tmp;

  XP_ASSERT(entry);

  if (entry && entry->addition_date != 0) {
	tmp = localtime(&(entry->addition_date));
	sprintf(buffer, asctime(tmp));
	return buffer;
  }
  return NULL;
}

char*
BM_PrettyAliasCount(MWContext* context, BM_Entry* entry) 
{
  static char buffer[100];
  int32 count;
  char* name = context->type == MWContextBookmarks ? 
	XP_GetString(XP_BKMKS_BOOKMARK) : XP_GetString(XP_BKMKS_ENTRY);   	 
  CHKCONTEXT(context);
  XP_ASSERT(entry);
  if (!entry) return NULL;

  count = BM_CountAliases(context, entry);
  buffer[0] = 0;

  if (count > 1) {
	sprintf(buffer, XP_GetString(XP_BKMKS_COUNTALIASES_MANY), count, name);
  } else if (count == 1) {
	sprintf(buffer, XP_GetString(XP_BKMKS_COUNTALIASES_ONE), name);
  } else {
	sprintf(buffer, XP_GetString(XP_BKMKS_COUNTALIASES_NONE), name);
  }
  return buffer;
}


BM_Entry*
BM_GetChildren(BM_Entry* entry)
{
  if (BM_ISHEADER(entry)) return entry->d.header.children;
  return NULL;
}

BM_Entry*
BM_GetNext(BM_Entry* entry)
{
  XP_ASSERT(entry);
  return entry ? entry->next : NULL;
}


BM_Entry*
BM_GetParent(BM_Entry* entry)
{
  XP_ASSERT(entry);
  return entry ? entry->parent : NULL;
}


XP_Bool
BM_HasNext(BM_Entry* entry)
{
  XP_ASSERT(entry);
  return entry ? (entry->next != NULL) : FALSE;
}


XP_Bool
BM_HasPrev(BM_Entry* entry)
{
  BM_Entry* parent = entry->parent;
  if (parent) {
	XP_ASSERT(BM_ISHEADER(parent));
	return parent->d.header.children != entry;
  } else {
	return GETFRAME(bm_GetContextForEntry(entry))->gBookmarks != entry;
  }
}


static void
bm_flush_updates(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXTVOID(context);
  if (f->first_update_line > 0) {
	BMFE_RefreshCells(context, f->first_update_line, f->last_update_line,
					  FALSE);
	f->first_update_line = 0;
  }
}

static void
bm_start_batch(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);

#ifdef XP_UNIX
  BMFE_StartBatch(context);
#endif

  CHKCONTEXTVOID(context);
  if (f->undo) UNDO_StartBatch(f->undo);
  f->batch_depth++;
}

static void
bm_end_batch(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXTVOID(context);
  f->batch_depth--;
  XP_ASSERT(f->batch_depth >= 0);
  if (f->batch_depth == 0) {
	bm_flush_updates(context);
  }
  if (f->undo) UNDO_EndBatch(f->undo, NULL, NULL);

#ifdef XP_UNIX
  BMFE_EndBatch(context);
#endif
}

static void 
bm_refresh(MWContext* context, int32 first, int32 last)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXTVOID(context);
  XP_ASSERT(first >= 1 && first <= last);
  if (first < 1 || first > last) {
	/* Something bogus got passed in; just repaint everything to
	   be safe. */
	first = 1;
	last = BM_LAST_CELL;
  }
  if (f->first_update_line <= 0 ||
	  first > f->last_update_line + 1 ||
	  last + 1 < f->first_update_line) {
	bm_flush_updates(context);
	f->first_update_line = first;
	f->last_update_line = last;
  } else {
	if (f->first_update_line > first) f->first_update_line = first;
	if (f->last_update_line < last) f->last_update_line = last;
  }
  if (f->batch_depth == 0) bm_flush_updates(context);
}


/* Handy routine to detect if we're already going to refresh everything.
   If so, then the caller knows there's no need to refresh any more... */
static XP_Bool
bm_refreshing_all(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  return (f != NULL &&
		  f->first_update_line == 1 &&
		  f->last_update_line == BM_LAST_CELL);
}


static void
bm_entry_changed_2(MWContext* context, BM_Entry* entry)
{
  int32 index = BM_GetIndex(context, entry);
  if (index < 1) return;
  if (context->type == MWContextBookmarks || entry->parent == NULL) {
	bm_refresh(context, index, index);
  } else {
	/* Changing the entry might have messed up the sorting order.  Better
	   go resort it.  What a hack...*/
	BM_Entry* parent = entry->parent;
	BM_RemoveChildFromHeader(context, parent, entry);
	bm_AddChildToHeaderSorted(context, parent, entry);
  }
}

static void
bm_entry_changed_1(MWContext* context, BM_Entry* entry, BM_Entry* find)
{
  for (; entry ; entry = entry->next) {
	if (BM_ISALIAS(entry) && entry->d.alias.original == find) {
	  bm_entry_changed_2(context, entry);
	} else if (BM_ISHEADER(entry)) {
	  bm_entry_changed_1(context, entry->d.header.children, find);
	}
  }
}

static void
bm_entry_changed(MWContext* context, BM_Entry* entry)
{
  XP_ASSERT(!BM_ISALIAS(entry));
  if (entry->flags & BM_ATTR_HASALIASES) {
	bm_entry_changed_1(context, BM_GetRoot(context), entry);
  }
  bm_entry_changed_2(context, entry);
}


static void
bm_save_timer(void* closure)
{
  MWContext* context = (MWContext*) closure;
  BM_Frame* f = GETFRAME(context);
  f->savetimer = NULL;
  BM_SaveBookmarks(context, NULL);
}

/* The bookmarks have been modified somehow.  Set or reset a timer to cause
   them to be saved.*/
static void
bm_SetModified(MWContext* context, XP_Bool mod)
{
  BM_Frame* f = GETFRAME(context);
  f->gBookmarksModified = mod;
  f->max_depth = 0;
  if (f->savetimer) {
	FE_ClearTimeout(f->savetimer);
	f->savetimer = NULL;
  }
  if (mod) {
	f->savetimer = FE_SetTimeout(bm_save_timer, context,
								 60000L); /* ### hard-coding... */
	if (!f->savetimer) BM_SaveBookmarks(context, NULL);
  }
}

/* give LI the ability to set the modified to false */
void
BM_SetModified(MWContext* context, XP_Bool mod)
{
	bm_SetModified(context, mod);
}

typedef struct bm_setheader_info {
  MWContext* context;
  BM_Entry* entry;
  XP_Bool isadd;
} bm_setheader_info;

static void
bm_setheader_freeit(void* closure)
{
  XP_FREE((bm_setheader_info*) closure);
}

static int bm_setheader_undo(void* closure);

static void
bm_SetMenuOrAddHeader(MWContext* context, BM_Entry* entry, XP_Bool isadd)
{
  BM_Frame* f = GETFRAME(context);
  XP_ASSERT(context->type == MWContextBookmarks);
  XP_ASSERT(BM_ISHEADER(entry));
  if (context->type == MWContextBookmarks && f && BM_ISHEADER(entry)) {
	if (f->undo) {
	  bm_setheader_info* info = XP_NEW_ZAP(bm_setheader_info);
	  if (!info) {
		UNDO_DiscardAll(f->undo);
	  } else {
		info->context = context;
		info->entry = isadd ? f->addheader : f->menuheader;
		info->isadd = isadd;
		UNDO_LogEvent(f->undo, bm_setheader_undo, bm_setheader_freeit, info, NULL, NULL);
	  }
	}
	bm_start_batch(context);
	bm_entry_changed(context, isadd ? f->addheader : f->menuheader);
	if (isadd) f->addheader = entry;
	else f->menuheader = entry;
	bm_entry_changed(context, entry);
	if (!isadd) BMFE_BookmarkMenuInvalid(context);
	bm_SetModified(context, TRUE);
	bm_end_batch(context);
  }
}

int bm_setheader_undo(void* closure)
{
  bm_setheader_info* info = (bm_setheader_info*) closure;
  bm_SetMenuOrAddHeader(info->context, info->entry, info->isadd);
  return 0;
}

BM_Entry*
BM_GetMenuHeader(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  XP_ASSERT(context->type == MWContextBookmarks);
  return f ? f->menuheader : NULL;
}

void BM_SetMenuHeader(MWContext* context, BM_Entry* entry)
{
  bm_SetMenuOrAddHeader(context, entry, FALSE);
}


BM_Entry*
BM_GetAddHeader(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  XP_ASSERT(context->type == MWContextBookmarks);
  return f ? f->addheader : NULL;
}

void BM_SetAddHeader(MWContext* context, BM_Entry* entry)
{
  bm_SetMenuOrAddHeader(context, entry, TRUE);
}


typedef struct bm_copy_string_info {
  MWContext* context;
  BM_Entry* entry;
  char** string;
  char* value;
} bm_copy_string_info;


static void
bm_copy_string_freeit(void* closure)
{
  bm_copy_string_info* info = (bm_copy_string_info*) closure;
  FREEIF(info->value);
  XP_FREE(info);
}

static int bm_copy_string_undo(void* closure);

static int
bm_CopyStringWithUndo(MWContext* context, BM_Entry* entry, char** string,
					  const char* value)
{
  BM_Frame* f = GETFRAME(context);
  int status = 0;
  bm_SetModified(context, TRUE);
  if (f->undo) {
	bm_copy_string_info* info = XP_NEW_ZAP(bm_copy_string_info);
	if (!info) {
	  UNDO_DiscardAll(f->undo);
	  status = MK_OUT_OF_MEMORY;
	} else {
	  info->context = context;
	  info->entry = entry;
	  info->string = string;
	  info->value = *string ? XP_STRDUP(*string) : NULL;
	  UNDO_LogEvent(f->undo, bm_copy_string_undo, bm_copy_string_freeit,
					info, NULL, NULL);
	}
  }
  if (*string) XP_FREE(*string);
  *string = value ? XP_STRDUP(value) : NULL;
  bm_entry_changed(context, entry);
  return 0;
}

static int
bm_copy_string_undo(void* closure)
{
  bm_copy_string_info* info = (bm_copy_string_info*) closure;
  if (info->string == &(info->entry->nickname)) {
	/* Have to use BM_SetNickName to get side effect of changing hashtable. */
	BM_SetNickName(info->context, info->entry, info->value);
  } else {
	bm_CopyStringWithUndo(info->context, info->entry, info->string,
						  info->value);
  }
  return 0;
}



/* sets the name for a bm entry */
void
BM_SetName(MWContext* context, BM_Entry* entry, const char* newName)
{
  CHKCONTEXTVOID(context);
  XP_ASSERT(entry);
  if (!entry) return;
  BM_CLEARFLAG(entry, BM_ATTR_ISNEW);

  switch (entry->type) {
  case BM_TYPE_URL:
  case BM_TYPE_HEADER:
  case BM_TYPE_ADDRESS:
	if (entry->name == NULL || XP_STRCMP(entry->name, newName) != 0) {
	  bm_CopyStringWithUndo(context, entry, &entry->name, newName);
	  BMFE_BookmarkMenuInvalid(context);
	}
	break;
  case BM_TYPE_ALIAS:
	BM_SetName(context, entry->d.alias.original, newName);
	break;
  }
}



/* sets the location field of a bm_url bookmarks entry */
void
BM_SetAddress(MWContext* context, BM_Entry* entry, const char* newAddress)
{
  CHKCONTEXTVOID(context);
  XP_ASSERT(entry);
  if (!entry) return;
  BM_CLEARFLAG(entry, BM_ATTR_ISNEW);

  switch (entry->type) {
  case BM_TYPE_URL:
	if (entry->d.url.address == NULL ||
		XP_STRCMP(entry->d.url.address, newAddress) != 0) {
	  bm_CopyStringWithUndo(context, entry, &entry->d.url.address, newAddress);
	}
	break;
  case BM_TYPE_ADDRESS:
	if (entry->d.address.address == NULL ||
		XP_STRCMP(entry->d.address.address, newAddress) != 0) {
	  bm_CopyStringWithUndo(context, entry, &entry->d.address.address,
							newAddress);
	}
	break;
  case BM_TYPE_ALIAS:
	BM_SetAddress(context, entry->d.alias.original, newAddress);
	break;
  }
}


/* sets the target field of a bm_url bookmarks entry */
void
BM_SetTarget(MWContext* context, BM_Entry* entry, const char* newTarget)
{
  CHKCONTEXTVOID(context);
  XP_ASSERT(entry);
  if (!entry) return;
  BM_CLEARFLAG(entry, BM_ATTR_ISNEW);

  switch (entry->type) {
  case BM_TYPE_URL:
	if (entry->d.url.target == NULL ||
		XP_STRCMP(entry->d.url.target, newTarget) != 0) {
	  bm_CopyStringWithUndo(context, entry, &entry->d.url.target, newTarget);
	  if (entry->d.url.target[0] == '\0') {
		entry->d.url.target = NULL;
	  }
	}
	break;
  case BM_TYPE_HEADER:
	if (entry->d.header.target == NULL ||
		XP_STRCMP(entry->d.header.target, newTarget) != 0) {
	  bm_CopyStringWithUndo(context, entry, &entry->d.header.target, newTarget);
	  if (entry->d.header.target[0] == '\0') {
		entry->d.header.target = NULL;
	  }
	}
	break;
  case BM_TYPE_ADDRESS:
	break;
  case BM_TYPE_ALIAS:
	BM_SetAddress(context, entry->d.alias.original, newTarget);
	break;
  }
}

/* sets the description field of an entry */
PUBLIC void
BM_SetDescription(MWContext* context, BM_Entry* entry, const char* newDesc)
{
  CHKCONTEXTVOID(context);
  XP_ASSERT(entry);
  if (!entry) return;
  BM_CLEARFLAG(entry, BM_ATTR_ISNEW);

  switch (entry->type) {
  case BM_TYPE_URL:
  case BM_TYPE_HEADER:
  case BM_TYPE_ADDRESS:
	if (entry->description == NULL ||
		XP_STRCMP(entry->description, newDesc)) {
	  bm_CopyStringWithUndo(context, entry, &entry->description, newDesc);
	}
	break;
  case BM_TYPE_ALIAS:
	BM_SetDescription(context, entry->d.alias.original, newDesc);
	break;
  }
}


/*	BM_SetNickName returns FALSE if it reported an error to the user .
 *	It returns TRUE if everything worked fine.
 *  5-16-95 jefft
 *  Passing in NULL value removes the entry from the hash table.
 */

XP_Bool
BM_SetNickName(MWContext* context, BM_Entry* entry, const char* value)
{
	BM_Frame* f = GETFRAME(context);
	char* pName;
	CHKCONTEXT(context);
	XP_ASSERT(context->type == MWContextAddressBook);
	if (!entry) return(TRUE); 
	if (!value) {
	  /* 5-16-95 jefft -- bug#: 20808, remove entry from the hash table */
		if (entry->nickname && *entry->nickname) {
			XP_Remhash(f->nicknameTable, entry->nickname);
			FREEIF(entry->nickname);
		}
		return(TRUE);
	}

	/* allocate a copy of the string so we can modify it to be a legal alias */
	/* But only if value is non-null */
	pName = (value) ? XP_STRDUP(value) : NULL;
	if (value && !pName) return(FALSE);

	BM_CLEARFLAG(entry, BM_ATTR_ISNEW);

	if (BM_ISALIAS(entry)) {
		XP_Bool retVal;
		retVal = BM_SetNickName(context, entry->d.alias.original, value);
		XP_FREE(pName);
		return(retVal);
	} else {
		if (entry->nickname == NULL || value == NULL ||	XP_STRCMP(entry->nickname, value)) {
			if (pName != NULL)
			{
				char* ptr;
				for (ptr = pName ; *ptr ; ptr++) {
					if (!isalnum(*ptr) && (*ptr != '-') && (*ptr != '_')) {
						FE_Alert(context, XP_GetString(XP_BKMKS_INVALID_NICKNAME));
						XP_FREE(pName);
						return(FALSE);
					}
					/* convert to lowercase */
					if (isupper(*ptr)) {
						*ptr = (char)tolower(*ptr);
					}
				}
				if (XP_Gethash(f->nicknameTable, pName, NULL)) {
					FE_Alert(context, XP_GetString(XP_BKMKS_NICKNAME_ALREADY_EXISTS));
					FREEIF(pName);
					return(FALSE);
				}
			}
		}
		if (entry->nickname && *entry->nickname) {
			XP_Remhash(f->nicknameTable, entry->nickname);
		}
		bm_CopyStringWithUndo(context, entry, &entry->nickname, pName);
		if (entry->nickname && *entry->nickname) {
			XP_Puthash(f->nicknameTable, entry->nickname, entry);
		}
	}
	FREEIF(pName);
	return(TRUE);
}



void
BM_CancelEdit(MWContext* context, BM_Entry* entry)
{
  CHKCONTEXTVOID(context);
  bm_start_batch(context);
  if (entry && (entry->flags & BM_ATTR_ISNEW) &&
	    !(entry->flags & BM_ATTR_HASALIASES) && entry->parent) {
	BM_RemoveChildFromHeader(context, entry->parent, entry);
	BM_FreeEntry(context, entry);
	bm_refresh(context, 1, BM_LAST_CELL);
  }
  bm_end_batch(context);
}



/* returns the number of children parent has
	if visible is TRUE, only visible children are counted,
	otherwise all children are counted */
static int32
bm_CountChildren(BM_Entry* parent, XP_Bool visible)
{
  BM_Entry*		child;
  int32			count = 1;

  XP_ASSERT(parent);
  XP_ASSERT(BM_ISHEADER(parent));

  if (!parent || !BM_ISHEADER(parent)) return 0;

  if (!visible || !(parent->flags & BM_ATTR_FOLDED)) {
	child = parent->d.header.children;
	while (child) {
	  if (BM_ISHEADER(child)) {
		count += bm_CountChildren(child, visible);
	  } else {
		count++;
	  }
	  child = child->next;
	}
  }

  return count;
}


static void
bm_WidestEntry_1(MWContext* context, BM_Entry* parent, BM_Entry** widest,
				 uint32* widestWidth)
{
  BM_Entry*		child;
  uint32			width;
  uint32			height;

  XP_ASSERT(parent);
  XP_ASSERT(BM_ISHEADER(parent));
  XP_ASSERT(widestWidth);
  XP_ASSERT(widest);

  BMFE_MeasureEntry(context, parent, &width, &height);

  if (width > *widestWidth) {
	*widestWidth = width;
	*widest = parent;
  }

  if (!(BM_ISFOLDED(parent))) {
	child = parent->d.header.children;
	while (child) {
	  if (BM_ISHEADER(child)) {
		bm_WidestEntry_1(context, child, widest, widestWidth);
	  } else {
		BMFE_MeasureEntry(context, child, &width, &height);
		if (width > *widestWidth) {
		  *widestWidth = width;
		  *widest = child;
		}
	  }
	  child = child->next;
	}
  }
}

/* returns the widest visible entry in the tree
(this uses a FE function to measure the width) */
PUBLIC BM_Entry*
BM_WidestEntry(MWContext* context)
{
  BM_Entry* widest = NULL;
  uint32 widestWidth = 0;
  CHKCONTEXT(context);
  
  bm_WidestEntry_1(context, BM_GetRoot(context), &widest, &widestWidth);
  return widest;
}


static void
bm_SyncCount(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  if (f) {
	f->gCount = -1;
	f->gVisCount = -1;
  }
  BMFE_SyncDisplay(context);
}

PRIVATE void
bm_SyncSelection_1(BM_Entry* parent, int32* count, uint32* selectionMask)
{
  BM_Entry* child;

  XP_ASSERT(parent);
  XP_ASSERT(BM_ISHEADER(parent));

  if (parent->flags & BM_ATTR_SELECTED) {
	*selectionMask |= BM_TYPE_HEADER;
	(*count)++;
  }

  child = parent->d.header.children;
  while (child) {
	if (BM_ISHEADER(child)) {
	  bm_SyncSelection_1(child, count, selectionMask);
	} else {
	  if (BM_ISSELECTED(child)) {
		*selectionMask |= child->type;
		(*count)++;
	  }
	}
	child = child->next;
  }
}

/* synchronizes the selection mask and the selection count with
	what is actually selected
	this is necessary when items become deselected because
	we don't have a global selection list, only a count and
	mask
*/
static void
bm_SyncSelection(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  f->gSelectionCount = 0;
  f->gSelectionMask = 0;

  bm_SyncSelection_1(BM_GetRoot(context), &(f->gSelectionCount),
					 &(f->gSelectionMask));
}

/* return the index number of item in cur_count with regards
to the BM_ATTR_FOLDED flag */
PRIVATE int32
bm_GetIndexNum(BM_Entry* parent, BM_Entry* item, int32* cur_count)
{
  BM_Entry* child;
  int32 rv = 0;

  XP_ASSERT(parent);
  XP_ASSERT(BM_ISHEADER(parent));

  child = parent->d.header.children;

  if (parent == item) return *cur_count;

  while (child) {
	(*cur_count)++;

	if (child == item) {
	  return *cur_count;
	}

	/* if it's a header and it's unfolded, traverse it's children */
	if (child->type == BM_TYPE_HEADER && !BM_ISFOLDED(child)) {
	  rv = bm_GetIndexNum(child, item, cur_count);
	  if (rv)
		return rv;
	}
	child = child->next;
  }
  return 0;
}

/* return the index number of item in cur_count without regards to
   the BM_ATTR_FOLDED flag */
PRIVATE int32
bm_GetUnfoldedIndexNum(BM_Entry* parent, BM_Entry* item, int32* cur_count)
{
  BM_Entry* child;
  int32 rv = 0;

  XP_ASSERT(parent);
  XP_ASSERT(parent->type == BM_TYPE_HEADER);

  if (parent == item) return *cur_count;

  for (child = parent->d.header.children; child; child = child->next) {
	(*cur_count)++;

	if (child == item) return *cur_count;


	if (child->type == BM_TYPE_HEADER) {
	  rv = bm_GetUnfoldedIndexNum(child, item, cur_count);
	  if (rv) return rv;
	}
  }
  return 0;
}

/* returns the child url entry of parent whose address is the same as
   url_address */
PRIVATE void
bm_FindItemStub(MWContext *context, BM_Entry* parent, char* url_address, EntryFunc pf, void *pClosure)
{
  BM_Entry*	child;

  if (!parent) { /* Eric made me do it */
	return;
  }

  for (child = parent->d.header.children; child; child = child->next) {
	if (child->type == BM_TYPE_URL && child->d.url.address &&
		!XP_STRCMP(child->d.url.address, url_address)) {
	  (*pf)(context, child, pClosure);
	}
	if (child->type == BM_TYPE_HEADER) {
	  bm_FindItemStub(context, child, url_address, pf, pClosure);
	}
  }
  return;
}


PRIVATE int32
bm_GetDepth(BM_Entry* parent, BM_Entry* item)
{
  int32 rv = 0;
  BM_Entry* next;

  if (!item) return -1;

  next = item;
  while (next && next->parent) { /* I think extra "next &&" is
										   necessary for Win16 busted
										   optimizer... */
	rv++;
	next = next->parent;
  }
  return rv;
}



static void
bm_simple_freeit(void* closure)
{
  XP_FREE(closure);
}


typedef struct bm_delete_child_info {
  MWContext* context;
  BM_Entry* parent;
  BM_Entry* child;
} bm_delete_child_info;


static int
bm_delete_child_doit(void* closure)
{
  bm_delete_child_info* info = (bm_delete_child_info*) closure;
  BM_RemoveChildFromHeader(info->context, info->parent, info->child);
  return 0;
}


static void
bm_LogDeleteChild(MWContext* context, BM_Entry* parent, BM_Entry* child)
{
  BM_Frame* f = GETFRAME(context);
  bm_delete_child_info* info;

  /* Magic side effect -- if a child has just been added, and it doesn't have
	 an addition date set, set it to be now. */
  if (child->addition_date == 0) {
	child->addition_date = XP_TIME();
  }

  if (!f || !f->undo) return;
  bm_SetModified(context, TRUE);
  info = XP_NEW_ZAP(bm_delete_child_info);
  if (!info) {
	UNDO_DiscardAll(f->undo);
  } else {
	info->context = context;
	info->parent = parent;
	info->child = child;
	UNDO_LogEvent(f->undo, bm_delete_child_doit, bm_simple_freeit, info, NULL, NULL);
  }
}

/* appends a child item to a parent at the end of the
	parents child list */
static void
bm_AppendChildToHeader(MWContext* context, BM_Entry* parent, BM_Entry* child)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* lastChild;

  XP_ASSERT(parent);
  XP_ASSERT(BM_ISHEADER(parent));
  XP_ASSERT(child);
  XP_ASSERT(child != parent);

  f->gCount = -1;
  f->gVisCount = -1;

  lastChild = parent->d.header.lastChild;
  if (lastChild) {
	lastChild->next = child;
	parent->d.header.lastChild = child;
  } else {
	parent->d.header.children = child;
	parent->d.header.lastChild = child;
  }

  parent->d.header.childCount++;
  child->parent = parent;
  
  if( !f->bSorting )
    child->iNaturalIndex = g_iNaturalIndexPool++;
    
  if (context) {
	BMFE_BookmarkMenuInvalid(context);
	bm_LogDeleteChild(context, parent, child);
  }
}

void
BM_AppendToHeader(MWContext* context, BM_Entry* parent, BM_Entry* child)
{
  int index;
  bm_start_batch(context);
  bm_AppendChildToHeader(context, parent, child);
  index = BM_GetIndex(context, child);
  if (index > 0) bm_refresh(context, index, BM_LAST_CELL);
  bm_end_batch(context);
}


/* Add a child item to a parent at the beginning of the
	parents child list */
void
BM_PrependChildToHeader(MWContext* context, BM_Entry* parent, BM_Entry* child)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* firstChild;

  XP_ASSERT(parent);
  XP_ASSERT(parent->type == BM_TYPE_HEADER);
  XP_ASSERT(child);
  XP_ASSERT(child != parent);

  f->gCount = -1;
  f->gVisCount = -1;
  firstChild = parent->d.header.children;
  if (!firstChild) {
	bm_AppendChildToHeader(context, parent, child);
  } else {
	child->next = firstChild;
	parent->d.header.children = child;

	parent->d.header.childCount++;
	child->parent = parent;
    
    if( !f->bSorting )    
      child->iNaturalIndex = g_iNaturalIndexPool++;
      
	if (context) {
	  BMFE_BookmarkMenuInvalid(context);
	  bm_LogDeleteChild(context, parent, child);
	}
  }
}


static int
bm_SortAddressBook(const void* obj1, const void* obj2)
{
  const BM_Entry* entry1 = (const BM_Entry*) obj1;
  const BM_Entry* entry2 = (const BM_Entry*) obj2;

  if (BM_ISALIAS(entry1)) {
	entry1 = entry1->d.alias.original;
	XP_ASSERT(!BM_ISALIAS(entry1));
  }
  if (BM_ISALIAS(entry2)) {
	entry2 = entry2->d.alias.original;
	XP_ASSERT(!BM_ISALIAS(entry2));
  }
  XP_ASSERT(BM_ISHEADER(entry1) || BM_ISADDRESS(entry1));
  XP_ASSERT(BM_ISHEADER(entry2) || BM_ISADDRESS(entry2));
  if (entry1 == entry2) return 0; /* Can happen with two aliases to the same
									 thing... */
  if (BM_ISHEADER(entry1)) {
	if (BM_ISHEADER(entry2)) {
#ifdef INTL_SORT
	  return XP_StrColl(entry1->name, entry2->name);
#else
	  return XP_STRCMP(entry1->name, entry2->name);
#endif
	} else {
	  return 1;
	}
  } else {
	if (BM_ISHEADER(entry2)) {
	  return -1;
	} else {
#ifdef INTL_SORT
	  return XP_StrColl(entry1->name, entry2->name);
#else
	  return XP_STRCMP(entry1->name, entry2->name);
#endif
	}
  }
}


static BM_Entry*
bm_RealEntry(BM_Entry* entry)
{
  if (BM_ISALIAS(entry)) return entry->d.alias.original;
  else return entry;
}


/* Adds a child item to a parent, sorting it according to address book sorting
   rules. */
static void
bm_AddChildToHeaderSorted(MWContext* context, BM_Entry* parent,
						  BM_Entry* child)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* entry;
  BM_Entry* previous = NULL;
  XP_ASSERT(context->type == MWContextAddressBook);
  XP_ASSERT(BM_ISHEADER(parent));
  if (!BM_ISALIAS(child)) parent = BM_GetRoot(context);
  if (parent->d.header.lastChild &&
	  bm_SortAddressBook(parent->d.header.lastChild, child) < 0) {
	/* Ah, the most common case (especially when loading from a file).  This
	   kid goes last. */
	previous = parent->d.header.lastChild;
	bm_AppendChildToHeader(context, parent, child);
  } else {
	for (entry = parent->d.header.children ; entry ; entry = entry->next) {
	  int value = bm_SortAddressBook(entry, child);
	  if (value > 0) break;
	  if (value == 0) {
		/* Hmm.  Let's not allow any duplicate aliases to the same thing
		   in the same header. */
		if (bm_RealEntry(entry) == bm_RealEntry(child)) {
		  if (BM_ISALIAS(child)) {
			BM_FreeEntry(context, child);
		  } else {
			XP_ASSERT(BM_ISALIAS(entry));
			BM_RemoveChildFromHeader(context, parent, entry);
			bm_AddChildToHeaderSorted(context, parent, child);
		  }
		  return;
		}
	  }
	  previous = entry;
	}
	if (previous == NULL) {
	  BM_PrependChildToHeader(context, parent, child);
	  previous = parent;
	} else {
	  bm_InsertItemAfter(context, previous, child, FALSE);
	}
  }
  f->gCount = -1;
  f->gVisCount = -1;
  if (!BM_ISFOLDED(parent) && !bm_refreshing_all(context)) {
	int index = BM_GetIndex(context, previous);
	if (index > 0) {
	  f->gVisCount++;
	  bm_refresh(context, index + 1, BM_LAST_CELL);
	}
  }
}

static BM_Entry*
bm_get_previous(BM_Entry* entry)
{
  BM_Entry*	child;
  BM_Entry*	previous = NULL;

  if (entry && entry->parent) {
	child = entry->parent->d.header.children;
	previous = NULL;
	while (child && child != entry) {
	  previous = child;
	  child = child->next;
	}
  }

  if (child == NULL) previous = NULL;

  return previous;
}



typedef struct bm_add_child_info {
  MWContext* context;
  BM_Entry* parent;
  BM_Entry* previous;
  BM_Entry* child;
} bm_add_child_info;


static int
bm_add_child_doit(void* closure)
{
  bm_add_child_info* info = (bm_add_child_info*) closure;
  XP_ASSERT(info->previous == NULL || info->previous->parent == info->parent);
  if (info->previous) {
	bm_InsertItemAfter(info->context, info->previous, info->child, FALSE);
  } else {
	BM_PrependChildToHeader(info->context, info->parent, info->child);
  }
  BM_ClearAllSelection(info->context, FALSE);
  return 0;
}



void BM_RemoveChildFromHeader(MWContext* context, BM_Entry* parent, BM_Entry* child)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* previous;

  XP_ASSERT(BM_ISHEADER(parent));
  if (!BM_ISHEADER(parent)) return;
  XP_ASSERT(child);
  if (!child) return;
  XP_ASSERT(child != parent);
  if (child == parent) return;
  XP_ASSERT(child->parent == parent);
  if (child->parent != parent) return;

  if (context && (child->flags & BM_ATTR_SELECTED)) {
	BM_SelectItem(context, child, TRUE, TRUE, FALSE);
  }
  previous = bm_get_previous(child);

  if (previous) previous->next = child->next;

  if (parent->d.header.children == child) {
	parent->d.header.children = child->next;
  }

  if (parent->d.header.lastChild == child) {
	parent->d.header.lastChild = previous;
  }

  f->gCount = -1;
  f->gVisCount = -1;

  parent->d.header.childCount--;
  
  if (context) {
	BM_Frame* f = GETFRAME(context);
	bm_add_child_info* info;
	BMFE_BookmarkMenuInvalid(context);
	bm_SetModified(context, TRUE);
	if (f->undo) {
	  info = XP_NEW_ZAP(bm_add_child_info);
	  if (!info) {
		UNDO_DiscardAll(f->undo);
	  } else {
		info->context = context;
		info->parent = parent;
		info->previous = previous;
		info->child = child;
		UNDO_LogEvent(f->undo, bm_add_child_doit, bm_simple_freeit, info, NULL, NULL);
	  }
	}
  }
  child->parent = NULL;
  child->next = NULL;
}



#define BM_HEADER_BEGIN		0xD000
#define BM_HEADER_END		0xE000
#define BM_UNKNOWN			0xF000

static uint16
bm_tokenize_line(MWContext* context, char* buffer, char** ptr)
{
  if ((*ptr = strcasestr(buffer, "HREF=\""))) {
	return context->type == MWContextBookmarks ? BM_TYPE_URL : BM_TYPE_ADDRESS;
  } else if ((*ptr = strcasestr(buffer, "<H")) && isdigit(*(*ptr + 2))) {
	return BM_TYPE_HEADER;
  } else if ((*ptr = strcasestr(buffer, "<HR>"))) {
	return BM_TYPE_SEPARATOR;
  } else if (strcasestr(buffer, "</UL>") ||
			 strcasestr(buffer, "</MENU>") ||
			 strcasestr(buffer, "</DL>")) {
	return BM_HEADER_END;
  } else if (strcasestr(buffer, "<UL>") ||
			 strcasestr(buffer, "<MENU>") ||
			 strcasestr(buffer, "<DL>")) {
	return BM_HEADER_BEGIN;
  } else {
	return BM_UNKNOWN;
  }
}

/* parse out the folded state in buffer */
static XP_Bool
bm_is_folded(char* buffer)
{
  XP_ASSERT(buffer);
  return strcasestr(buffer, "FOLDED") != NULL;
}

/* parse out the addition date in buffer */
static time_t
bm_addition_date(char* buffer)
{
	char*		ptr;
	char*		end;
	time_t		add_date = 0;

	XP_ASSERT(buffer);

	ptr = strcasestr(buffer, "ADD_DATE=\"");
	if (ptr)
	{
		/* find the end of the addition date */
		end = XP_STRCHR(ptr + 10, '"');
		if (end)
		{
			/* temporarily stick a NULL in the buffer */
			*end = '\0';

			add_date = (time_t)atol(ptr + 10);

			/* replace the quote */
			*end = '"';
		}
	}
	return add_date;
}

/* parse out the last visited or last modified date in buffer */
static time_t
bm_last_date(char* buffer, XP_Bool ismodified)
{
	char* ptr;
	char* start;
	char* end;
	time_t result = 0;

	ptr = strcasestr(buffer,
		ismodified ? "LAST_MODIFIED=\"": "LAST_VISIT=\"");
	if (ptr) {
		start = ptr + (ismodified ? 15 : 12);
		end = XP_STRCHR(start, '"');
		if (end) {
			/* temporarily stick a NULL in the buffer */
			*end = '\0';

			result = (time_t)atol(start);

			/* replace the quote */
			*end = '"';
		}
	}
	return result;
}


/* parse out the target string in buffer */
static char *
bm_target(char* buffer, XP_Bool ismodified)
{
	char* ptr;
	char* start;
	char* end;
	char *result = NULL;

	ptr = strcasestr(buffer, "TARGET=\"");
	if (ptr) {
		start = ptr + 8;
		end = XP_STRCHR(start, '"');
		if (end) {
			/* temporarily stick a NULL in the buffer */
			*end = '\0';

			result = (start) ? XP_STRDUP(start) : NULL;

			/* replace the quote */
			*end = '"';
		}
	}
	return result;
}


typedef struct bm_alias_info {
  char* id;					/* String to use for this alias in the file. */
  char* key;				/* Key to use to lookup this alias in the table */
  BM_Entry* entry;
} bm_alias_info;


static bm_alias_info* 
bm_find_alias_info(MWContext* context, const char* ptr, XP_Bool create)
{
  BM_Frame* f = GETFRAME(context);
  bm_alias_info* info = NULL;
  if (f->aliasTable) {
	info = (bm_alias_info *)XP_Gethash(f->aliasTable, ptr, NULL);
	XP_ASSERT(info == NULL || XP_STRCMP(ptr, info->key) == 0);
	if (!info && create) {
	  info = XP_NEW_ZAP(bm_alias_info);
	  if (info) {
		info->key = XP_STRDUP(ptr);
		XP_Puthash(f->aliasTable, info->key, info);
	  }
	}
  }
  return info;
}

static bm_alias_info*
bm_find_writealias_info(MWContext* context, BM_Entry* entry)
{
  BM_Frame* f = GETFRAME(context);
  static char key[20];
  bm_alias_info* info;
  XP_SPRINTF(key, "%ld", (long) entry);
  info = bm_find_alias_info(context, key, TRUE);
  if (info && info->id == NULL) {
	info->id = (char *)XP_ALLOC(10);
	if (info->id) {
	  XP_SPRINTF(info->id, "%d", f->aliasID++);
	}
  }
  return info;
}

static XP_Bool 
bm_free_alias_info(XP_HashTable table, const void* key, void* value,
				   void* closure)
{
  bm_alias_info* info = (bm_alias_info*) value;
  FREEIF(info->id);
  FREEIF(info->key);
  XP_FREE(info);
  return TRUE;
}


static int
bm_string_cmp (const void *obj1, const void *obj2)
{
  XP_ASSERT (obj1 && obj2);
  return XP_STRCMP((char*) obj1, (char*) obj2);
}

static void
bm_clear_alias_info(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  if (f->aliasTable) {
	XP_Maphash(f->aliasTable, bm_free_alias_info, NULL);
	XP_Clrhash(f->aliasTable);
  } else {
	f->aliasTable = XP_HashTableNew(100, XP_StringHash, bm_string_cmp);
  }
  f->aliasID = 0;
}



/* Checks if the given item is an alias to another item, or has aliases to it.
   Takes care of all the required tree mucking, and updating of the alias
   table.  The return value is the item for the caller to insert into the
   tree; it is usually but not always the item passed in. */
static BM_Entry*
bm_check_read_alias(MWContext* context, BM_Entry* new_item, char* parseString)
{
  char* ptr;
  char* end = NULL;
  ptr = strcasestr(parseString, "ALIASID=\"");
  if (ptr) {
	ptr += 9;
	end = XP_STRCHR(ptr, '"');
	if (end) {
	  bm_alias_info* info;
	  *end = '\0';
	  info = bm_find_alias_info(context, ptr, TRUE);
	  if (info) {
		if (info->entry) {
		  /* Sigh.  We have the definition of an alias, but there
			 have already been some references to this alias, and we don't
			 really want to go chasing the references down.  Instead, we'll
			 just copy our data into the existing record.  Yikes. */
		  BM_Entry* tmp = XP_NEW(BM_Entry);
		  if (tmp) {
			XP_MEMCPY(tmp, info->entry, sizeof(BM_Entry));
			XP_MEMCPY(info->entry, new_item, sizeof(BM_Entry));
			BM_FreeEntry(context, tmp);
			XP_FREE(new_item);
			new_item = info->entry;
		  }
		} else {
		  info->entry = new_item;
		}
	  }
	  BM_SETFLAG(new_item, BM_ATTR_HASALIASES);
	}
  } else {
	ptr = strcasestr(parseString, "ALIASOF=\"");
	if (ptr) {
	  ptr += 9;
	  end = XP_STRCHR(ptr, '"');
	  if (end) {
		bm_alias_info* info;
		*end = '\0';
		info = bm_find_alias_info(context, ptr, TRUE);
		if (info) {
		  if (info->entry == NULL) {
			/* Even though this entry is probably not complete, it will
			   do better than nothing.  If something better comes along,
			   this one will get replaced.  If not, we'll insert this
			   item into the main tree at the end. */
			info->entry = new_item;
		  } else {
			BM_FreeEntry(context, new_item);
		  }
		  new_item = bm_NewAlias(info->entry);
		  if (!new_item) return NULL;
		}
	  }
	}
  }

  /* replace the quotes */
  if (end) *end = '"';
  return new_item;
}

static void
bm_check_nickname(MWContext* context, BM_Entry* entry, char* str)
{
  char* ptr = strcasestr(str, "NICKNAME=\"");
  char* end;
  if (ptr) {
	ptr += 10;
	end = XP_STRCHR(ptr, '"');
	if (end) {
	  *end = '\0';
	  BM_SetNickName(context, entry, ptr);
	  *end = '"';
	}
  }
}

/*
// Replace all occurances of escaped quotes (%22) with explicit quotes (").
// Do not replace beyond the end of the " delimited string.
// 
// Return a ptr to the position after the last occurance of an escaped quote.
*/
static char *bm_explicit_quotes( char *pszSource )
{
    char *pszCsr  = NULL;
    char *pszLast = NULL;
    
    if( !pszSource )
    {
        return pszSource;
    }
    
    
    pszLast = XP_STRCHR( pszSource, '"' );
    
    while( (pszCsr = strstr( pszSource, "%22" )) && (pszCsr < pszLast) )
    {
        *pszCsr = '"';    
        pszSource = pszCsr + 1;
	XP_MEMMOVE( pszSource, pszSource+2, XP_STRLEN(pszSource+2)+1 );
    }    

    return pszSource;
}


static BM_Entry*
bm_read_url(MWContext* context, XP_File fp, char* buffer, char* ptr,
			const char* relative_url)
{
  char* endQuote;
  char* gtr_than;
  char* parseString;
  char* end;
  char* url;
  char *pszAfterLastEscapedQuote;
  BM_Entry* new_item = NULL;

  /* find next quote */
  parseString = ptr + 6;

  /* Replace escaped quotes with explicit ones */
  pszAfterLastEscapedQuote = bm_explicit_quotes( parseString );
  
  endQuote = XP_STRCHR(pszAfterLastEscapedQuote, '"');

  if (endQuote) {
  
	/* temporarily terminate */
	*endQuote = '\0';

	url = NET_MakeAbsoluteURL((char*)relative_url, parseString);
	if (url) {
	  new_item = BM_NewUrl(NULL, url, NULL, 0);
	  XP_FREE(url);
	}
	if (!new_item) return NULL;

	/* find '>' and the name will be right after it */
	gtr_than = XP_STRCHR(endQuote + 1, '>');
	if (gtr_than) {
	  /* find the end of the name */
	  end = strcasestr(gtr_than, "</A>");
	  if (end) {
		*end = '\0';
		StrAllocCopy(new_item->name, XP_StripLine(gtr_than + 1));
		/* terminate at beginning of name since there
		   is nothing interesting after that */
		*gtr_than = '\0';
	  } else {
		StrAllocCopy(new_item->name,
					 XP_StripLine(gtr_than + 1));

		/* what happens if this breaks??  this is bogus stuff I don't
		   know what to do with */
		XP_FileReadLine(buffer, READ_BUFFER_SIZE, fp);
		end = strcasestr(buffer, "</A>");

		if (end) *end = '\0';

		StrAllocCat(new_item->name, XP_StripLine(buffer));
	  }
	}

	parseString = endQuote + 1;

	new_item->d.url.target = bm_target(parseString, FALSE);

	new_item->addition_date = bm_addition_date(parseString);

	new_item->d.url.last_visit = bm_last_date(parseString, FALSE);
	new_item->d.url.last_modified = bm_last_date(parseString, TRUE);
	if (new_item->d.url.last_modified == 0) {
	  new_item->d.url.last_modified = new_item->d.url.last_visit;
	}

	new_item = bm_check_read_alias(context, new_item, parseString);

	/* replace the quotes */
	*endQuote = '"';
  }

  return new_item;
}


static BM_Entry*
bm_read_address(MWContext* context, XP_File fp, char* buffer, char* ptr)
{
  char* endQuote;
  char* gtr_than;
  char* parseString;
  char* end;
  char* url;
  BM_Entry* new_item = NULL;

  XP_ASSERT(context->type == MWContextAddressBook);

  /* find next quote */
  parseString = ptr + 6;

  endQuote = XP_STRCHR(parseString, '"');
  if (endQuote) {
	*endQuote = '\0';

	url = parseString;

	if (strncasecomp(url, "mailto:", 7) == 0) {
	  url += 7;
	}

	new_item = bm_NewAddress(NULL, url);
	if (!new_item) return NULL;

	/* find '>' and the name will be right after it */
	gtr_than = XP_STRCHR(endQuote + 1, '>');
	if (gtr_than) {
	  *gtr_than++ = '\0';
	  /* find the end of the name */
	  end = strcasestr(gtr_than, "</A>");
	  if (end) {
		*end = '\0';
		StrAllocCopy(new_item->name, XP_StripLine(gtr_than));
	  }
	}

	parseString = endQuote + 1;

	new_item = bm_check_read_alias(context, new_item, parseString);

	if (!BM_ISALIAS(new_item)) {
	  bm_check_nickname(context, new_item, parseString);
	}
  }

  return new_item;
}


/* ptr should point to the chars "<Hx" */
static BM_Entry*
bm_read_header(MWContext* context, char* buffer, char* ptr)
{
  BM_Frame* f = GETFRAME(context);
  char* gtr_than;
  char* end;
  BM_Entry* new_item = NULL;


  /* find the beginning of the name */
  gtr_than = XP_STRCHR(ptr + 3, '>');

  /* find the end of the name */
  if (gtr_than) {
	end = strcasestr(gtr_than, "</H");
  }

  if (gtr_than && end) {
	/* temporarily NULL the name string */
	*end = '\0';

	new_item = BM_NewHeader(gtr_than + 1);

	if (!new_item)
	  return NULL;

	*gtr_than = '\0';

	new_item->d.header.target = bm_target(buffer, FALSE);

	new_item->addition_date = bm_addition_date(buffer);

	if (bm_is_folded(buffer))
	  BM_SETFLAG(new_item, BM_ATTR_FOLDED);
	else
	  BM_CLEARFLAG(new_item, BM_ATTR_FOLDED);

	new_item = bm_check_read_alias(context, new_item, buffer);

	if (!BM_ISALIAS(new_item)) {
	  if (context->type == MWContextAddressBook) {
		bm_check_nickname(context, new_item, buffer);
	  } else {
		if (strcasestr(buffer, "MENUHEADER")) f->menuheader = new_item;
		if (strcasestr(buffer, "NEWITEMHEADER")) f->addheader = new_item;
	  }
	}

  }

  return new_item;
}



static void
bm_read_description(BM_Entry* new_item, char* buffer )
{
  char* ptr;
  char* end;
  int length;

  /* assume the rest is descriptions; ignore if item is not a reasonable
	 type */

  if (!new_item || !buffer) return;

  switch (new_item->type) {
  case BM_TYPE_HEADER:
  case BM_TYPE_URL:
  case BM_TYPE_ADDRESS:

	length = XP_STRLEN(buffer);

	/* skip <DL> if present */
	if (*buffer == '<') {
	  buffer += 4;
	  length -= 4;
    }

	if (length <= 0) return;

	end = buffer + length - 1;

	/* check for <BR> on the end and remove it also add a return */
	if (*end == '>') {
	  end -= 3;
	  XP_STRCPY(end, LINEBREAK);
	  end += LINEBREAK_LEN;
	  *end = '\0';
	} else {
	  end++;
	  XP_STRCPY(end, LINEBREAK);
	  end += LINEBREAK_LEN;
	  *end = '\0';
	}

	/* go through and turn &lt; into '<' */
	for (ptr = buffer, end = buffer; *end != '\0'; end++) {
	  if (!strncasecomp(end, "&lt;", 4)) {
		end += 3;
		*ptr++ = '<';
	  } else {
		*ptr++ = *end;
	  }
	}
	*ptr = '\0'; /* terminate */
	StrAllocCat(new_item->description, buffer);
  }
}


	

/* Find the next entry after this one, where "next" means "the one that would
   show up on the next line if we didn't fold any headers". Also, this will
   wrap around from the end back to the beginning.  In other words, it will
   never return NULL. */
static BM_Entry*
bm_GetNextSpanningWrapping(MWContext* context, BM_Entry* at)
{
  if (BM_ISHEADER(at) && at->d.header.children) return at->d.header.children;
  if (at->next) return at->next;
  do {
	at = at->parent;
	if (at && at->next) {
	  return at->next;
	}
  } while (at);
  return BM_GetRoot(context);
}



static XP_Bool
bm_StringMatches(MWContext* context, BM_FindInfo* findInfo, const char* str) {
  char* ptr;
  INTL_CharSetInfo c = LO_GetDocumentCharacterSetInfo(context);
  if (!str) return FALSE;
  if (findInfo->matchCase) {
	ptr = INTL_Strstr(INTL_GetCSIWinCSID(c), str, findInfo->textToFind);
  } else {
	ptr = INTL_Strcasestr(INTL_GetCSIWinCSID(c), str, findInfo->textToFind);
  }
  if (!ptr) return FALSE;
  if (findInfo->matchWholeWord) {
	XP_ASSERT(ptr >= str);
	XP_ASSERT(ptr + XP_STRLEN(findInfo->textToFind) <= str + XP_STRLEN(str));
	if (ptr != str && !isspace(ptr[-1]) && !ispunct(ptr[-1])) return FALSE;
	ptr += XP_STRLEN(findInfo->textToFind);
	if (*ptr != '\0' && !isspace(*ptr) && !ispunct(*ptr)) return FALSE;
  }
  return TRUE;
}


static XP_Bool
bm_IsMatch(MWContext* context, BM_Entry* entry, BM_FindInfo* findInfo)
{

  if(!context)
	  return FALSE;

  if (BM_ISALIAS(entry)) entry = entry->d.alias.original;
  if (findInfo->checkNickname) {
	if (bm_StringMatches(context, findInfo, entry->nickname)) return TRUE;
  }
  if (findInfo->checkName) {
	if (bm_StringMatches(context, findInfo, BM_GetName(entry))) return TRUE;
  }
  if (findInfo->checkLocation && BM_ISURL(entry)) {
	if (bm_StringMatches(context, findInfo, entry->d.url.address)) return TRUE;
  }
  if (findInfo->checkDescription) {
	if (bm_StringMatches(context, findInfo, entry->description)) return TRUE;
  }
  return FALSE;
}

static BM_Entry*
bm_DoFindBookmark_1(MWContext* context, BM_Entry* at, BM_FindInfo* findInfo) {
  BM_Entry* start = at;
  if (!at) return NULL;
  do {
	if (bm_IsMatch(context, at, findInfo)) return at;
	at = bm_GetNextSpanningWrapping(context, at);
  } while (at != start);
  return NULL;
}

/* reads an item from fp using the specified buffer and relative_url */
static void
bm_ReadFromHTML(MWContext* context,
				XP_File fp,
				BM_Entry* item,
				char* buffer,
				const char* relative_url)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry*	new_item = NULL;
  char* buffer_ptr;
  char* ptr;
  uint16 type;

  /* read loop */
  while (XP_FileReadLine(buffer, READ_BUFFER_SIZE, fp)) {
	buffer_ptr = XP_StripLine(buffer);

	type = bm_tokenize_line(context, buffer_ptr, &ptr);

	switch (type) {
	case BM_TYPE_URL:
	  new_item = bm_read_url(context, fp, buffer_ptr, ptr, relative_url);
	  break;

	case BM_TYPE_ADDRESS:
	  new_item = bm_read_address(context, fp, buffer_ptr, ptr);
	  break;

	case BM_TYPE_HEADER:
	  new_item = bm_read_header(context, buffer_ptr, ptr);
	  break;

	case BM_TYPE_SEPARATOR:
	  if (context->type == MWContextBookmarks) {
		new_item = bm_NewSeparator();
	  }
	  break;

	case BM_HEADER_END:
	  if (item != f->gBookmarks) return;
	  break;

	case BM_UNKNOWN:
	  if (new_item)
		bm_read_description(new_item, buffer_ptr);
	  else if (item)
		bm_read_description(item, buffer_ptr);
	  break;
	}

	/* test for insertable item -- nb you'll need
	   to update this if you add new types/items */
	if (new_item && (type == BM_TYPE_URL ||
					 type == BM_TYPE_HEADER ||
					 type == BM_TYPE_SEPARATOR ||
					 type == BM_TYPE_ADDRESS)) {
	  if (!item) {
		if (!f->gBookmarks) {
		  if (new_item->type == BM_TYPE_HEADER) {
			f->gBookmarks = new_item;
			if (context->type == MWContextBookmarks) {
			  f->menuheader = f->addheader = new_item;
			}
			goto SKIP;
		  } else {
			(void) BM_GetRoot(context);	/* Has side effect of creating
										   root header. */
			if (!f->gBookmarks) return;
		  }
		}
		item = f->gBookmarks;
	  }

	  if (context->type == MWContextBookmarks) {
		bm_AppendChildToHeader(context, item, new_item);
	  } else {
		bm_AddChildToHeaderSorted(context, item, new_item);
	  }

SKIP:
	  /* if it's a header, recurse */
	  if (new_item->type == BM_TYPE_HEADER) {
		bm_ReadFromHTML(context, fp, new_item, buffer, relative_url);
	  }
	}
  }
}

static int
bm_WriteAsHTML(MWContext* context, XP_File fp, BM_Entry* item, int32 level,
			   XP_Bool isalias);


static int
bm_write_ok(const char* str, int length, XP_File fp)
{
  if (length < 0) length = XP_STRLEN(str);
  if ((int) XP_FileWrite(str, length, fp) < length) return -1;
  return 0;
}


#define WRITE(str, length, fp) \
if (bm_write_ok((str), (length), (fp)) < 0) return -1

static int
bm_write_alias_info(MWContext* context, XP_File fp, BM_Entry* entry,
					XP_Bool isalias)
{
  bm_alias_info* info;
  XP_ASSERT(!isalias || (entry->flags & BM_ATTR_HASALIASES));
  if (entry->flags & BM_ATTR_HASALIASES) {
	if (!isalias) {
	  /* Well, we think we have some aliases, but we can't be sure.  Let's
		 make sure. */
	  if (BM_CountAliases(context, entry) == 0) return 0;
	}
	info = bm_find_writealias_info(context, entry);
	WRITE(isalias ? " ALIASOF=\"" : " ALIASID=\"", -1, fp);
	WRITE(info->id, -1, fp);
	WRITE("\"", -1, fp);
  }
  return 0;
}


static int
bm_write_nickname(MWContext* context, XP_File fp, BM_Entry* entry,
				  XP_Bool isalias)
{
  if (context->type == MWContextAddressBook && !isalias &&
	  entry->nickname && *entry->nickname) {
	WRITE(" NICKNAME=\"", -1, fp);
	WRITE(entry->nickname, -1, fp);
	WRITE("\"", -1, fp);
  }
  return 0;		/* XXX This was left out. Is is 0 right? */
}
	


static int
bm_write_html_header(MWContext* context, XP_File fp, BM_Entry* item,
					 int32 level, XP_Bool isalias)
{
  BM_Frame* f = GETFRAME(context);
  char buffer[16];
  int32 i;
  BM_Entry*	child;
  char* target;
  int status;

  XP_ASSERT(BM_ISHEADER(item));

  target = BM_GetTarget(item, FALSE);

  if (level != 0) {
	if (item->name) {
	  WRITE("<DT><H3", -1, fp);
	  /* write folded state */
	  if (item->flags & BM_ATTR_FOLDED) {
		WRITE(" FOLDED", -1, fp);
	  }

	  if (item == f->menuheader) {
		WRITE(" MENUHEADER", -1, fp);
	  }
	  if (item == f->addheader) {
		WRITE(" NEWITEMHEADER", -1, fp);
	  }

	  status = bm_write_alias_info(context, fp, item, isalias);
	  if (status < 0) return status;

	  status = bm_write_nickname(context, fp, item, isalias);
	  if (status < 0) return status;

	  /* write target */
	  if ((target)&&(target[0] != '\0'))
	  {
		WRITE(" TARGET=\"", -1, fp);
		WRITE(target, -1, fp);
		WRITE("\"", -1, fp);
	  }

	  if (context->type == MWContextBookmarks) {
		/* write addition date */
		WRITE(" ADD_DATE=\"", -1, fp);
		XP_SPRINTF(buffer, "%ld\"", item->addition_date);
		WRITE(buffer, XP_STRLEN(buffer), fp);
	  }
	  WRITE(">", -1, fp);

	  /* write name */
	  WRITE(item->name, XP_STRLEN(item->name), fp);
	  WRITE("</H3>", -1, fp);
	  WRITE(LINEBREAK, LINEBREAK_LEN, fp);
	}
  }

  /* write description if there is one */
  if (item->description) {
	char *ptr = XP_StripLine(item->description);

	WRITE("<DD>", -1, fp);

	for (; *ptr != '\0'; ptr++) {
	  if (*ptr == '<') {
		WRITE("&lt;", -1, fp);
	  } else if (*ptr == '\n') {
		WRITE("<BR>", -1, fp);
		WRITE(LINEBREAK, LINEBREAK_LEN, fp);
	  } else {
		WRITE(ptr, 1, fp);
	  }
	}
	WRITE(LINEBREAK, LINEBREAK_LEN, fp);
  }

  if (!isalias) {
	/* write children out */
	for (i = 0; i < level; i++) {
	  WRITE("    ", -1, fp); /* indent */
	}
	WRITE("<DL><p>" LINEBREAK, -1, fp);
	
	for (child = item->d.header.children; child ; child = child->next) {
	  bm_WriteAsHTML(context, fp, child, level + 1, FALSE);
	}
	
	for (i = 0; i < level; i++) {
	  WRITE("    ", -1, fp);
	}
	
	WRITE("</DL><p>" LINEBREAK, -1, fp);
  }
  return 0;
}

static int
bm_write_separator(XP_File fp)
{
  WRITE("<HR>", -1, fp);
  WRITE(LINEBREAK, LINEBREAK_LEN, fp);
  return 0;
}

static int
bm_write_address( char *pszAddress, XP_File fp )
{
    /*
    // Replace explicit quotes with escaped quotes before writing the address.
    // For example:
    //    javascript:netscape.plugin.composer.Document.editDocument("http://myserver.com/docs/schedule.html")
    // is converted to:
    //    javascript:netscape.plugin.composer.Document.editDocument(%20http://myserver.com/docs/schedule.html%20)
    */
    
    int     iBufPos, iLen = 0;
    char *  pszCsr = pszAddress;
    char *  pszBuf = NULL;
    
    if( !pszAddress || !fp )
    {
        return 0;
    }

    if( !XP_STRCHR( pszAddress, '"' ) )
    {
        /* No quotes to convert, so just write it and return. */
        
        WRITE( pszAddress, -1, fp );
        return 0;
    }
        
    /*
    // Calculate the size of the new string.
    */
    iLen = XP_STRLEN( pszAddress );
    while( *pszCsr )
    {
        if( *pszCsr == '"' )
        {
            iLen += 2;
        }
        pszCsr++;
    }
 
    pszBuf = (char *)XP_ALLOC( iLen+1 );
    if( !pszBuf )
    {
        return 0;
    }

    /*
    // Copy the url while converting explicit quotes to escaped quotes.
    */
    iBufPos = 0;    
    pszCsr  = pszAddress;
    while( *pszCsr )
    {
        if( *pszCsr == '"' )
        {
            pszBuf[iBufPos]   = '%';
            pszBuf[++iBufPos] = '2';
            pszBuf[++iBufPos] = '2';
        }
        else
        {
            pszBuf[iBufPos] = *pszCsr;
        }
        
        iBufPos++;
        pszCsr++;
    }
    pszBuf[iBufPos] = 0;
    
    /* Finally write out the converted address */
    
    WRITE( pszBuf, -1, fp );
    
    XP_FREE( pszBuf );
}

/* writes out a URL entry to look like:
 *
 * <DT><A HREF="http://www.ncsa.uiuc.edu/radio/radio.html" \
 * ADD_DATE="777240414" LAST_VISIT="802992591">Internet Talk Radio</A>
 *
 */
static int
bm_write_url_or_address(MWContext* context, XP_File fp, BM_Entry* item,
						XP_Bool isalias)
{
  char buffer[16];
  char* address;
  char* target;
  int status;

  address = BM_GetAddress(item);
  target = BM_GetTarget(item, FALSE);

  if (address) {
	WRITE("<DT>", -1, fp);

	/* write address */
	WRITE("<A HREF=\"", -1, fp);
	if (context->type == MWContextAddressBook) {
	  WRITE("mailto:", -1, fp);
	}
	bm_write_address(address, fp);
	WRITE("\"", -1, fp);

	/* write target */
	if ((target)&&(target[0] != '\0'))
	{
	  WRITE(" TARGET=\"", -1, fp);
	  WRITE(target, -1, fp);
	  WRITE("\"", -1, fp);
	}

	status = bm_write_alias_info(context, fp, item, isalias);
	if (status < 0) return status;

	status = bm_write_nickname(context, fp, item, isalias);
	if (status < 0) return status;

	if (BM_ISURL(item)) {
	  /* write the addition date  */
	  WRITE(" ADD_DATE=\"", -1, fp);
	  XP_SPRINTF(buffer, "%ld", item->addition_date);
	  WRITE(buffer, -1, fp);
	  WRITE("\"", -1, fp);

	  /* write the last visited date */
	  WRITE(" LAST_VISIT=\"", -1, fp);
	  XP_SPRINTF(buffer, "%ld\"", item->d.url.last_visit);
	  WRITE(buffer, -1, fp);

	  /* write the last modified date */
	  WRITE(" LAST_MODIFIED=\"", -1, fp);
	  XP_SPRINTF(buffer, "%ld\"", item->d.url.last_modified);
	  WRITE(buffer, -1, fp);
	}
	WRITE(">", -1, fp);

	/* write the name */

	if (item->name) {
	  WRITE(item->name, -1, fp);
	} else {
	  if (BM_ISURL(item)) {
		WRITE(item->d.url.address, -1, fp);
	  } else {
		XP_ASSERT(BM_ISADDRESS(item));
		WRITE(item->d.address.address, -1, fp);
	  }
	}

	WRITE("</A>", -1, fp);
	WRITE(LINEBREAK, LINEBREAK_LEN, fp);

	/* write description if there is one */
	if (item->description) {
	  char *ptr = XP_StripLine(item->description);

	  WRITE("<DD>", -1, fp);

	  for (; *ptr != '\0'; ptr++) {
		if (*ptr == '<') {
		  WRITE("&lt;", -1, fp);
		} else if (*ptr == '\n') {
		  WRITE("<BR>", -1, fp);
		  WRITE(LINEBREAK, LINEBREAK_LEN, fp);
		} else {
		  WRITE(ptr, 1, fp);
		}
	  }
	  WRITE(LINEBREAK, LINEBREAK_LEN, fp);
	}
  }
  return 0;
}


/* writes an item into fp at the specified indentation level */
static int
bm_WriteAsHTML(MWContext* context, XP_File fp, BM_Entry* item, int32 level,
			   XP_Bool isalias)
{
  int32 i;
  int status = 0;

  /* indent */
  if (!isalias) {
	for (i = 0; i < level; i++) {
	  WRITE("    ", -1, fp);
	}
  }

  switch (item->type) {
  case BM_TYPE_HEADER:
	status = bm_write_html_header(context, fp, item, level, isalias);
	break;

  case BM_TYPE_SEPARATOR:
	status = bm_write_separator(fp);
	break;

  case BM_TYPE_URL:
  case BM_TYPE_ADDRESS:
	status = bm_write_url_or_address(context, fp, item, isalias);
	break;

  case BM_TYPE_ALIAS:
	XP_ASSERT(!isalias);
	status = bm_WriteAsHTML(context, fp, item->d.alias.original, level, TRUE);
	break;
  }
  return status;
}

/* clears the selected state of parent and all of it's children
	if refresh is TRUE, the FE is called to redraw necessary items
	count should match the index of parent in the visible tree.  count will
	be NULL if a parent is folded. */
PRIVATE void
bm_ClearSelection(MWContext* context, BM_Entry* parent, XP_Bool refresh, int32* count)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* child;
  XP_ASSERT(parent);
  XP_ASSERT(BM_ISHEADER(parent));

  if (parent->flags & BM_ATTR_SELECTED) {
	BM_CLEARFLAG(parent, BM_ATTR_SELECTED);
	if (refresh && count) bm_refresh(context, *count, *count);
	f->gSelectionCount = -9999;
  }

  if (count) (*count)++;

  if (BM_ISFOLDED(parent)) count = NULL;

  for (child = parent->d.header.children ; child ; child = child->next) {
	if (child->type != BM_TYPE_HEADER) {
	  if (child->flags & BM_ATTR_SELECTED) {
		BM_CLEARFLAG(child, BM_ATTR_SELECTED);
		if (refresh && count) bm_refresh(context, *count, *count);
		f->gSelectionCount = -9999;
	  }
	  if (count) (*count)++;
	} else {
	  bm_ClearSelection(context, child, refresh, count);
	}
  }
}

PRIVATE void
bm_SelectAll(MWContext* context, BM_Entry* parent)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* child;

	XP_ASSERT(parent);
	XP_ASSERT(parent->type == BM_TYPE_HEADER);

	child = parent->d.header.children;
	if (!BM_ISSELECTED(parent))
	{
		BM_SETFLAG(parent, BM_ATTR_SELECTED);
		f->gSelectionCount++;
		f->gSelectionMask |= BM_TYPE_HEADER;
	}

	while (child)
	{
		if (BM_ISHEADER(child))
			bm_SelectAll(context, child);
		else
		{
			if (!(child->flags & BM_ATTR_SELECTED))
			{
				BM_SETFLAG(child, BM_ATTR_SELECTED);
				f->gSelectionCount++;
				f->gSelectionMask |= child->type;
			}
		}
		child = child->next;
	}
}

PUBLIC void
BM_SelectAll(MWContext* context, XP_Bool refresh)
{
	BM_Entry*		root;
	CHKCONTEXTVOID(context);

	root = BM_GetRoot(context);

	if (root)
	{
		bm_SelectAll(context, root);
		if (refresh)
			bm_refresh(context, 1, BM_LAST_CELL);
	}
}

static void
bm_TellGoingAway(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_Frame* f = GETFRAME(context);
  if (entry == f->menuheader) {
	BM_SetMenuHeader(context, BM_GetRoot(context));
  }
  if (entry == f->addheader) {
	BM_SetAddHeader(context, BM_GetRoot(context));
  }
  BMFE_EntryGoingAway(context, entry);
  if (entry->nickname) {
	BM_SetNickName(context, entry, NULL);	/* Causes the nickname hash to be
											   cleared, adding undo stuff to
											   bring it back if this entry
											   somehow gets brought back. */
	XP_ASSERT(entry->nickname == NULL);
  }
#ifdef DEBUG
  /* Confirm that we are not deleting any dangling aliases. */
  if (entry->flags & BM_ATTR_HASALIASES) {
	int32 count = BM_CountAliases(context, entry);
	if (count) {
	  BM_Entry* deleteroot = (BM_Entry*) closure;
	  if (BM_ISHEADER(deleteroot)) {
		/* Reduce the count by the number of aliases that we're going to
		   delete. */
		count -= bm_CountAliases_1(deleteroot, entry);
	  }
	  XP_ASSERT(count == 0);
	}
  }
#endif /* DEBUG */
}

/* free's a bmlist entry */
PRIVATE void
bm_ShallowFreeEntry(BM_Entry* entry)
{
  if (entry) {
	XP_ASSERT(entry->next == NULL);
	XP_ASSERT(entry->nickname == NULL); 

	FREEIF(entry->name);
	FREEIF(entry->description);
	switch (entry->type) {
	case BM_TYPE_HEADER:
	  XP_ASSERT(entry->d.header.children == NULL);
	  break;

	case BM_TYPE_URL:
	  FREEIF(entry->d.url.address);
	  FREEIF(entry->d.url.content_type);
	  break;

	case BM_TYPE_ADDRESS:
	  FREEIF(entry->d.address.address);
	  break;
	}
	XP_FREE(entry);
  }
}

PRIVATE void
bm_ReallyFreeEntry(void* data)
{
  BM_Entry* entry = (BM_Entry*) data;
  while (entry) {
	BM_Entry* next = entry->next;
	entry->next = NULL;
	if (BM_ISHEADER(entry)) {
	  /* free all the children */
	  bm_ReallyFreeEntry(entry->d.header.children);
	  entry->d.header.children = NULL;
	}
	bm_ShallowFreeEntry(entry);
	entry = next;
  }
}


typedef struct bm_free_info {
  BM_Entry* entry;
  XP_Bool usedFromUndo;
} bm_free_info;

static int
bm_cancel_free(void* closure)
{
  bm_free_info* info = (bm_free_info*) closure;
  info->usedFromUndo = TRUE;
  return 0;
}


static void
bm_free_freeit(void* closure)
{
  bm_free_info* info = (bm_free_info*) closure;
  if (!info->usedFromUndo) bm_ReallyFreeEntry(info->entry);
  XP_FREE(info);
}


/* free's a BM_Entry and all of its succeeding siblings... if it's a
	header, it frees all of its children.  However, actually this
	does nothing, but logs an event in the undo queue.  When the event gets
	freed, then we know that nothing in the undo chain needs this thing,
	so *then* we can free it.*/
void
BM_FreeEntry(MWContext* context, BM_Entry* entry)
{
  BM_Frame* f = GETFRAME(context);
  if (!entry) return;

  bm_start_batch(context);
  bm_EachEntryDo_1(context, entry, bm_TellGoingAway, entry);

  if (f->undo) {
	bm_free_info* info = XP_NEW_ZAP(bm_free_info);
	if (!info) {
	  UNDO_DiscardAll(f->undo);
	  bm_ReallyFreeEntry(entry);
	} else {
	  info->entry = entry;
	  UNDO_LogEvent(f->undo, bm_cancel_free, bm_free_freeit, info, NULL, NULL);
	}
  } else {
	bm_ReallyFreeEntry(entry);
  }
  bm_end_batch(context);
}



int 
BM_InitializeBookmarksContext(MWContext* context)
{
  BM_Frame* f;
  XP_ASSERT(context);
  if (!context) return -1;		/* ### Need better error code? */
  f = XP_NEW_ZAP(BM_Frame);
  if (!f) return MK_OUT_OF_MEMORY;
  XP_ASSERT(context->bmframe == NULL);
  f->undo = UNDO_Create(10);
  if (!f->undo) goto FAIL;
  f->nicknameTable = XP_HashTableNew(100, XP_StringHash, bm_string_cmp);
  if (!f->nicknameTable) goto FAIL;
  f->errorSavingBookmarks = FALSE;
  f->enSortType = BM_Sort_Natural;
  f->bSorting = FALSE;
  context->bmframe = f;
  f->next = ContextList;
  ContextList = context;
  (void) BM_GetRoot(context);	/* Has side effect of creating root header. */
  bm_refresh(context, 1, BM_LAST_CELL);
  return 0;
FAIL:
  if (f->undo) UNDO_Destroy(f->undo);
  if (f->nicknameTable) XP_HashTableDestroy(f->nicknameTable);
  XP_FREE(f);
  return MK_OUT_OF_MEMORY;
}

void
BM_CleanupBookmarksContext(MWContext* context)
{
  BM_Frame* f;
  MWContext** tmp;
  CHKCONTEXTVOID(context);
  BM_SaveBookmarks(context, NULL);

									/* This cleanup code can be slow and
								   inefficient.  Since we're gonna exit soon
								   anyway, let's not bother doing this stuff.
								   ### - DMB - Let's, at least for debug
									detection of meory leaks. How slow could it
									be?	*/
  f = GETFRAME(context);
  UNDO_Destroy(f->undo);
  f->undo = NULL;
  BM_FreeEntry(context, f->gBookmarks);
  if (f->savetimer) {
	FE_ClearTimeout(f->savetimer);
	f->savetimer = NULL;
  }

  f->gBookmarks = NULL;
  if (f->aliasTable) {
	bm_clear_alias_info(context);
	XP_HashTableDestroy(f->aliasTable);
  }
  if (f->nicknameTable) {
	XP_HashTableDestroy(f->nicknameTable);
  }
       
  for (tmp = &ContextList ; *tmp ; tmp = &(f->next)) {
	f = GETFRAME(*tmp);
	if (*tmp == context) {
      (*tmp)->bmframe = NULL;
	  *tmp = f->next;
	  XP_FREE(f);
	  return;
	}
  }
  XP_ASSERT(0);
}


/* returns TRUE if the bookmarks have been modified since
the file was read, FALSE otherwise */
PUBLIC XP_Bool
BM_Modified(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXT(context);
  return f ? f->gBookmarksModified : FALSE;
}

static void
bm_UpdateTimeStamp(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_Date cur_time = *(BM_Date *)closure;
  BM_Frame* f = GETFRAME(context);
  int32 oldstate;
  
  if (!entry) return;
  
  oldstate = BM_GetChangedState(entry);
  entry->d.url.last_visit = cur_time;
  if (entry->d.url.last_modified == 0) {
    /* Well, this current visitation is a good enough estimate for the modification time. */
    entry->d.url.last_modified = cur_time;
  }

  /* Deliberately *don't* call bm_SetModified here; we don't want to cause
     the file to be saved soon for this trivial change.  Just turn on the
     modified bit so that we know things will be saved eventually. */
  f->gBookmarksModified = TRUE;

  if (BM_GetChangedState(entry) != oldstate) {
    bm_entry_changed(context, entry);
  }
}

/* checks the bmlist for a url and updates the last accessed time */
PUBLIC void
BM_UpdateBookmarksTime(URL_Struct* URL_s, BM_Date cur_time)
{
  MWContext* context;
  BM_Frame* f;

  if (!URL_s) return;

  for (context = ContextList ; context ; context = f->next) {
	f = GETFRAME(context);
	if (context->type != MWContextBookmarks) continue;
	bm_FindItemStub(context, BM_GetRoot(context), URL_s->address, bm_UpdateTimeStamp, (void *)&cur_time);
  }
}


/* returns the total number of items in the tree */
PUBLIC int32
BM_GetCount(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXT(context);
  if (!f) return 0;
  if (f->gCount <= 0) {
	f->gCount = bm_CountChildren(BM_GetRoot(context), FALSE);
  }
  return f->gCount;
}

/* returns the number of items in the tree that are presently
	visible */
PUBLIC int32
BM_GetVisibleCount(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXT(context);
  if (!f) return 0;
  if (f->gVisCount <= 0) {
	f->gVisCount = bm_CountChildren(BM_GetRoot(context), TRUE);
  }
  return f->gVisCount;
}





static void
bm_InsertItemAfter(MWContext* context, BM_Entry* insert_after,
				   BM_Entry* insertee, XP_Bool sync)
{
    BM_Frame* f = GETFRAME(context);
    CHKCONTEXTVOID(context);

	XP_ASSERT(insertee);
	if (!insertee) return;

    /* insert after the item if specified */
	if (insert_after)
	{
		BM_Entry* tmp;
		BM_Entry* parent;

		if (insert_after->parent == NULL)
		{
			/* insert as first child, displayed below header */
			tmp = insert_after->d.header.children;
			parent = insert_after;
			parent->d.header.childCount++;
			parent->d.header.children = insertee;
		}
		else
		{
			tmp = insert_after->next;
			parent = insert_after->parent;

			if (parent)
			{
				parent->d.header.childCount++;
				if (!tmp)
					parent->d.header.lastChild = insertee;
			}
			insert_after->next = insertee;
		}
		insertee->next = tmp;
		insertee->parent = parent;
        
        if( !f->bSorting )        
          insertee->iNaturalIndex = g_iNaturalIndexPool++;
        
		BMFE_BookmarkMenuInvalid(context);
		bm_LogDeleteChild(context, parent, insertee);
	}
	else
		bm_AppendChildToHeader(context, BM_GetRoot(context), insertee);

	bm_SetModified(context, TRUE);
	if (sync)
	    bm_SyncCount(context);
}

/* insert an item after another item in the bmlist
	if the insert_after item is NULL the item
	will be inserted at the end of the bookmarks */
PUBLIC void
BM_InsertItemAfter(MWContext* context, BM_Entry* insert_after, BM_Entry* insertee)
{
  CHKCONTEXTVOID(context);
  bm_start_batch(context);
  bm_InsertItemAfter(context, insert_after, insertee, TRUE);
  bm_end_batch(context);
}

/* insert an item in a header if "insert_after" is a
	Header type, or after the item if "insert after" is
	not a header type.
	if the insert_after item is NULL or not found the item
	will be inserted at the end of the bookmarks */
PUBLIC void
BM_InsertItemInHeaderOrAfterItem(	MWContext* context,
									BM_Entry* insert_after,
							    	BM_Entry* insertee)
{
  CHKCONTEXTVOID(context);
  XP_ASSERT(insertee);

  bm_start_batch(context);
  if (insert_after && insert_after->type == BM_TYPE_HEADER)
	bm_AppendChildToHeader(context, insert_after, insertee);
  else
	BM_InsertItemAfter(context, insert_after, insertee);
  bm_SyncCount(context);
  bm_end_batch(context);
}

void remove_to(MWContext* context, BM_Entry* entry, void* to)
{
	BM_Entry*		moveTo = (BM_Entry*)to;
	BM_Entry*		parent;

	XP_ASSERT(entry);

	parent = entry->parent;
	if (parent)
		BM_RemoveChildFromHeader(context, parent, entry);

	bm_AppendChildToHeader(context, moveTo, entry);
}



static int
bm_get_max_depth_1(BM_Entry* entry)
{
  int result = 0;
  for (; entry ; entry = entry->next) {
	if (BM_ISHEADER(entry) && !BM_ISFOLDED(entry)) {
	  int value = bm_get_max_depth_1(entry->d.header.children);
	  if (result < value) result = value;
	}
  }
  return result + 1;
}

int
BM_GetMaxDepth(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXT(context);
  if (!f) return 0;
  if (f->max_depth == 0) {
	f->max_depth = bm_get_max_depth_1(f->gBookmarks);
  }
  return f->max_depth;
}


PUBLIC XP_Bool
BM_IsDragEffectBox(MWContext* context, int line, XP_Bool under)
{
  BM_Entry* entry;
  CHKCONTEXT(context);
  if (line <= 0) return FALSE;
  entry = BM_AtIndex(context, line);
  if (!entry) return FALSE;		/* ### */
  if (BM_ISHEADER(entry)) {
	if (under && (BM_ISFOLDED(entry) || entry->d.header.childCount == 0)) {
	  return FALSE;
	}
	return TRUE;
  }
  return FALSE;
}


PUBLIC void
BM_DoDrop(MWContext* context, int line, XP_Bool under)
{
  BM_Entry* dest = BM_AtIndex(context, line);
  BM_Entry* tmp;
  BM_Entry* parent;
  BM_Entry* entry;
  CHKCONTEXTVOID(context);
  if (!dest) return;
  for (parent = dest ; parent ; parent = parent->parent) {
	if (BM_ISSELECTED(parent)) return;
  }
  tmp = BM_NewHeader("");
  if (!tmp) return;
  bm_start_batch(context);
  if (BM_ISHEADER(dest) && BM_IsDragEffectBox(context, line, under)) {
	parent = dest;
	dest = NULL;
  } else {
	parent = dest->parent;
  }
  BM_EachProperSelectedEntryDo(context, remove_to, tmp, NULL);
  entry = tmp->d.header.children;
  if (entry) {
	BM_ClearAllSelection(context, FALSE);
	while ((entry = tmp->d.header.children)) {
	  BM_RemoveChildFromHeader(context, tmp, entry);
	  BM_CLEARFLAG(entry, BM_ATTR_SELECTED);
	  if (context->type == MWContextBookmarks) {
		if (dest) {
		  bm_InsertItemAfter(context, dest, entry, FALSE);
		} else {
		  BM_PrependChildToHeader(context, parent, entry);
		}
	  } else {
		if (BM_ISALIAS(entry)) {
		  bm_AddChildToHeaderSorted(context, parent, entry);
		} else {
		  bm_AddChildToHeaderSorted(context, BM_GetRoot(context), entry);
		  entry = bm_NewAlias(entry);
		  bm_AddChildToHeaderSorted(context, parent, entry);
		}
	  }
	  if (!BM_ISHEADER(parent) || !BM_ISFOLDED(parent))
	  BM_SelectItem(context, entry, FALSE, TRUE, TRUE);
	  dest = entry;
	  entry = tmp->d.header.children;
	}
	if (BM_ISHEADER(parent) && BM_ISFOLDED(parent))
	  BM_SelectItem(context, parent, FALSE, TRUE, TRUE);
	BMFE_BookmarkMenuInvalid(context);
	bm_SyncCount(context);
	bm_refresh(context, 1, BM_LAST_CELL);
  }
  BM_FreeEntry(context, tmp);
  bm_end_batch(context);
}



typedef struct bm_goingaway_info {
  XP_Bool userasked;
  XP_Bool userconfirmed;
  BM_Entry* entry;
  int count;
} bm_goingaway_info;


static void
bm_subtract_alias_for(MWContext* context, BM_Entry* entry, void* closure)
{
  bm_goingaway_info* info = (bm_goingaway_info*) closure;
  if (BM_ISALIAS(entry) && entry->d.alias.original == info->entry) {
	info->count--;
  }
}


static void
bm_delete_alias_for(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_Entry* base = (BM_Entry*) closure;
  if (BM_ISALIAS(entry) && entry->d.alias.original == base) {
	BM_RemoveChildFromHeader(context, entry->parent, entry);
	BM_FreeEntry(context, entry);
  }
}



static void
bm_check_dangling_aliases(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_Frame* f = GETFRAME(context);
  bm_goingaway_info* info = (bm_goingaway_info*) closure;
  if (entry->flags & BM_ATTR_HASALIASES) {
	info->count = BM_CountAliases(context, entry);
	if (info->count) {
	  /* Reduce the count by the number of aliases that we're going to
		 delete. */
	  info->entry = entry;
	  BM_EachProperSelectedEntryDo(context, bm_subtract_alias_for, info, NULL);
	  XP_ASSERT(info->count >= 0);
	  if (info->count) {
		if (!info->userasked) {
		  if (f->gSelectionCount < 0) bm_SyncSelection(context);
		  if (f->gSelectionCount == 1) {
			char* buf = (char*) XP_ALLOC(512);
			if (buf) {
			  XP_SPRINTF(buf,
						 XP_GetString(XP_BKMKS_REMOVE_THIS_ITEMS_ALIASES), info->count);
			  info->userconfirmed = FE_Confirm(context, buf);
			  XP_FREE(buf);
			} else {
			  info->userconfirmed =
				FE_Confirm
				(context,
				 XP_GetString(XP_BKMKS_REMOVE_SOME_ITEMS_ALIASES) );
			}
			info->userasked = TRUE;
		  }
		  if (info->userconfirmed) {
			BM_EachEntryDo(context, bm_delete_alias_for, entry);
		  }
		}
	  }
	}
  }
}

static void
bm_delete(MWContext* context)
{
	BM_Entry*		tmp;
	struct BM_Entry_Focus bmFocus;
	bm_goingaway_info info;

	tmp = BM_NewHeader("");
	XP_ASSERT(tmp);
	if (!tmp) return;

	XP_MEMSET(&info, 0, sizeof(info));
	BM_EachProperSelectedEntryDo(context, bm_check_dangling_aliases, &info,
	  NULL);
	if (info.userasked && !info.userconfirmed) return;

	bmFocus.saveFocus = (BM_Entry*) NULL;
	bmFocus.foundSelection = FALSE;
	BM_EachProperSelectedEntryDo(context, remove_to, tmp, &bmFocus);

	BM_FreeEntry(context, tmp);
	bm_SyncCount(context);

	if (bmFocus.saveFocus == NULL)
	  bmFocus.saveFocus = BM_GetRoot(context);
	if (bmFocus.saveFocus)
	  BM_SETFLAG(bmFocus.saveFocus, BM_ATTR_SELECTED);
	bm_refresh(context, 1, BM_LAST_CELL);
}


static void
bm_copy(MWContext* context)
{
  char*			block;
  int32			length;

  block = BM_ConvertSelectionsToBlock(context, TRUE, &length);
  BMFE_SetClipContents(context, (void*)block, length);

  XP_FREE(block);
}

static void
bm_cut(MWContext* context)
{
  bm_copy(context);
  bm_delete(context);
}


static void
bm_paste(MWContext* context)
{
  BM_Entry*		firstSelected;
  char*			buffer;
  int32			length;

  firstSelected = BM_FirstSelectedItem(context);
  buffer = (char*)BMFE_GetClipContents(context, &length);
  if (buffer)
	{
	  BM_InsertBlockAt(context, buffer, firstSelected, TRUE, length);
	  bm_SyncCount(context);
	  bm_refresh(context, 1, BM_LAST_CELL);
	}
}

/* Insert a block of long-format entries */
PUBLIC void
BM_DropBlockL( MWContext *pContext, char *pData, BM_Entry *firstSelected )
{
   int32 length;

   if( !firstSelected )
   {
      firstSelected = BM_FirstSelectedItem( pContext );
   }
   
   if( pData )
   {
      /* Length is stored at byte 0 as int32 */
      XP_MEMCPY( &length, pData, sizeof(int32) );
      pData += sizeof(int32);
      
      BM_InsertBlockAt( pContext, pData, firstSelected, TRUE, length );
      bm_SyncCount( pContext );
      bm_refresh( pContext, 1, BM_LAST_CELL );
   }
}

/* returns an integer index of the item in the visible tree */
int32
BM_GetIndex(MWContext* context, BM_Entry* item)
{
	int32 count = 1;

	CHKCONTEXT(context);

	if (BM_GetRoot(context))
	    return bm_GetIndexNum(BM_GetRoot(context), item, &count);
	else
		return 0;
}

/* returns an integer index of the item in the list and does not pay
	attention to the BM_ATTR_FOLDED value */
int32
BM_GetUnfoldedIndex(MWContext* context, BM_Entry* item)
{
    int32 count = 1;

	BM_Entry* root = BM_GetRoot(context);

	CHKCONTEXT(context);

    if (root)
        return bm_GetUnfoldedIndexNum(root, item, &count);
    else
        return 0;
}

/* returns TRUE if the second argument is a direct
	descendent of the first argument.
	returns FALSE otherwise */
PUBLIC XP_Bool
BM_IsDescendent(MWContext* context, BM_Entry* parent, BM_Entry* possible_child)
{
    int32 count = 1;
	CHKCONTEXT(context);

    if (	parent &&
    		parent->type == BM_TYPE_HEADER &&
    		bm_GetUnfoldedIndexNum(parent, possible_child, &count))
        return TRUE;

    return FALSE;
}


/* returns an integer depth of the item in the list starting at zero */
PUBLIC int32
BM_GetDepth(MWContext* context, BM_Entry* item)
{
	CHKCONTEXT(context);
    if (BM_GetRoot(context))
        return bm_GetDepth(BM_GetRoot(context), item);
    else
        return 0;
}


/* returns the item at "count" visible indexes below "item" */
static BM_Entry*
bm_AtIndex(BM_Entry* item, int32* count)
{
	XP_ASSERT(item);
	XP_ASSERT(item->type == BM_TYPE_HEADER);

	(*count)--;

	/* first check to see if parent is the node we are looking for */
	if (*count <= 0)
		return item;

	if (!BM_ISFOLDED(item))
	{
		BM_Entry*		child;

		child = item->d.header.children;
		while (child)
		{
			if (child->type == BM_TYPE_HEADER)
			{
				BM_Entry*		rv = NULL;

				rv = bm_AtIndex(child, count);
				if (rv)
					return rv;
			}
			else
			{
				(*count)--;
				if (*count <= 0)
					return child;
			}
			child = child->next;
		}
	}
	return NULL;
}

/* returns the object associated with the index returned by BM_GetIndex() */
BM_Entry*
BM_AtIndex(MWContext* context, int32 index)
{
	BM_Frame* f = GETFRAME(context);
    static BM_Frame*		last_f = NULL;
	static BM_Entry*		last_item = NULL;
	static int32			last_index = -1;

	int32					count = index;

	CHKCONTEXT(context);

	/* only used the cached items if last_item is non-NULL and the
	requested index is one more than the last requested index */
	if (last_f == f && last_item && (index == (last_index + 1)))
	{
		/* if we're not a header
			or we're a header but folded,
			or we're a header but have no children,
			just go to the next item and set the local cache */
		if (	(last_item->type != BM_TYPE_HEADER) ||
				(last_item->flags & BM_ATTR_FOLDED) ||
				(! last_item->d.header.children))
		{
			last_item = last_item->next;
			if (last_item)
			{
				last_index = index;
				return last_item;
			}
			else
			{
				last_index = -1;
				return BM_AtIndex(context, index);
			}
		}
		else
		/* we're a header, we're unfolded, and we have children */
		{
			last_item = last_item->d.header.children;
			last_index = index;
			return last_item;
		}
	}

    if (BM_GetRoot(context) && index > 0)
	{
		last_item =	bm_AtIndex(BM_GetRoot(context), &count);
		if (last_item)
		{
		    last_f = f;
			last_index = index;
			return last_item;
		}
	}
	return NULL;
}

PRIVATE BM_Entry*
bm_GetUnfoldedIndex(BM_Entry* parent, int32* index)
{
    BM_Entry* child;
    BM_Entry* rv = 0;

	XP_ASSERT(parent);
	XP_ASSERT(parent->type == BM_TYPE_HEADER);
	child = parent->d.header.children;

    while (child)
	{
		*(index) -= 1;

		if (*index <= 0)
			return child;

		if (child->type == BM_TYPE_HEADER)
		{
			rv = bm_GetUnfoldedIndex(child, index);

			if (rv)
				return rv;
		}
		child = child->next;
	}

    return NULL;
}

/* returns the object associated with the index returned by BM_GetUnfoldedIndex() */
PUBLIC BM_Entry*
BM_AtUnfoldedIndex(MWContext* context, int32 index)
{
  	CHKCONTEXT(context);
	if (BM_GetRoot(context) && index > 0)
		return bm_GetUnfoldedIndex(BM_GetRoot(context), &index);
	else
		return NULL;
}


static void
bm_fold_header_all(MWContext* context, BM_Entry* entry, XP_Bool fold,
				   XP_Bool refresh)
{
  if (BM_ISHEADER(entry)) {
	BM_FoldHeader(context, entry, fold, refresh, FALSE);
	for (entry = entry->d.header.children ; entry ; entry = entry->next) {
	  bm_fold_header_all(context, entry, fold, refresh);
	}
  }
}


/* folds the header bm
	if fold is TRUE, the item becomes folded
		else the item is unfolded
	if refresh is TRUE, the FE is called to
		redraw necessary items
	if foldAll is TRUE, all headers appearing
		below bm in the tree are folded or unfolded
		according to "fold"
*/
PUBLIC void
BM_FoldHeader(MWContext* context, BM_Entry* bm, XP_Bool fold, XP_Bool refresh, XP_Bool foldAll)
{
  BM_Frame* f = GETFRAME(context);
	int32				firstChangedCell = 0;

	CHKCONTEXTVOID(context);
	XP_ASSERT(bm);
	if (!bm) return;

	bm_CancelLastFind(context);

	f->max_depth = 0;
	if (foldAll)
	{
	  bm_start_batch(context);
	  bm_fold_header_all(context, bm, fold, refresh);
	  bm_end_batch(context);
	  return;
	}
	else
	{

		if (BM_ISFOLDED(bm) != fold)
		{
			int32		count;
			firstChangedCell = BM_GetIndex(context, bm);

			count = firstChangedCell;

			if (bm)
			{
				if (fold)
				{
					if (firstChangedCell != 0)
						BM_ClearAllChildSelection(context, bm, FALSE);
					BM_SETFLAG(bm, BM_ATTR_FOLDED);
				}
				else
					BM_CLEARFLAG(bm, BM_ATTR_FOLDED);
			}

		}
	}

	bm_SetModified(context, TRUE);
	bm_SyncCount(context);

	if (foldAll)
		firstChangedCell = MIN(1, firstChangedCell);

	if (refresh && (firstChangedCell != 0))
		bm_refresh(context, firstChangedCell, BM_LAST_CELL);
}



/* clears the selection state of all items in the tree
	if refresh is TRUE, the FE is called to redraw items
	which need to be redrawn
*/
PUBLIC void
BM_ClearAllSelection(MWContext* context, XP_Bool refresh)
{
  BM_Frame* f = GETFRAME(context);
	int32		t = 1;
	CHKCONTEXTVOID(context);

	bm_ClearSelection(context, BM_GetRoot(context), refresh, &t);
	f->gSelectionCount = 0;
	f->gSelectionMask = 0;
}

/*  Clears the selection state of all children of the item passed.
	if refresh is TRUE, the FE is called to redraw items
	which need to be redrawn
*/
PUBLIC void
BM_ClearAllChildSelection(MWContext* context, BM_Entry* at, XP_Bool refresh)
{
	CHKCONTEXTVOID(context);
	if (!at) return;
	if (at->type != BM_TYPE_HEADER)
		return;
    at = at->d.header.children;
	while (at)
	{
		if (BM_ISSELECTED(at))
		  BM_SelectItem(context, at, refresh, TRUE, FALSE);
		if ((at->type == BM_TYPE_HEADER) && (at->d.header.children))
			BM_ClearAllChildSelection(context, at->d.header.children, refresh);
		at = at->next;
	}
}

/* selects the item
	if refresh is TRUE, the FE is called to redraw the item
	if extend is TRUE, the item is added to the selection
		else the selection is cleared and the item becomes
		the selection
	if select is TRUE, the item is selected
		else it is deselected

	if extend is FALSE and select is FALSE, the selection
	becomes empty
*/
PUBLIC void
BM_SelectItem(MWContext* context, BM_Entry* item, XP_Bool refresh,
			  XP_Bool extend, XP_Bool select)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXTVOID(context);
  XP_ASSERT(item);
  if (!item) return;
  bm_start_batch(context);
  bm_CancelLastFind(context);
  if (!extend) {
	BM_ClearAllSelection(context, refresh);
	if (select) f->lastSelectedItem = item;
  }
  if (select) {
	if (!BM_ISSELECTED(item)) f->gSelectionCount++;
	f->gSelectionMask |= item->type;
	BM_SETFLAG(item, BM_ATTR_SELECTED);
  } else {
	BM_CLEARFLAG(item, BM_ATTR_SELECTED);
	f->gSelectionCount = -9999;
  }
  if (refresh) {
	int32 index = BM_GetIndex(context, item);
	if (index) bm_refresh(context, index, index);
  }
  if (!extend) BMFE_EditItem(context, item);
  bm_end_batch(context);
}



static BM_Entry*
bm_validate_selected_item(MWContext* context, BM_Entry* item)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* result;
  for (; item ; item = item->next) {
	if (item == f->lastSelectedItem && BM_ISSELECTED(item)) return item;
	if (BM_ISHEADER(item) && !BM_ISFOLDED(item)) {
	  result = bm_validate_selected_item(context, item->d.header.children);
	  if (result) return result;
	}
  }
  return NULL;
}



static void
bm_select_range(MWContext* context, BM_Entry* item, int32 min, int32 max,
				int32* cur)
{
  for (; item ; item = item->next) {
	if (*cur >= min) {
	  if (*cur > max) return;
	  BM_SelectItem(context, item, FALSE, TRUE, TRUE);
	}
	(*cur)++;
	if (BM_ISHEADER(item) && !BM_ISFOLDED(item)) {
	  bm_select_range(context, item->d.header.children, min, max, cur);
	}
  }
}



void
BM_SelectRangeTo(MWContext* context, BM_Entry* item)
{
  BM_Frame* f = GETFRAME(context);
  int32 min;
  int32 max;
  int32 cur;
  CHKCONTEXTVOID(context);
  XP_ASSERT(item);
  if (!item) return;
  /* First very carefully validate the lastSelectedItem pointer.  That item
	 might have been deleted or something, and the code in question may not
	 have updated the lastSelectedItem pointer.  So, we make sure that it still
	 points to a valid item, and that the item is selected. */
  f->lastSelectedItem = bm_validate_selected_item(context,
												  BM_GetRoot(context));
  if (!f->lastSelectedItem) {
	BM_SelectItem(context, item, TRUE, FALSE, TRUE);
	XP_ASSERT(f->lastSelectedItem == item);	/* Not that we can do much if
											   this fails...*/
	return;
  }
  min = BM_GetIndex(context, f->lastSelectedItem);
  max = BM_GetIndex(context, item);
  if (min < 1 || max < 1) return;
  if (min > max) {
	int32 tmp = min;
	min = max;
	max = tmp;
  }
  bm_start_batch(context);
  BM_ClearAllSelection(context, TRUE);
  cur = 1;
  bm_select_range(context, BM_GetRoot(context), min, max, &cur);
  XP_ASSERT(BM_ISSELECTED(item));				/* More sanity checking; not */
  XP_ASSERT(BM_ISSELECTED(f->lastSelectedItem)); /* really much we can do if
													these assertions fail.*/
  bm_refresh(context, min, max);
  bm_end_batch(context);
}


/* toggles the selected state of the item
(see BM_SelectItem) */
PUBLIC void
BM_ToggleItem(MWContext* context, BM_Entry* item, XP_Bool refresh, XP_Bool extend)
{
	CHKCONTEXTVOID(context);
	XP_ASSERT(item);
	if (!item) return;

	if (item->flags & BM_ATTR_SELECTED)
		BM_SelectItem(context, item, refresh, extend, FALSE);
	else
		BM_SelectItem(context, item, refresh, extend, TRUE);
}



static XP_Bool
bm_ConfirmSave(MWContext* context)
{
/*	XP_Bool		doSave = FALSE;
	char*		msg = "Save changes to %s?\n";

	sprintf(msg, f->gFile);

	if (BM_Modified(context))
		doSave = FE_SimpleConfirm(msg);
	return doSave;
*/
	return TRUE;
}


const char*
BM_GetFileName(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  return f->gFile;
}



static XP_Bool
bm_insert_bogus_aliases(XP_HashTable table, const void* key, void* value,
						void* closure)
{
  MWContext* context = (MWContext*) closure;
  bm_alias_info* info = (bm_alias_info*) value;
  if (info->entry->parent == NULL) {
	/* This was an alias that was made up and inserted because we never could
	   find the real entry for it.  So, now we had better insert it into the
	   tree. */
	if (context->type == MWContextBookmarks) {
	  bm_AppendChildToHeader(context, BM_GetRoot(context), info->entry);
	} else {
	  bm_AddChildToHeaderSorted(context, BM_GetRoot(context), info->entry);
	}
  }
  return TRUE;
}


/* Make sure the address book is sorted. */
static void
bm_resort_headers(MWContext* context, BM_Entry* header)
{
  XP_Bool needssort;
  BM_Entry* entry;
  BM_Entry* prev;
  for ( ; header ; header = header->next) {
	if (BM_ISHEADER(header)) {
	  prev = NULL;
	  needssort = FALSE;
	  for (entry = header->d.header.children ; entry ; entry = entry->next) {
		if (BM_ISHEADER(entry)) bm_resort_headers(context, entry);
		if (prev && bm_SortAddressBook(prev, entry) > 0) {
		  needssort = TRUE;
		}
		prev = entry;
	  }
	  if (needssort) {
		BM_SelectItem(context, header, FALSE, FALSE, TRUE);
		bm_SortSelected(context, BM_Sort_Name);
	  }
	}
  }
}


/* read bmlist file from disk
	pass in a file url */
PUBLIC void
BM_ReadBookmarksFromDisk(MWContext* context, const char* filename,
						 const char* relative_url)
{
  BM_Frame* f = GETFRAME(context);
  XP_File fp;
  char* buffer;
  UndoState* undo;

  CHKCONTEXTVOID(context);

  undo = f->undo;
  if (BM_Modified(context)) {
	if (!bm_ConfirmSave(context)) return;
	if (BM_SaveBookmarks(context, f->gFile) < 0) return;
  }
  if (f->gBookmarks) BM_FreeEntry(context, f->gBookmarks);
  f->gBookmarks = NULL;

  buffer = (char*) XP_ALLOC(READ_BUFFER_SIZE);
  if (!buffer) return;

  /* don't kill ourselves */
  if (f->gFile != filename)
  	StrAllocCopy(f->gFile, filename);
  
  XP_ASSERT(f->gFile != NULL);
  

  if (XP_Stat(filename, &(f->laststat), xpBookmarks) != 0) {
	XP_MEMSET(&(f->laststat), 0, sizeof(f->laststat));
  }

  fp = XP_FileOpen(filename, xpBookmarks, XP_FILE_READ);

  if (!fp) {
	XP_FREE(buffer);
	return;
  }

  /* read in the first line */
  XP_FileReadLine(buffer, READ_BUFFER_SIZE, fp);

  /* DONT REQUIRE THE COOKIE FOR NOW
   *
   * if(XP_STRNCMP(buffer, BMLIST_COOKIE, strlen(BMLIST_COOKIE)
   && XP_STRNCMP(buffer, BM_ADDR_LIST_COOKIE, strlen(BM_ADDR_LIST_COOKIE))
   *   {
   * 	   TRACEMSG(("ERROR! - Hotlist cookie not found in bmlist file"));
   *	   XP_FREE(buffer);
   *	   return;
   *   }
   */

  f->undo = NULL;			/* No need to log all this stuff... */
  bm_start_batch(context);
  bm_refresh(context, 1, BM_LAST_CELL);

  bm_clear_alias_info(context);

  /* gBookmarks shouldn't exist yet! */
  bm_ReadFromHTML(context, fp, NULL, buffer, relative_url);
  bm_SyncCount(context);

  XP_FileClose(fp);

  XP_Maphash(f->aliasTable, bm_insert_bogus_aliases, context);

  if (context->type == MWContextAddressBook) {
	bm_resort_headers(context, BM_GetRoot(context));
  }

  bm_SetModified(context, FALSE);

  XP_FREE(buffer);

  bm_end_batch(context);
  f->undo = undo;
  UNDO_DiscardAll(undo);
}

PUBLIC int32
BM_SaveBookmarks(MWContext* context, const char* filename)
{
  BM_Frame* f = GETFRAME(context);
  XP_File fp = NULL;
  const char* bm_list_name;
  XP_FileType tmptype;
  char* tmpname = NULL;
  XP_StatStruct curstat;
  XP_Bool defaultFile;
  BM_SortType enSortType = f->enSortType;
  /* recognize if we're saving the current bookmarks file */
  defaultFile = (filename == NULL || (f->gFile && XP_STRCMP(filename, f->gFile) == 0));

  CHKCONTEXT(context);

  if (filename == NULL) {
	filename = f->gFile;
	if (filename == NULL) return -1; /* ### */
	if (XP_Stat(filename, &curstat, xpBookmarks) != 0) {
	  /* The stat failed.  Treat it as if the stat gave the same thing as last
		 time (i.e., make sure to *not* whine about the file changing from us;
		 most likely, the user just removed it.) */
	  XP_MEMCPY(&curstat, &(f->laststat), sizeof(curstat));
	}
	if (curstat.st_mtime != f->laststat.st_mtime ||
		curstat.st_size != f->laststat.st_size) {
	  if (f->gBookmarksModified) {
		if (FE_Confirm(context,
					   XP_GetString(context->type == MWContextAddressBook ?
									XP_BKMKS_ADDRESSBOOK_CONFLICT :
									XP_BKMKS_BOOKMARKS_CONFLICT))) {
		  f->gBookmarksModified = FALSE; /* Prevent BM_ReadBookmarksFromDisk
											from calling us back again. */
		  BM_ReadBookmarksFromDisk(context, filename, NULL);
		  return 0;
		}
	  } else {
		FE_Alert(context,
				 XP_GetString(context->type == MWContextAddressBook ?
							  XP_BKMKS_ADDRESSBOOK_CHANGED :
							  XP_BKMKS_BOOKMARKS_CHANGED));
		BM_ReadBookmarksFromDisk(context, filename, NULL);
		return 0;
	  }
	} else {
	  if (!f->gBookmarksModified) return 0; /* No changes need to be saved. */
	}
  }

  /* Save the natural (user arranged) sort */
  if (defaultFile && (enSortType != BM_Sort_Natural)) {
    bm_SortSilent( context, BM_Sort_Natural );
  }

  bm_list_name = FE_UsersFullName();
  if (!bm_list_name) bm_list_name = FE_UsersMailAddress();

  tmpname = FE_GetTempFileFor(NULL, filename, xpBookmarks, &tmptype);
  if (!tmpname || tmpname[0] == 0) goto FAIL;

  fp = XP_FileOpen(tmpname, tmptype, XP_FILE_WRITE);

  if (!fp) goto FAIL;

  /* write cookie */
  if (context->type == MWContextBookmarks) {
	if (bm_write_ok(BMLIST_COOKIE, -1, fp) < 0) goto FAIL;
  } else {
	if (bm_write_ok(BM_ADDR_LIST_COOKIE, -1, fp) < 0) goto FAIL;
  }
  if (bm_write_ok(LINEBREAK, LINEBREAK_LEN, fp) < 0) goto FAIL;

  if (bm_write_ok(XP_GetString(XP_BKMKS_AUTOGENERATED_FILE), -1, fp) < 0) {
	goto FAIL;
  }
  if (bm_write_ok(LINEBREAK, LINEBREAK_LEN, fp) < 0) goto FAIL;
  if (bm_write_ok(XP_GetString(XP_BKMKS_READ_AND_OVERWRITE), -1, fp) < 0) goto FAIL;
  if (bm_write_ok(LINEBREAK, LINEBREAK_LEN, fp) < 0) goto FAIL;
  if (bm_write_ok(XP_GetString(XP_BKMKS_DO_NOT_EDIT), -1, fp) < 0) goto FAIL;
  if (bm_write_ok(LINEBREAK, LINEBREAK_LEN, fp) < 0) goto FAIL;

  if(context->type == MWContextBookmarks) {
  	if(bm_list_name) {
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_SOMEONE_S_BOOKMARKS),
					"<TITLE>", 
					bm_list_name,
					"</TITLE>" LINEBREAK);
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_SOMEONE_S_BOOKMARKS),
					"<H1>",
					bm_list_name,
					"</H1>\n" LINEBREAK);
 	} else {	
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_PERSONAL_BOOKMARKS),
					"<TITLE>",
					"</TITLE>" LINEBREAK);
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_PERSONAL_BOOKMARKS),
					"<H1>",
					"</H1>\n" LINEBREAK);
	}
  } else {
  	if(bm_list_name) {
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_SOMEONE_S_ADDRESSBOOK),
					"<TITLE>", 
					bm_list_name,
					"</TITLE>" LINEBREAK);
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_SOMEONE_S_ADDRESSBOOK),
					"<H1>",
					bm_list_name,
					"</H1>\n" LINEBREAK);
 	} else {
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_PERSONAL_ADDRESSBOOK),
					"<TITLE>",
					"</TITLE>" LINEBREAK);
  		XP_FilePrintf(fp, XP_GetString(XP_BKMKS_PERSONAL_ADDRESSBOOK),
					"<H1>",
					"</H1>\n" LINEBREAK);
	}
  }	

  bm_clear_alias_info(context);

  if (BM_GetRoot(context)) {
	if (bm_WriteAsHTML(context, fp, BM_GetRoot(context), 0, FALSE) < 0) {
	  goto FAIL;
	}
  } else {
	XP_TRACE(("No bmlist to write!"));   
  }

  if (XP_FileClose(fp) != 0) {
	fp = NULL;
	goto FAIL;
  }
  fp = NULL;
  XP_FileRename(tmpname, tmptype, filename, xpBookmarks);
  XP_FREE(tmpname);
  tmpname = NULL;

#ifdef XP_UNIX
  /* If we write the bookmarks with at different uid or gid 
   * than what it had, try to change it back.
   * Fix for 67572, bookmarks.html get wiped out.
   */
  if (curstat.st_uid != getuid()
	  || curstat.st_gid != getgid()) {
	chown (filename, curstat.st_uid, curstat.st_gid);
  }
#endif /* XP_UNIX */

  /* only update global mod date if we saved the current bookmk file */ 
  if (defaultFile) {
  
    if (XP_Stat(filename, &(f->laststat), xpBookmarks) != 0) {
		XP_MEMSET(&(f->laststat), 0, sizeof(f->laststat));
	  }
	
	  bm_SetModified(context, FALSE);

    /* Reset the previous sort order */          
    if (enSortType != BM_Sort_Natural) {
      bm_SortSilent( context, enSortType );
    }
      
  }

  if (context->type == MWContextBookmarks) 
      f->errorSavingBookmarks = FALSE;

  return 1;

FAIL:
  if (fp) XP_FileClose(fp);
  if (tmpname) {
    XP_FileRemove(tmpname, tmptype);
    XP_FREE(tmpname);
    tmpname = NULL;
  }

  if (context->type == MWContextAddressBook) {
      FE_Alert(context, XP_GetString(XP_BKMKS_CANT_WRITE_ADDRESSBOOK));
  }
  else {
      if (!f->errorSavingBookmarks) {
          FE_Alert(context, XP_GetString(XP_BKMKS_CANT_WRITE_BOOKMARKS));
          f->errorSavingBookmarks = TRUE;
      }
  }

  return -1;
}


/* returns the first selected item
	if parent is selected, parent is returned,
	otherwise the first selected child is returned
	if there is no selected item following parent,
	NULL is returned */
PRIVATE BM_Entry*
bm_FirstSelectedItem_1(BM_Entry* parent)
{
	BM_Entry*		child;

	XP_ASSERT(parent);
	XP_ASSERT(BM_ISHEADER(parent));

	if (BM_ISSELECTED(parent)) return parent;

	child = parent->d.header.children;
	while (child)
	{
		if (child->flags & BM_ATTR_SELECTED)
			return child;
		if (child->type == BM_TYPE_HEADER)
		{
			BM_Entry*	rv;
			rv = bm_FirstSelectedItem_1(child);
			if (rv)
				return rv;
		}
		child = child->next;
	}
	return NULL;
}

PUBLIC BM_Entry*
BM_FirstSelectedItem(MWContext* context)
{
  CHKCONTEXT(context);
  return bm_FirstSelectedItem_1(BM_GetRoot(context));
}

static void
bm_InsertBySelection(MWContext* context, BM_Entry* firstSelected,
					 BM_Entry* newItem)
{
  BM_Frame* f = GETFRAME(context);
  CHKCONTEXTVOID(context);
  XP_ASSERT(firstSelected);
  if (!firstSelected) firstSelected = BM_GetRoot(context);

  /* If we've selected the root node, then make sure it's not folded, so that
	 we will be sure to put our new item inside the root, where it belongs. */

  if (firstSelected == BM_GetRoot(context) && BM_ISFOLDED(firstSelected)) {
	BM_FoldHeader(context, firstSelected, FALSE, TRUE, FALSE);
  }

  f->max_depth = 0;

  if (context->type == MWContextAddressBook && !BM_ISALIAS(newItem)) {
	bm_AddChildToHeaderSorted(context, BM_GetRoot(context), newItem);
  } else {
	/* insert into header if it's open, else after it */
	if (BM_ISHEADER(firstSelected) && !BM_ISFOLDED(firstSelected)) {
	  if (context->type == MWContextBookmarks) {
		BM_PrependChildToHeader(context, firstSelected, newItem);
	  } else {
		bm_AddChildToHeaderSorted(context, firstSelected, newItem);
	  }
	} else {
	  if (context->type == MWContextBookmarks) {
		bm_InsertItemAfter(context, firstSelected, newItem, TRUE);
	  } else {
		bm_AddChildToHeaderSorted(context, firstSelected->parent, newItem);
	  }
	}
  }
}

static void
bm_BeginEditNewHeader(MWContext* context)
{
	BM_Entry* header;
	BM_Entry* firstSelected;
	int32 index;


	firstSelected = BM_FirstSelectedItem(context);
	if (firstSelected)
		index = BM_GetIndex(context, firstSelected);
	else
	{
		firstSelected = BM_GetRoot(context);
		index = 1;
	}

	header = BM_NewHeader(XP_GetString(XP_BKMKS_NEW_HEADER));

	BM_SETFLAG(header, BM_ATTR_ISNEW);

	bm_InsertBySelection(context, firstSelected, header);
	BM_SelectItem(context, header, TRUE, FALSE, TRUE);
	bm_SyncCount(context);
	bm_refresh(context,
			   context->type == MWContextBookmarks ? index + 1 : 1,
			   BM_LAST_CELL);
	BMFE_OpenBookmarksWindow(context);
	BMFE_EditItem(context, header);
}

static void
bm_BeginEditNewUrl(MWContext* context)
{
	BM_Entry*			url;
	BM_Entry*		firstSelected;
	int32			index;

	firstSelected = BM_FirstSelectedItem(context);
	if (firstSelected)
		index = BM_GetIndex(context, firstSelected);
	else
	{
		firstSelected = BM_GetRoot(context);
		index = 1;
	}

	if (context->type == MWContextBookmarks) {
	  url = BM_NewUrl(XP_GetString(XP_BKMKS_NEW_BOOKMARK), NULL, NULL, 0);
	} else {
	  url = bm_NewAddress("", "");
	}
	if (!url) return;
	BM_SETFLAG(url, BM_ATTR_ISNEW);
	if (context->type == MWContextBookmarks) {
	  bm_InsertBySelection(context, firstSelected, url);
	} else {
	  bm_AddChildToHeaderSorted(context, BM_GetRoot(context), url);
	}
	BM_SelectItem(context, url, TRUE, FALSE, TRUE);
	bm_SyncCount(context);
	bm_refresh(context,
			   context->type == MWContextBookmarks ? index + 1 : 1,
			   BM_LAST_CELL);
	BMFE_OpenBookmarksWindow(context);
	BMFE_EditItem(context, url);
}

PUBLIC void
BM_GotoBookmark(MWContext* context, BM_Entry* item)
{
	char*	url = NULL;
	char*	target = NULL;

	CHKCONTEXTVOID(context);

	XP_ASSERT(item);
	if (!item) return;

	if (item->type == BM_TYPE_ALIAS)
	{
	  XP_ASSERT(BM_ISURL(item->d.alias.original));
	  if (BM_ISURL(item->d.alias.original)) {
		url = item->d.alias.original->d.url.address;
		target = item->d.alias.original->d.url.target;
	  }
	}
	else if (item->type == BM_TYPE_URL)
	{
		url = item->d.url.address;
		target = item->d.url.target;
	}

	if (url)
		BMFE_GotoBookmark(context, url, target);
}

static void
bm_BeginFindBookmark(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  if (!f->gFindInfo) {
	f->gFindInfo = XP_NEW_ZAP(BM_FindInfo);
	if (!f->gFindInfo) return;
	if (context->type == MWContextAddressBook) {
	  f->gFindInfo->checkNickname = TRUE;
	}
	f->gFindInfo->checkName = TRUE;
	f->gFindInfo->checkLocation = TRUE;
	f->gFindInfo->checkDescription = TRUE;
  }
  f->gFindInfo->lastEntry = NULL;
  f->gTemporary = BMFE_OpenFindWindow(context, f->gFindInfo);
}


PRIVATE void
bm_SelectAliases(MWContext* context, BM_Entry* at, BM_Entry* forEntry)
{
  BM_Entry* head;
  for ( ; at ; at = at->next) {
	if (at->type == BM_TYPE_HEADER) {
	  bm_SelectAliases(context, at->d.header.children, forEntry);
	} else if (BM_ISALIAS(at) && at->d.alias.original == forEntry) {
	  BM_SelectItem(context, at, TRUE, TRUE, TRUE);
	  for (head = at->parent ; head ; head = head->parent) {
		if (BM_ISFOLDED(head)) {
		  BM_FoldHeader(context, head, FALSE, TRUE, FALSE);
		}
	  }
	}
  }
}

PUBLIC void
BM_SelectAliases(MWContext* context, BM_Entry* forEntry)
{
  CHKCONTEXTVOID(context);
  XP_ASSERT(forEntry);
  if (forEntry) {
	bm_start_batch(context);
	bm_SelectAliases(context, BM_GetRoot(context), forEntry);
	bm_end_batch(context);
	BMFE_ScrollIntoView(context, forEntry);
  }
}

static void
bm_CloseLastFind_1(MWContext* context, BM_Entry* entry, XP_Bool closeit)
{
  for ( ; entry ; entry = entry->next) {
	if (BM_ISHEADER(entry)) {
	  if (entry->flags & BM_ATTR_FINDAFF) {
		if (closeit) BM_FoldHeader(context, entry, TRUE, TRUE, FALSE);
		BM_CLEARFLAG(entry, BM_ATTR_FINDAFF);
	  }
	  bm_CloseLastFind_1(context, entry->d.header.children, closeit);
	}
  }
}


/* Close any headers that we may have opened last time we did a find. */
static void
bm_CloseLastFind(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  if (f->unfoldedForFind) {
	f->unfoldedForFind = FALSE; /* Do this first, to not confuse
									  BM_FoldHeader(). */
	bm_CloseLastFind_1(context, BM_GetRoot(context), TRUE);
  }
}

/* Forget about any headers that were opened last time we did a find; leave
   things the way they are now. */
static void
bm_CancelLastFind(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  if (f->unfoldedForFind) {
	f->unfoldedForFind = FALSE;
	bm_CloseLastFind_1(context, BM_GetRoot(context), FALSE);
  }
}

static void
bm_OpenNewFind(MWContext* context, BM_Entry* entry)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* head;
  XP_Bool found = FALSE;
  if (!entry) return;
  bm_CloseLastFind(context);
  for (head = entry->parent ; head ; head = head->parent) {
	if (BM_ISFOLDED(head)) {
	  BM_FoldHeader(context, head, FALSE, TRUE, FALSE);
	  BM_SETFLAG(head, BM_ATTR_FINDAFF);
	  found = TRUE;
	}
  }
  f->unfoldedForFind = found; /* Must set last, to not confuse
									BM_FoldHeader. */
}


void
BM_DoFindBookmark(MWContext* context, BM_FindInfo* findInfo)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry* found = NULL;
  BM_Entry* startAt = NULL;
  XP_Bool unfoldedForFind;

  /* If no find string is specified, return. */
  if (findInfo->textToFind == NULL)
      return;

  bm_CloseLastFind(context);

  if (findInfo->lastEntry) {
	startAt = bm_GetNextSpanningWrapping(context, findInfo->lastEntry);
  } else {
	startAt = BM_GetRoot(context);
  }
  found = bm_DoFindBookmark_1(context, startAt, findInfo);

  if (found) {
	bm_CloseLastFind(context);
	bm_OpenNewFind(context, found);
	unfoldedForFind = f->unfoldedForFind;
	f->unfoldedForFind = FALSE; /* Don't confuse BM_SelectItem */
	BM_SelectItem(context, found, TRUE, FALSE, TRUE);
	bm_flush_updates(context);
	BMFE_ScrollIntoView(context, found);
	f->unfoldedForFind = unfoldedForFind;
  } else {
	FE_Alert(context, XP_GetString(XP_BKMKS_NOT_FOUND));
  }
  findInfo->lastEntry = found;
}


static void
bm_parse_mailto(const char* url, char** name, char** addr)
{
  char* ptr;
  char* buf;
  int32 L;
  if (name) *name = NULL;
  if (addr) *addr = NULL;
  if (strncasecomp(url, "mailto:?to=", 11) != 0) return;
  url += 11;
  ptr = XP_STRCHR(url, '&');
  L = (ptr ? (ptr - url) : XP_STRLEN(url));
  buf = (char *) XP_ALLOC(L+1);
  if (!buf) return;
  XP_MEMCPY (buf, url, L);
  buf[L] = 0;
  buf = NET_UnEscape(buf);

#if 0
  MSG_ParseRFC822Addresses(buf, name, addr);
#else
  /* We need to do it this way to get msg_quote_phrase_or_addr() to be
	 called on the names.  Perhaps that function should just be exported...
   */
#ifdef MOZ_MAIL_NEWS
  if (name) *name = MSG_ExtractRFC822AddressNames (buf);
  if (addr) *addr = MSG_ExtractRFC822AddressMailboxes (buf);
#endif /* MOZ_MAIL_NEWS */
#endif

  XP_FREE(buf);
}

BM_Entry*
BM_FindAddress(MWContext* context, const char* url)
{
  char* address;
  BM_Entry* result = NULL;
  BM_Entry* entry;
  CHKCONTEXT(context);
  XP_ASSERT(url);
  if (!url) return NULL;
  bm_parse_mailto(url, NULL, &address);
  if (!address) return NULL;

  /* Takes advantage of the fact that addressbook always has all address
	 entries as children of the root header. */
  for (entry = BM_GetRoot(context)->d.header.children;
	   entry;
	   entry = entry->next) {
	if (BM_ISADDRESS(entry) &&
		XP_STRCMP(entry->d.address.address, address) == 0) {
	  result = entry;
	  break;
	}
  }
  XP_FREE(address);
  return result;
}


void
BM_EditAddress(MWContext* context, const char* url)
{
  BM_Entry* entry;
  CHKCONTEXTVOID(context);
  XP_ASSERT(url);
  if (!url) return;
  bm_start_batch(context);
  entry = BM_FindAddress(context, url);
  if (!entry) {
	char* name;
	char* address;
	bm_parse_mailto(url, &name, &address); /* Parsing a second time.  Oh,
											  well. ### */
	if (!name) name = XP_STRDUP("");
	if (!address) address = XP_STRDUP("");
	if (!name || !address) goto FAIL;

	entry = bm_NewAddress(name, address);
	XP_FREE(name);
	XP_FREE(address);
	if (!entry) goto FAIL;
	BM_SETFLAG(entry, BM_ATTR_ISNEW);
	bm_AddChildToHeaderSorted(context, BM_GetRoot(context), entry);
	BM_SelectItem(context, entry, TRUE, FALSE, TRUE);
	bm_SyncCount(context);
	bm_refresh(context, 1, BM_LAST_CELL);
  }
  BMFE_OpenBookmarksWindow(context);
  BMFE_EditItem(context, entry);
FAIL:
  bm_end_batch(context);
}




/*
 * Utilities to fuss with bookmarks in a drag and drop evnironment
 *
 * There are two user visible functions in this file:
 *
 * Allocate and return a string that contains the text representation of
 *   a list of bookmarks entries (including headers and their contents).
 * The caller is responsible for freeing the string.  The total length of
 *   the block that was allocated is returned in lTotalLen.  List is the
 *   list of pointers to bookmarks items that are selected, iCount is the
 *   length of that list.
 * This function has two modes of operation, a short mode and a long mode.
 *   If bLongFormat == FALSE the returned block just has URLs separated by
 *   \n's.  If bLongFormat == TRUE all of the information needed to recreate
 *   the bookmarks item is included
 *

PUBLIC char *
BM_ConvertSelectionsToBlock(BM_Entry ** list,
                             int iCount,
                             int bLongFormat,
                             int32 * lTotalLen);
 *
 * ------------------------
 *
 * Take a block of memory formatted by BM_ConvertSelectionsToBlock and insert
 *   the items it represents into the bookmarks following 'item'.  If item is
 *   NULL insert at the beginning of the bookmarks.  bLongFormat has the same
 *   meaning as in BM_ConvertSelectionsToBlock().  lTotalLen should be the
 *   length of the block of memory --- I'm not sure if this is necessary
 *   because on Windows at least the value we get back is meaningless, so
 *   this function just ignores it.
 *
PUBLIC void
BM_InsertBlockAt(char * pOriginalBlock,
                  BM_Entry * item,
                  int bLongFormat,
                  int32 lTotalLen);
 *
 */



#define TEXT_INDENT 3

/*
 * Measure a boring URL bookmarks entry and return the length in bytes
 *
 * Short format:
 *  "     item->address\n"
 *    where there are nIndent number of spaces before the item
 *
 * Long format:
 *  uint16		type
 *  char		item->name\n
 *  char		item->address\n
 *  time_t		addition_date
 *  time_t		last_visit_date
 *  time_t		last_modified_date
 *  char		item->description\0
 *
 * The item->description field is *NOT* \n terminated since it might
 *  be a multi-line string and is therefore \0 terminated
 */
PRIVATE int32
bm_measure_URL(BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	int32		iSpace = 0;

	if (!item)
        return 0;

	XP_ASSERT(BM_ISURL(item));

    /* NO. We cannot check for ISSELECTED here. We could be called
     * by bm_measure_Header which was selected although we by
	 * ourselves are not selected.
	 *
	 * if (! BM_ISSELECTED(item))
	 *	return 0;
	 */

	if (bLongFormat)
    {
		iSpace += 	sizeof(item->type) +
					sizeof(item->addition_date) +
					sizeof(item->d.url.last_visit) +
		  			sizeof(item->d.url.last_modified);

		if (item->name)
			iSpace += XP_STRLEN(item->name);
		iSpace++;	/* +1 for '\n' */
		if (item->description)
			iSpace += XP_STRLEN(item->description);
		iSpace++;	/* +1 for '\0' */
	}
	else
	{
		/* space indentation and '\n' terminator */
		iSpace = nIndent;
    }

	/* the address appears in both formats */
	if (item->d.url.address)
		iSpace += (XP_STRLEN(item->d.url.address) + 1); /* +1 for '\n' */

	return (iSpace);
}

PRIVATE int32
bm_measure_Alias(BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	int32		iSpace = 0;

	XP_ASSERT(BM_ISALIAS(item));

	if (bLongFormat)
    {
		iSpace += sizeof(item->type);
	}
	return (iSpace);
}

/*
 * Measure a separator entry and return the length in bytes
 *
 * Short format:
 *  "     -------------\0"
 *    where there are nIndent number of spaces before the 13 -'s
 *
 * Long format:
 *  uint16		type
 *
 */
PRIVATE int32
bm_measure_Separator(BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	int32		iSpace = 0;

	if (!item)
        return 0;

	XP_ASSERT(BM_ISSEPARATOR(item));

	if (bLongFormat)
    {
		iSpace += 	sizeof(item->type);
	}
	else
	{
		/* space indentation and '\n' terminator */
		iSpace = nIndent;
		iSpace += 13;
		iSpace ++ /* for '\n' */;
    }

	return (iSpace);
}

/*
 * Measure a header entry and all its children
 *
 * Short format:
 *  "     item->name\n"
 *  "        child1->address\n"
 *  "        child2->address\n"
 *    where there are nIndent number of spaces before the item and
 *    TEXT_INDENT spaces between levels
 *
 * Long format:
 *  uint16		type
 *  char		item->name\n
 *  time_t		addition_date
 *  uint32		number of children
 *  char		item->description\0
 *
 * The item->description field is *NOT* \n terminated since it might
 *  be a multi-line string and is therefore \0 terminated.  Note that
 *  the address field is *NOT* written for headers since its it meaningless
 */
PRIVATE int32
bm_measure_Header(BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	int32		iSpace = 0;
	BM_Entry*	child = NULL;

	if (!item)
		return 0;

	XP_ASSERT(BM_ISHEADER(item));

	/* if the header is selected, count it as well */
	if (bLongFormat)
	{
		iSpace += 	sizeof(item->type) +
					sizeof(item->addition_date) +
					sizeof(item->d.header.childCount);

		if (item->description)
			iSpace += XP_STRLEN(item->description);
		iSpace++;	/* for \0 */
	}
	else
	{
		/* space indentation and '\n' terminator */
		iSpace = nIndent;
	}

	/* the name appears in both formats */
	if (item->name)
		iSpace += XP_STRLEN(item->name);
	iSpace ++;	/* for \n terminator */

	/* measure the amount of space taken up by this item's children */
	child = item->d.header.children;
	while (child)
	{
		switch (child->type)
		{
			case BM_TYPE_URL:
				iSpace += bm_measure_URL(child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			case BM_TYPE_ALIAS:
				iSpace += bm_measure_Alias(child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			case BM_TYPE_HEADER:
				iSpace += bm_measure_Header(child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			case BM_TYPE_SEPARATOR:
				iSpace += bm_measure_Separator(child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			default:
			break;
		}
		child = child->next;
	}

	return iSpace;
}

/*
 * Write out a separator bookmarks entry.
 */
PRIVATE char*
bm_write_Separator(char* buffer, BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	int32				iLen;
    BM_Type				type;

	if (!item || !buffer)
	    return buffer;

	XP_ASSERT(BM_ISSEPARATOR(item));

	if (bLongFormat)
	{
		/* copy the type */
		type = item->type;
		iLen = sizeof(BM_Type);
		XP_MEMCPY(buffer, &type, iLen);
		buffer += iLen;
	}
	else
	{
		XP_MEMSET(buffer, ' ', nIndent);
		buffer += nIndent;
		XP_MEMSET(buffer, '-', 13);
		buffer += 13;
		*buffer++ = '\0';
	}

	return buffer;
}

/*
 * Write out a boring URL bookmarks entry.  See comment at the top of
 *   bm_measure_URL for the format used.  Assume we start writing at
 *   the start of the buffer passed in.  Return a pointer to where the
 *   buffer ends when we get done.
 */
PRIVATE char*
bm_write_URL(char* buffer, BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	int32				iLen;
    BM_Date				lVal;
    BM_Type				type;

	if (!item || !buffer)
	    return buffer;

	XP_ASSERT(BM_ISURL(item) || BM_ISALIAS(item));

	if (bLongFormat)
	{
		/* copy the type */
		type = item->type;
		iLen = sizeof(BM_Type);
		XP_MEMCPY(buffer, &type, iLen);
		buffer += iLen;

		if (BM_ISALIAS(item))
			return buffer;

		/* copy the name */
		if (item->name)
		{
			iLen = XP_STRLEN(item->name);
			XP_MEMCPY(buffer, item->name, iLen);
			buffer += iLen;
		}
		/* put the \n terminator on */
		*buffer++ = '\n';

		/* copy the address */
		if (item->d.url.address)
		{
			iLen = XP_STRLEN(item->d.url.address);
			XP_MEMCPY(buffer, item->d.url.address, iLen);
			buffer += iLen;
		}
		/* put the \n terminator on */
		*buffer++ = '\n';

		/* addition date */
		lVal = item->addition_date;
		iLen = sizeof(BM_Date);
		XP_MEMCPY(buffer, &lVal, iLen);
		buffer += iLen;

		/* last visit date */
		lVal = item->d.url.last_visit;
		iLen = sizeof(BM_Date);
		XP_MEMCPY(buffer, &lVal, iLen);
		buffer += iLen;

		/* last modified date */
		lVal = item->d.url.last_modified;
		iLen = sizeof(BM_Date);
		XP_MEMCPY(buffer, &lVal, iLen);
		buffer += iLen;

		/* copy the description */
		if (item->description)
		{
			iLen = XP_STRLEN(item->description);
			XP_MEMCPY(buffer, item->description, iLen);
			buffer += iLen;
		}
		/* put the \n terminator on */
		*buffer++ = '\0';

	}
	else if (BM_ISURL(item))
	{
		XP_MEMSET(buffer, ' ', nIndent);
		buffer += nIndent;

		if(item->d.url.address)
		{
			XP_STRCPY(buffer, item->d.url.address);
			buffer += XP_STRLEN(item->d.url.address);
		}
		*buffer++ = '\n';
	}

	return buffer;
}


/*
 * Write out a bookmarks header entry.  See comment at the top of
 *   bm_measure_Header for the format used.  Assume we start writing at
 *   the start of the buffer passed in.  Return a pointer to where the
 *   buffer ends when we get done.
 */
PRIVATE char*
bm_write_Header(char* buffer, BM_Entry* item, XP_Bool bLongFormat, int nIndent)
{
	long			iLen;
	BM_Date			lVal;
	BM_Type			type;
	uint32			children;
	BM_Entry*		child = NULL;

	if (!item || !buffer)
	    return buffer;

	XP_ASSERT(BM_ISHEADER(item));

	if (bLongFormat)
	{
		/* copy the type */
		type = item->type;
		iLen = sizeof(BM_Type);
		XP_MEMCPY(buffer, &type, iLen);
		buffer += iLen;

		/* copy the name */
		if (item->name)
		{
			iLen = XP_STRLEN(item->name);
			XP_MEMCPY(buffer, item->name, iLen);
			buffer += iLen;
		}
		/* put the \n terminator on */
		*buffer++ = '\n';

		/* addition date */
		lVal = item->addition_date;
		iLen = sizeof(BM_Date);
		XP_MEMCPY(buffer, &lVal, iLen);
		buffer += iLen;

		/* number of children */
		children = item->d.header.childCount;
		iLen = sizeof(uint32);
		XP_MEMCPY(buffer, &children, iLen);
		buffer += iLen;

		/* copy the description */
		if (item->description)
		{
			iLen = XP_STRLEN(item->description);
			XP_MEMCPY(buffer, item->description, iLen);
			buffer += iLen;
		}
		/* put the \n terminator on */
		*buffer++ = '\0';

	}
	else
	{
		XP_MEMSET(buffer, ' ', nIndent);
		buffer += nIndent;
		if(item->name)
		{
			XP_STRCPY(buffer, item->name);
			buffer += XP_STRLEN(item->name);
		}
		*buffer++ = '\n';
	}

	child = item->d.header.children;
	while (child)
	{
		switch (child->type)
		{
			case BM_TYPE_URL:
			case BM_TYPE_ALIAS:
				buffer = bm_write_URL(buffer, child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			case BM_TYPE_HEADER:
				buffer = bm_write_Header(buffer, child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			case BM_TYPE_SEPARATOR:
				buffer = bm_write_Separator(buffer, child, bLongFormat, nIndent + TEXT_INDENT);
			break;
			default:
			break;
	    }
		child = child->next;
	}

	return buffer;
}

/*
 * Take a separator packed in a block the way bm_write_Separator packs it.
 * Return the new item if we created one
 */
PRIVATE BM_Entry*
bm_read_Separator(char* buffer, XP_Bool bLongFormat, int32* lBytesEaten)
{
	BM_Entry* new_item = NULL;

	if (!buffer)
		return NULL;

	if (bLongFormat)
	{
		/* for now the separator written has only the type.
		   since that was already read in before this was clled
		   we have nothing to eat here */
		new_item = bm_NewSeparator();
		*lBytesEaten = 0;
	}
	else
	{
		/* we should really strip leading whitespace */
		new_item = bm_NewSeparator();
		*lBytesEaten = XP_STRLEN(buffer) + 1;
	}

	return new_item;
}

/*
 * Take a URL packed in a block the way bm_write_URL packs it.
 * Return the new item if we created one
 */
PRIVATE BM_Entry*
bm_read_url_long(char* buffer, XP_Bool bLongFormat, int32* lBytesEaten)
{
	BM_Entry* new_item = NULL;

	if (!buffer)
		return NULL;

	if (bLongFormat)
	{
		BM_Date		addition;
		BM_Date		visit;
		BM_Date		modified;

		/* get the name */
		char* name = buffer;
		char* address = strchr(name, '\n');
		char* description = NULL;
		char* ptr;
		if (!address)
		    return NULL;
		*address++ = '\0';

		/* get the address */
		ptr = strchr(address, '\n');
		if(!ptr)
		    return NULL;
		*ptr++ = '\0';

		/* addition date */
		XP_MEMCPY(&addition, ptr, sizeof(BM_Date));
		ptr += sizeof(BM_Date);

		/* visiting date */
		XP_MEMCPY(&visit, ptr, sizeof(BM_Date));
		ptr += sizeof(BM_Date);

		/* modified date */
		XP_MEMCPY(&modified, ptr, sizeof(BM_Date));
		ptr += sizeof(BM_Date);

		/* get the description (it should be NULL terminated) */
		description = ptr;

		/* we should really strip leading whitespace */
		new_item = BM_NewUrl(name, address, 0, visit);
		new_item->addition_date = addition;
		new_item->description = XP_STRDUP(description);
		new_item->d.url.last_modified = modified;
		*lBytesEaten = XP_STRLEN(description) + (description - buffer) + 1;

	}
	else
	{
		char* end = strchr(buffer, '\n');

		/* if there was a return NULL terminate the current string */
		if (end)
		    *end++ = '\0';

		/* we should really strip leading whitespace */
		new_item = BM_NewUrl(buffer, buffer, 0, 0);
		new_item->addition_date = XP_TIME();
		*lBytesEaten = XP_STRLEN(buffer) + 1;
	}

	return new_item;
}

/*
 * Take a header and children packed in a block the way bm_write_Header
 * packs it.  Return the new header item if we created one
 */
PRIVATE BM_Entry*
bm_read_header_long(MWContext* context, char* buffer, XP_Bool bLongFormat,
			   int32* lBytesEaten)
{
	uint32			kids = 0;
	BM_Type			type;
	BM_Entry*		new_item = NULL;
	BM_Entry*		kid = NULL;
	char*			name = NULL;
	char*			ptr = NULL;
	char*			description = NULL;
	BM_Date			addition;
	uint32			i;
	int32			lEat;

	if (!buffer)
	    return NULL;

	/* can only read long format headers */
	if (bLongFormat)
	{
		/* get the name */
		name = buffer;
		ptr = strchr(name, '\n');
		description = NULL;
		if (!ptr)
		    return NULL;

		/* skip over the \n but change it to a \0 so strcpy() will work */
		*ptr++ = '\0';

		/* addition date */
		XP_MEMCPY(&addition, ptr, sizeof(BM_Date));
		ptr += sizeof(BM_Date);

		/* number of children to read */
		XP_MEMCPY(&kids, ptr, sizeof(uint32));
		ptr += sizeof(uint32);

		/* get the description (it should be NULL terminated) */
		description = ptr;

		/* we should really strip leading whitespace */
		new_item = BM_NewHeader(name);
		new_item->addition_date = addition;
		new_item->description = XP_STRDUP(description);
		*lBytesEaten = XP_STRLEN(description) + (description - buffer) + 1;

		/* handle all of the kids now */
		if (kids)
		{
			buffer += *lBytesEaten;

			for (i = 0; i < kids; i++)
			{
				/* determine the type of the next entry */
				XP_MEMCPY(&type, buffer, sizeof(BM_Type));
				buffer += sizeof(BM_Type);
				*lBytesEaten += sizeof(BM_Type);

				switch (type)
				{
					case BM_TYPE_URL:
					    kid = bm_read_url_long(buffer, bLongFormat, &lEat);
					    *lBytesEaten += lEat;
					    buffer += lEat;
						bm_AppendChildToHeader(context, new_item, kid);
				    break;
					case BM_TYPE_ALIAS:
					break;
					case BM_TYPE_HEADER:
					    kid = bm_read_header_long(context, buffer, bLongFormat, &lEat);
					    *lBytesEaten += lEat;
					    buffer += lEat;
					    bm_AppendChildToHeader(context, new_item, kid);
				    break;
					case BM_TYPE_SEPARATOR:
					    kid = bm_read_Separator(buffer, bLongFormat, &lEat);
					    *lBytesEaten += lEat;
					    buffer += lEat;
					    bm_AppendChildToHeader(context, new_item, kid);
				    break;
					case 12345:
						/* Ah ha! this is the end marker we wrote. Something
						   terribly wrong here. We shouldn't have hit this
						   before we read all the kids in. */
						abort();
					break;
					default:
				    /* bogus type.  Who knows whats going on.  Just quit and get out */
				    break;
				}

			}

	    }
	}

	return new_item;
}

PRIVATE int32
bm_measure(BM_Entry* root, XP_Bool bLongFormat, int32 indent)
{
	int32		iSpace = 0;
	BM_Entry*	child;

	XP_ASSERT(root);
	XP_ASSERT(BM_ISHEADER(root));

	child = root->d.header.children;

	while (child)
	{
		switch (child->type)
		{
			case BM_TYPE_URL:
				if (BM_ISSELECTED(child))
					iSpace += bm_measure_URL(child, bLongFormat, indent);
			break;
			case BM_TYPE_ALIAS:
				iSpace += bm_measure_Alias(child, bLongFormat, indent + TEXT_INDENT);
			break;
			case BM_TYPE_HEADER:
				if (BM_ISSELECTED(child))
					iSpace += bm_measure_Header(child, bLongFormat, indent);
				else
					if (! BM_ISFOLDED(child))
						iSpace += bm_measure(child, bLongFormat, indent);
			break;
			case BM_TYPE_SEPARATOR:
				if (BM_ISSELECTED(child))
					iSpace += bm_measure_Separator(child, bLongFormat, indent);
			break;
		}
		child = child->next;
	}
	return iSpace;
}

PRIVATE char*
bm_write(char* buffer, BM_Entry* root, XP_Bool bLongFormat, int32 indent)
{
	BM_Entry*	child;

	XP_ASSERT(root);
	XP_ASSERT(BM_ISHEADER(root));

	child = root->d.header.children;

	while (child)
	{
		switch (child->type)
		{
			case BM_TYPE_URL:
			case BM_TYPE_ALIAS:
				if (BM_ISSELECTED(child))
					buffer = bm_write_URL(buffer, child, bLongFormat, indent);
			break;

			case BM_TYPE_HEADER:
				if (BM_ISSELECTED(child))
					buffer = bm_write_Header(buffer, child, bLongFormat, indent);
				else
					if (! BM_ISFOLDED(child))
						buffer = bm_write(buffer, child, bLongFormat, indent);
			break;
			case BM_TYPE_SEPARATOR:
				if (BM_ISSELECTED(child))
					buffer = bm_write_Separator(buffer, child, bLongFormat, indent);
			break;
		}
		child = child->next;
	}
	return buffer;
}

/*
 * Allocate and return a string that contains the text representation of
 *   a list of bookmarks entries from specified selections in a History window.
 * The caller is responsible for freeing the string
 */
PUBLIC char *
BM_ClipCopyHistorySelection( void *pHistCsr, uint32 *pSelections, int iCount, int *pSize, XP_Bool bLongFormat )
{
   int i, iLen, iSize = 0;
   uint16 marker;
   char *      pStorage = NULL;
   char *      pStgCsr  = NULL;
   BM_Entry *  pBM      = NULL;
   BM_Entry *  pPrevBM  = NULL;   
   BM_Entry *  pCsr     = NULL;      
   BM_Entry *  pBMList  = NULL;         
   
   if( !pHistCsr || !pSelections || iCount <= 0 )
   {
      return NULL;
   }
   
   /*
    * Build the list of BM_Entrys and calc the total size
    */
   for( i = 0; i < iCount; i++ )
   {
      gh_HistEntry *pHistEntry = GH_GetRecord( pHistCsr, pSelections[i] );
      if( !pHistEntry )
      {
         /* In case history file was somehow compromised */
         continue;
      }

      /*
       * Map the gh_HistEntry to the BM_Entry
       */
            
      pBM = (BM_Entry *)XP_ALLOC( sizeof(BM_Entry) );
	  XP_MEMSET( pBM, 0, sizeof(BM_Entry) );
      if( !pPrevBM )
      {
         pBMList = pBM;
      }
      else
      {
         pPrevBM->next = pBM;
      }
      pPrevBM = pBM;

      /* type */
      pBM->type = BM_TYPE_URL;

      if( bLongFormat )
      {      
         /* name */
         iLen = pHistEntry->pszName ? XP_STRLEN( pHistEntry->pszName )+1 : 0;
         pBM->name = iLen ? (char *)XP_ALLOC( iLen*sizeof(char) ) : NULL;
         if( iLen )
         {
            XP_STRCPY( pBM->name, pHistEntry->pszName );
         }
         
         /* addition date */
         time( &pBM->addition_date );

         /* last visit date */
         pBM->d.url.last_visit = pHistEntry->last_accessed;
         
         /* last modified */
         time( &pBM->d.url.last_modified );

         /* description */
         pBM->description = NULL;
      }
      
      /* address */
      iLen = pHistEntry->address ? XP_STRLEN( pHistEntry->address )+1 : 0;
      pBM->d.url.address = iLen ? (char *)XP_ALLOC( iLen*sizeof(char) ) : NULL;
      if( iLen )
      {
         XP_STRCPY( pBM->d.url.address, pHistEntry->address );
      }
      
      
      /*
       * Calc the size
       */
      
      iSize += bm_measure_URL( pBM, bLongFormat, 0 );
   }
   
   if( bLongFormat )
   {
      iSize += sizeof(uint16);
   }
   
   /* Leave room for the termination character */
   iSize++;

  #ifdef XP_WIN16
   if( (uint16)iSize > (uint16)(32*1024-1) )
   {
      return NULL;
   }
  #endif

   /* Allocate the storage */
   pStorage = pStgCsr = (char *)XP_ALLOC( iSize*sizeof(char) );
   if( !pStorage )
   {
      return NULL;
   }
   
   /* Copy the bookmarks to the storage */
   pCsr = pBMList;
   while( pCsr )
   {
      pStgCsr = bm_write_URL( pStgCsr, pCsr, bLongFormat, 0 );
      
      pPrevBM = pCsr;
      
      pCsr = pCsr->next;
   
      /* Free the entry */
      pPrevBM->next = NULL;
      bm_ShallowFreeEntry( pPrevBM );
   }

   if (bLongFormat)
   {
      /* Write the end-of-list marker */
      marker = 12345;
      XP_MEMCPY( pStgCsr, &marker, 2 );
      pStgCsr += sizeof(uint16);
   }
   
   /* End the string */
   *pStgCsr++ = '\0';
   *pSize = (pStgCsr - pStorage);
   
   return pStorage;
}


/*
 * Allocate and return a string that contains the text representation of
 *   a list of bookmarks entries (including headers and their contents).
 * The caller is responsible for freeing the string
 */
PUBLIC char*
BM_ConvertSelectionsToBlock(MWContext* context,
	XP_Bool bLongFormat,
	int32* lTotalLen)
{
	uint16			marker;
	int32			iSpace = 0;
	char*			pString;
	char*			pOriginal;

	BM_Entry*		tmp;

	CHKCONTEXT(context);

	tmp = BM_GetRoot(context);
	iSpace = bm_measure(tmp, bLongFormat, 0);

	/* leave room for end of list marker */
    if (bLongFormat)
        iSpace += sizeof(uint16);

	/* leave room for the termination character */
    iSpace++;

#ifdef XP_WIN16
	if (iSpace > 32000)
		return NULL;
#endif

	/* allocate the string */
	pOriginal = pString = (char*)XP_ALLOC(iSpace * sizeof(char));
	if (!pString)
		return NULL;

	/* Make a big string */
	pString = bm_write(pString, tmp, bLongFormat, 0);

	/* stick the end of list marker on so that when we are decoding this */
	/* block we know when we are done                                    */
	if (bLongFormat)
	{
		marker = 12345;
		XP_MEMCPY(pString, &marker, 2);
		pString += sizeof(uint16);
	}

	/* end the string and return the total length to our caller */
	*pString++ = '\0';
	*lTotalLen = (pString - pOriginal);
	return pOriginal;
}


/*
 * Take a block of memory formatted by BM_ConvertSelectionsToBlock and insert
 *   the items it represents into the bookmarks following 'item'.  If item is
 *   NULL insert at the beginning of the bookmarks.
 */
PUBLIC void
BM_InsertBlockAt(MWContext* context,
				 char* pOriginalBlock,
				 BM_Entry* addTo,
				 XP_Bool bLongFormat,
				 int32 lTotalLen)
{
  BM_Type type;
  int32 lBytesEaten = 0;   /* total number of bytes eaten */
  int32 lEat;              /* number of bytes eaten on this item */
  char* pCurrentPos;
  char* pBlock;
  BM_Entry*	tmp;
  BM_Entry*	item;
  XP_Bool first = TRUE;

  CHKCONTEXTVOID(context);
  if (!pOriginalBlock) return;

  if (addTo == NULL) addTo = BM_GetRoot(context);

  /* make a copy of the string we can write into */
  pCurrentPos = pBlock = (char*) XP_ALLOC(lTotalLen + 1);
  if (!pBlock) return;

  bm_start_batch(context);
  /* copy the data over and make sure we are NULL terminated to make life
	 easier */
  XP_MEMCPY(pBlock, pOriginalBlock, lTotalLen);
  pBlock[lTotalLen] = '\0';

  /* long format can have all kinds of different types of things in it */
  if (bLongFormat) {
	while (lBytesEaten < lTotalLen) {
	  /* determine the type of the next entry */
	  XP_MEMCPY(&type, pCurrentPos, sizeof(BM_Type));
	  pCurrentPos += sizeof(BM_Type);
	  lBytesEaten += sizeof(BM_Type);

	  item = NULL;

	  switch (type) {
	  case BM_TYPE_URL:
		item = bm_read_url_long(pCurrentPos, bLongFormat, &lEat);
		lBytesEaten += lEat;
		pCurrentPos += lEat;
		break;

	  case BM_TYPE_ALIAS:
		break;

	  case BM_TYPE_HEADER:
		item = bm_read_header_long(context, pCurrentPos, bLongFormat, &lEat);
		lBytesEaten += lEat;
		pCurrentPos += lEat;
		break;

	  case BM_TYPE_SEPARATOR:
		item = bm_read_Separator(pCurrentPos, bLongFormat, &lEat);
		lBytesEaten += lEat;
		pCurrentPos += lEat;
		break;

	  case 12345:
		/* Ah ha! this is the end marker we wrote! remember... */
		XP_ASSERT((lBytesEaten+1) == lTotalLen);
		break;
	  default:
		/* bogus type.  Who knows whats going on.  Just quit and
		   get out */
		goto GETOUT;

		break;
	  }
	  if (item) {
		if (first && BM_ISHEADER(addTo) && !BM_ISFOLDED(addTo)) {
		  /* Adding inside a folder as first child. */
		  BM_PrependChildToHeader(context, addTo, item);
		} else {
		  bm_InsertItemAfter(context, addTo, item, FALSE);
		}
		addTo = item;
		first = FALSE;
	  }
	}
  } else {
	item = NULL;
	/* short format is just a list of URLs separated by \n's */
	while (lBytesEaten < lTotalLen) {
	  item = bm_read_url_long(pCurrentPos, bLongFormat, &lEat);
	  lBytesEaten += lEat;
	  pCurrentPos += lEat;

	  if (item) {
		tmp = addTo->next;
		addTo->next = item;
		item->next = tmp;
		addTo = item;
	  }
	  /* if we just walked over a \0 we are done */
	  if (pOriginalBlock[lBytesEaten - 1] == '\0') {
		lBytesEaten = lTotalLen;
	  }
	}
  }

GETOUT:
  /* mark the bookmark list as changed and clean up */
  bm_SetModified(context, TRUE);
  XP_FREE(pBlock);
  bm_end_batch(context);
}




static void
bm_make_alias(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_Entry*		newAlias;
  int32			index;

  CHKCONTEXTVOID(context);

  if (BM_ISURL(entry)) {
	newAlias = bm_NewAlias(entry);
	if (!newAlias) return;

	bm_InsertItemAfter(context, entry, newAlias, FALSE);

	index = BM_GetIndex(context, newAlias);
	if (index > 0) {
	  bm_refresh(context, index, BM_LAST_CELL);
	}
  }
}

PUBLIC void
BM_MakeAliases(MWContext* context)
{
  CHKCONTEXTVOID(context);
  bm_start_batch(context);
  BM_EachSelectedEntryDo(context, bm_make_alias, NULL);
  bm_SyncCount(context);
  bm_end_batch(context);
}


BM_Entry*
BM_GetAliasOriginal(BM_Entry* entry)
{
  XP_ASSERT(BM_ISALIAS(entry));
  return BM_ISALIAS(entry) ? entry->d.alias.original : NULL;
}


static XP_Bool
bm_SelectionIsContiguous_1(MWContext* context, BM_Entry* entry,
						   XP_Bool* started, XP_Bool* finished)
{
  XP_Bool result;
  for (; entry ; entry = entry->next) {
	if (BM_ISSELECTED(entry)) {
	  if (*finished) return FALSE;
	  *started = TRUE;
	} else {
	  if (*started) *finished = TRUE;
	}
	if (BM_ISHEADER(entry) && !BM_ISFOLDED(entry)) {
	  result = bm_SelectionIsContiguous_1(context, entry->d.header.children,
										  started, finished);
	  if (!result) return result;
	}
  }
  return TRUE;
}

static XP_Bool
bm_SelectionIsContiguous(MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  XP_Bool started = FALSE;
  XP_Bool finished = FALSE;
  if (f->gSelectionCount < 0) bm_SyncSelection(context);
  if (f->gSelectionCount < 2) return TRUE;
  return bm_SelectionIsContiguous_1(context, BM_GetRoot(context),
									&started, &finished);
}

XP_Bool BM_FindCommandStatus(MWContext* context, BM_CommandType command)
{
  BM_Frame* f = GETFRAME(context);
  int32	length;

  CHKCONTEXT(context);

  if (f->gSelectionCount < 0) bm_SyncSelection(context);

  switch (command) {
  case BM_Cmd_Invalid:
	return FALSE;

  case BM_Cmd_Open: /**/
  case BM_Cmd_ImportBookmarks:
  case BM_Cmd_SaveAs: /**/
	return TRUE;

  case BM_Cmd_Close: /**/
	return TRUE;

  case BM_Cmd_Undo:
	return UNDO_CanUndo(f->undo);
  case BM_Cmd_Redo:
	return UNDO_CanRedo(f->undo);

  case BM_Cmd_Cut:
  case BM_Cmd_Copy:
  case BM_Cmd_Delete:
	return (f->gSelectionCount > 0);

  case BM_Cmd_Paste:
	return BMFE_GetClipContents(context, &length) ? TRUE : FALSE;
	break;


  case BM_Cmd_SelectAllBookmarks: /**/
	return TRUE;

  case BM_Cmd_Find:
	return TRUE;

  case BM_Cmd_FindAgain:
	return ((f->gFindInfo != NULL) && (f->gFindInfo->textToFind != NULL));

  case BM_Cmd_BookmarkProps: /**/
	return (f->gSelectionCount == 1 &&
			!(f->gSelectionMask & BM_TYPE_SEPARATOR));

  case BM_Cmd_Sort_Name:
  case BM_Cmd_Sort_Name_Asc:
  case BM_Cmd_Sort_Address:
  case BM_Cmd_Sort_Address_Asc:
  case BM_Cmd_Sort_AddDate:
  case BM_Cmd_Sort_AddDate_Asc:
  case BM_Cmd_Sort_LastVisit:
  case BM_Cmd_Sort_LastVisit_Asc:
  case BM_Cmd_Sort_Natural:
	if (f->gSelectionCount == 0) return FALSE;
	if (f->gSelectionCount == 1) {
	  return f->gSelectionMask == BM_TYPE_HEADER;
	}
	return bm_SelectionIsContiguous(context);

  case BM_Cmd_InsertBookmark:
  case BM_Cmd_InsertHeader:
	return TRUE;

  case BM_Cmd_InsertSeparator:
	return context->type == MWContextBookmarks;

  case BM_Cmd_GotoBookmark:
	if (context->type == MWContextBookmarks) {
	  return (f->gSelectionCount == 1 &&
			  (f->gSelectionMask & (BM_TYPE_URL | BM_TYPE_ALIAS)));
	} else {
	  return f->gSelectionCount > 0;
	}

  case BM_Cmd_MakeAlias:
	return (f->gSelectionCount == 1 &&
			(f->gSelectionMask & BM_TYPE_URL));

  case BM_Cmd_SetAddHeader:
	return (context->type == MWContextBookmarks &&
			f->gSelectionCount == 1 &&
			(f->gSelectionMask & BM_TYPE_HEADER) &&
			BM_FirstSelectedItem(context) != f->addheader);
			
  case BM_Cmd_SetMenuHeader:
	return (context->type == MWContextBookmarks &&
			f->gSelectionCount == 1 &&
			(f->gSelectionMask & BM_TYPE_HEADER) &&
			BM_FirstSelectedItem(context) != f->menuheader);

  default:
	XP_ASSERT(0);
	break;
  }
  return FALSE;
}

static void
bm_open_file(MWContext* context, char* newFile, void* closure)
{
  if (newFile) {
#ifdef XP_WIN
	BMFE_ChangingBookmarksFile();
#endif

	BM_ReadBookmarksFromDisk(context, newFile, NULL);

#ifdef XP_WIN
	BMFE_ChangedBookmarksFile();
#endif

	XP_FREE(newFile);
  }
}


/* LI_STUFF give li the ability to open a new bookmarks file */
void
BM_Open_File(MWContext* context, char* newFile)
{
	bm_open_file(context, newFile, NULL);
}


static void
bm_import_file(MWContext* context, char* newFile, void* closure)
{
  BM_Frame* f = GETFRAME(context);
  bm_start_batch(context);
  UNDO_DiscardAll(f->undo);
  if (newFile) {
	BM_Entry* oldroot = f->gBookmarks;
	BM_Entry* oldmenuheader = f->menuheader;
	BM_Entry* oldaddheader = f->addheader;
	BM_Entry* newroot;
	BM_Entry* entry;
	BM_Entry* next;
	char* oldfile = NULL;
	XP_StatStruct savedStat;
	
	if (f->gFile) {
	  oldfile = XP_STRDUP(f->gFile);
	  if (!oldfile) return; /* Out of memory... */
	}
	f->gBookmarks = NULL;
	f->gBookmarksModified = FALSE; /* Don't save now. */
	
	/* save real stat 'cause it's about to get busted */
	XP_MEMCPY(&savedStat, &(f->laststat), sizeof(savedStat));
	
	BM_ReadBookmarksFromDisk(context, newFile, NULL);
	
	/* restore real stat */
	XP_MEMCPY(&(f->laststat), &savedStat,  sizeof(savedStat));
		
	newroot = f->gBookmarks;
	if (newroot && oldroot) {
	  /* Make the new stuff be the first folder of the old stuff. */
	  f->gBookmarks = oldroot;
	  if (context->type == MWContextAddressBook) {
		XP_ASSERT(BM_ISHEADER(newroot));
		if (BM_ISHEADER(newroot)) {
		  for (entry = newroot->d.header.children;
			   entry;
			   entry = next) {
			next = entry->next;
			entry->next = NULL;
			bm_AddChildToHeaderSorted(context, oldroot, entry);
		  }
		}
	  } else {
		BM_PrependChildToHeader(context, oldroot, newroot);
	  }
	  f->menuheader = oldmenuheader;
	  f->addheader = oldaddheader;
	} else {
	  f->gBookmarks = oldroot ? oldroot : newroot;
	}
	FREEIF(f->gFile);
	f->gFile = oldfile;
	oldfile = NULL;
	bm_SetModified(context, TRUE);
	bm_refresh(context, 1, BM_LAST_CELL);
	XP_FREE(newFile);
  }
  bm_end_batch(context);
}


static void
bm_save_as_file(MWContext* context, char* saveName, void* closure)
{
  if (saveName) {
	BM_SaveBookmarks(context, saveName);
	XP_FREE(saveName);
  }
}


static int
bm_comparenames(const void* e1, const void* e2)
{
#ifdef INTL_SORT
  return XP_StrColl((*((BM_Entry**)e1))->name, (*((BM_Entry**)e2))->name);
#else
  return XP_STRCMP((*((BM_Entry**)e1))->name, (*((BM_Entry**)e2))->name);
#endif
}
static int
bm_comparenames_Asc(const void* e1, const void* e2)
{
#ifdef INTL_SORT
  return XP_StrColl((*((BM_Entry**)e2))->name, (*((BM_Entry**)e1))->name);
#else
  return XP_STRCMP((*((BM_Entry**)e2))->name, (*((BM_Entry**)e1))->name);
#endif
}

static int bm_compare_address( const void *elem1, const void *elem2 )
{
    BM_Entry *p1 = *(BM_Entry **)elem1;
    BM_Entry *p2 = *(BM_Entry **)elem2;

    if( !(p1->type == BM_TYPE_URL ||
          p1->type == BM_TYPE_ADDRESS) )
    {
        if( !(p2->type == BM_TYPE_URL ||
              p2->type == BM_TYPE_ADDRESS) )
        {
            return 0;
        }
        return -1;
    }
    else if( !(p2->type == BM_TYPE_URL ||
               p2->type == BM_TYPE_ADDRESS) )
    {
        return 1;
    }

    /* Note we rely on d.url.address is at same mem address as d.address.address */

   #ifdef INTL_SORT
    return XP_StrColl( p1->d.url.address, p2->d.url.address );
   #else
    return XP_STRCMP( p1->d.url.address, p2->d.url.address );
   #endif
}
static int bm_compare_address_Asc( const void *elem1, const void *elem2 )
{
    BM_Entry *p2 = *(BM_Entry **)elem1;
    BM_Entry *p1 = *(BM_Entry **)elem2;

    if( !(p1->type == BM_TYPE_URL ||
          p1->type == BM_TYPE_ADDRESS) )
    {
        if( !(p2->type == BM_TYPE_URL ||
              p2->type == BM_TYPE_ADDRESS) )
        {
            return 0;
        }
        return -1;
    }
    else if( !(p2->type == BM_TYPE_URL ||
               p2->type == BM_TYPE_ADDRESS) )
    {
        return 1;
    }

    /* Note we rely on d.url.address is at same mem address as d.address.address */

   #ifdef INTL_SORT
    return XP_StrColl( p1->d.url.address, p2->d.url.address );
   #else
    return XP_STRCMP( p1->d.url.address, p2->d.url.address );
   #endif
}

#ifdef SUNOS4
/* difftime() doesn't seem to exist on SunOS anywhere. -mcafee */
static double difftime(time_t time1, time_t time0)
{
  return (double) (time1 - time0);
}
#endif

static int bm_compare_time (time_t time1, time_t time0)
{
  double diff = difftime( time1, time0 );

  if (diff > 0.0) return 1;
  if (diff < 0.0) return -1;

  return 0;
}

static int bm_compare_addition_date( const void *elem1, const void *elem2 )
{
    BM_Entry *p1 = *(BM_Entry **)elem1;
    BM_Entry *p2 = *(BM_Entry **)elem2;
    
    return bm_compare_time( p2->addition_date, p1->addition_date );
}
static int bm_compare_addition_date_Asc( const void *elem1, const void *elem2 )
{
    BM_Entry *p2 = *(BM_Entry **)elem1;
    BM_Entry *p1 = *(BM_Entry **)elem2;
    
    return bm_compare_time( p2->addition_date, p1->addition_date );
}

static int bm_compare_natural( const void *elem1, const void *elem2 )
{
    BM_Entry *p2 = *(BM_Entry **)elem1;
    BM_Entry *p1 = *(BM_Entry **)elem2;
    
    return (p2->iNaturalIndex >= p1->iNaturalIndex) ? 1 : -1;
}

static int bm_compare_last_visit( const void *elem1, const void *elem2 )
{
    BM_Entry *p1 = *(BM_Entry **)elem1;
    BM_Entry *p2 = *(BM_Entry **)elem2;
    if( p2->type != BM_TYPE_URL )
    {
        if( p1->type != BM_TYPE_URL )
        {
            return 0;
        }
        return -1;
    }
    else if( p1->type != BM_TYPE_URL )
    {
        return 1;
    }
    
    return bm_compare_time( p2->d.url.last_visit, p1->d.url.last_visit );
}
static int bm_compare_last_visit_Asc( const void *elem1, const void *elem2 )
{
    BM_Entry *p2 = *(BM_Entry **)elem1;
    BM_Entry *p1 = *(BM_Entry **)elem2;
    if( p2->type != BM_TYPE_URL )
    {
        if( p1->type != BM_TYPE_URL )
        {
            return 0;
        }
        return -1;
    }
    else if( p1->type != BM_TYPE_URL )
    {
        return 1;
    }
    
    return bm_compare_time( p2->d.url.last_visit, p1->d.url.last_visit );
}

static void
bm_SortSelected_1(MWContext* context, BM_Entry* header, BM_SortType enSortType )
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry** list = NULL;
  int numlist;
  BM_Entry* entry;
  BM_Entry* previous = NULL;
  int i;
#ifdef XP_WIN  
  int (__cdecl *pfSort)(const void *, const void *);
#else
  int (*pfSort)(const void *, const void *);
#endif

  XP_ASSERT(BM_ISHEADER(header));
  
  switch( enSortType )
  {
     case BM_Sort_Name:
        pfSort = bm_comparenames;
        break;
     case BM_Sort_Name_Asc:
        pfSort = bm_comparenames_Asc;
        break;

     case BM_Sort_Address:
        pfSort = bm_compare_address;
        break;
     case BM_Sort_Address_Asc:
        pfSort = bm_compare_address_Asc;
        break;

     case BM_Sort_AddDate:
        pfSort = bm_compare_addition_date;
        break;
     case BM_Sort_AddDate_Asc:
        pfSort = bm_compare_addition_date_Asc;
        break;

     case BM_Sort_LastVisit:
        pfSort = bm_compare_last_visit;
        break;
     case BM_Sort_LastVisit_Asc:
        pfSort = bm_compare_last_visit_Asc;
        break;
        
     case BM_Sort_Natural:
     default:
        pfSort = bm_compare_natural;     
        break;
  }
  
  if (header->d.header.childCount == 0) return;
  if (BM_ISSELECTED(header) && f->gSelectionCount == 1) {
	numlist = header->d.header.childCount;
	list = (BM_Entry**) XP_ALLOC(numlist * sizeof(BM_Entry*));
	if (!list) return;
	for (i = 0, entry = header->d.header.children;
		 i < numlist;
		 i++, entry = entry->next) {
	  list[i] = entry;
	}
  } else {
	i = 0;
	for (entry = header->d.header.children ; entry ; entry = entry->next) {
	  if (BM_ISSELECTED(entry)) {
		if (list == NULL) {
		  list = (BM_Entry**) XP_ALLOC(header->d.header.childCount *
									   sizeof(BM_Entry*));
		  if (list == NULL) return;
		}
		list[i++] = entry;
	  } else {
		if (list == NULL) previous = entry;
	  }
	}
	numlist = i;
  }
  if (list) {
	if (numlist > 1) {
	  for (i=0 ; i<numlist ; i++) {
		if (list[i]->name == NULL) {
		  list[i]->name = XP_STRDUP("");
		  if (list[i]->name == NULL) return;
		}
		BM_RemoveChildFromHeader(context, header, list[i]);
	  }
	  XP_QSORT(list, numlist, sizeof(BM_Entry*), pfSort);
	  for (i=0 ; i<numlist ; i++) {
		if (previous) {
		  bm_InsertItemAfter(context, previous, list[i], FALSE);
		} else {
		  BM_PrependChildToHeader(context, header, list[i]);
		}
		previous = list[i];
	  }
	}
	XP_FREE(list);
	list = NULL;
  }
  for (entry = header->d.header.children ; entry ; entry = entry->next) {
	if (BM_ISHEADER(entry)) bm_SortSelected_1(context, entry, enSortType );
  }
}

static void
bm_SortSilent_1(MWContext* context, BM_Entry* header, BM_SortType enSortType )
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry** list = NULL;
  int numlist;
  BM_Entry* entry;
  BM_Entry* previous = NULL;
  int i;
  XP_Bool bSelected = FALSE;
#ifdef XP_WIN  
  int (__cdecl *pfSort)(const void *, const void *);
#else
  int (*pfSort)(const void *, const void *);
#endif

  XP_ASSERT(BM_ISHEADER(header));
  
  switch( enSortType )
  {
     case BM_Sort_Name:
        pfSort = bm_comparenames;
        break;
     case BM_Sort_Name_Asc:
        pfSort = bm_comparenames_Asc;
        break;

     case BM_Sort_Address:
        pfSort = bm_compare_address;
        break;
     case BM_Sort_Address_Asc:
        pfSort = bm_compare_address_Asc;
        break;

     case BM_Sort_AddDate:
        pfSort = bm_compare_addition_date;
        break;
     case BM_Sort_AddDate_Asc:
        pfSort = bm_compare_addition_date_Asc;
        break;

     case BM_Sort_LastVisit:
        pfSort = bm_compare_last_visit;
        break;
     case BM_Sort_LastVisit_Asc:
        pfSort = bm_compare_last_visit_Asc;
        break;
        
     case BM_Sort_Natural:
     default:
        pfSort = bm_compare_natural;     
        break;
  }
  
  if (header->d.header.childCount == 0) return;
  numlist = header->d.header.childCount;
  list = (BM_Entry**) XP_ALLOC(numlist * sizeof(BM_Entry*));
  if (!list) return;
  for (i = 0, entry = header->d.header.children;
  	 i < numlist;
  	 i++, entry = entry->next) {
    list[i] = entry;
  }
  if (numlist > 1) {
    for (i=0 ; i<numlist ; i++) {
      if (list[i]->name == NULL) {
      	list[i]->name = XP_STRDUP("");
     	if (list[i]->name == NULL) return;
      }
      if (BM_ISSELECTED(list[i])) {
        BM_CLEARFLAG(list[i], BM_ATTR_SELECTED);
        bSelected = TRUE;
      }
      BM_RemoveChildFromHeader(context, header, list[i]);
      if (bSelected) {
        BM_SETFLAG(list[i], BM_ATTR_SELECTED);
        bSelected = FALSE;
      }
    }
    XP_QSORT(list, numlist, sizeof(BM_Entry*), pfSort);
    for (i=0 ; i<numlist ; i++) {
      if (previous) {
        bm_InsertItemAfter(context, previous, list[i], FALSE);
      } else {
        BM_PrependChildToHeader(context, header, list[i]);
      }
      previous = list[i];
    }
  }
  XP_FREE(list);
  list = NULL;
  for (entry = header->d.header.children ; entry ; entry = entry->next) {
	if (BM_ISHEADER(entry)) bm_SortSilent_1(context, entry, enSortType );
  }
}

/*
//  Normalize bookmarks based on the current sort.  Note the current sort
//  should be the natural sort order (aka BM_Sort_Natural) as no other 
//  sort order has a mapping to the natural index.
*/
static void
bm_Normalize(MWContext* context, BM_Entry* at)
{
  BM_Entry* nextChild;
  BM_Entry* children;

  int32 iNaturalIndex = 0;
  
  while (at) {
	nextChild = at->next;
	if (BM_ISHEADER(at)) {
	  children = at->d.header.children;
	} else {
	  children = NULL;
	}

	at->iNaturalIndex = iNaturalIndex++;

	if (children) {
	  bm_Normalize(context, children);
	}

	at = nextChild;
  }
}

static void
bm_SortSelected(MWContext* context, BM_SortType enSortType )
{
  BM_Frame* f = GETFRAME(context);
  if (f->gSelectionCount < 0) bm_SyncSelection(context);
  if (f->enSortType == BM_Sort_Natural) bm_Normalize(context, BM_GetRoot(context));
  f->enSortType = enSortType;
  f->bSorting = TRUE;
  bm_SortSelected_1(context, BM_GetRoot(context), enSortType);
  f->bSorting = FALSE;
  bm_refresh(context, 1, BM_LAST_CELL);
}

static void
bm_SortSilent(MWContext* context, BM_SortType enSortType )
{
  BM_Frame* f = GETFRAME(context);
  if (f->gSelectionCount < 0) bm_SyncSelection(context);
  if (f->enSortType == BM_Sort_Natural) bm_Normalize(context, BM_GetRoot(context));
  f->enSortType = enSortType;
  f->bSorting = TRUE;
  bm_SortSilent_1(context, BM_GetRoot(context), enSortType);
  f->bSorting = FALSE;
}

static void bm_append_address_string(MWContext* context, BM_Entry* entry,
									 void* closure);

static void
bm_append_fulladdress_string(MWContext* context, BM_Entry* entry,
							 void* closure)
{
  if (BM_ISALIAS(entry)) entry = entry->d.alias.original;
  if (entry->flags & BM_ATTR_MARKED) return;
  if (BM_ISHEADER(entry)) {
	BM_SETFLAG(entry, BM_ATTR_MARKED);
	for (entry = entry->d.header.children ; entry ; entry = entry->next) {
	  bm_append_fulladdress_string(context, entry, closure);
	}
  } else if (BM_ISADDRESS(entry)) {
	bm_append_address_string(context, entry, closure);
	XP_ASSERT(entry->flags & BM_ATTR_MARKED);
  }
}

static void
bm_append_address_string(MWContext* context, BM_Entry* entry, void* closure)
{
  if (BM_ISALIAS(entry)) entry = entry->d.alias.original;
  if (entry->flags & BM_ATTR_MARKED) return;
  if (BM_ISADDRESS(entry) || BM_ISHEADER(entry)) {
	char* address =
	  BM_ISHEADER(entry) ? BM_GetNickName(entry) : BM_GetAddress(entry);
	if (BM_ISHEADER(entry) && (address == NULL || *address == '\0')) {
	  /* No nickname for a header, so we don't have anything to write down
		 that we can remember later.  Just write down all the members of
		 this list. */
	  bm_append_fulladdress_string(context, entry, closure);
	} else {
#ifdef MOZ_MAIL_NEWS
	  char** buf = (char**) closure;
	  char* full = MSG_MakeFullAddress(BM_GetName(entry), address);
	  if (full) {
		if (*buf) NET_SACat(buf, ", ");
		NET_SACat(buf, full);
		XP_FREE(full);
	  }
#endif /* MOZ_MAIL_NEWS */
	}
  }	
  BM_SETFLAG(entry, BM_ATTR_MARKED);
}

char*
BM_GetFullAddress(MWContext* context, BM_Entry* entry)
{
  char* result = NULL;
  bm_append_address_string(context, entry, &result);
  return result;
}

static void
bm_ComposeMessageToSelected(MWContext* context)
{
  char* buf = NULL;
  char* tmp;
  URL_Struct *url_struct;
  bm_ClearMarkEverywhere(context);
  BM_EachSelectedEntryDo(context, bm_append_address_string, &buf);
  if (!buf) return;
  tmp = NET_Escape(buf, URL_PATH);
  XP_FREE(buf);
  buf = tmp;
  if (!buf) return;
  tmp = XP_Cat("mailto:?to=", buf, (char*)/*Win16*/ NULL);
  XP_FREE(buf);
  buf = tmp;
  if (!buf) return;
  url_struct = NET_CreateURLStruct (buf, NET_NORMAL_RELOAD);
  if (url_struct) {
	url_struct->internal_url = TRUE;
	FE_GetURL(context, url_struct);
  }
  XP_FREE(buf);
}


char*
BM_ExpandHeaderString(MWContext* context, const char* value,
					  XP_Bool expandfull)
{
  BM_Frame* f = GETFRAME(context);
  char* name;
  char* address;
  char* curname;
  char* curaddress;
  char* pHashStr;
  int num;
  int i,j;
  XP_Bool found = FALSE;
  BM_Entry* entry;
  char* result = NULL;
  char* pTempStr = NULL;
  int tempBufLen = 0;
  CHKCONTEXT(context);
#ifdef MOZ_MAIL_NEWS
  num = MSG_ParseRFC822Addresses(value, &name, &address);
#else
  num = 0;
#endif /* MOZ_MAIL_NEWS */
  curname = name;
  curaddress = address;
  bm_ClearMarkEverywhere(context);
  for (i=0 ; i<num ; i++) {
	pHashStr = NULL;
	if (XP_STRCHR(curaddress, '@') == NULL) {
		/* to make nicknames case-insensitive, we have to 
		** change the string here to be all lowercase before
		** passing it to the hash lookup function */

		/* first make sure the temporary buffer we have 
		** is long enough for this string.  If not, make
		** a new one that is long enough. */
		int curlen;
		curlen = XP_STRLEN(curaddress);
		if (!pTempStr || (curlen > tempBufLen)) {
			FREEIF(pTempStr);
			pTempStr = XP_STRDUP(curaddress);
			tempBufLen = curlen;
		} else {
			/* just copy the string into the existing buffer */
			XP_STRCPY(pTempStr, curaddress);
		}
		if (pTempStr) {
			/* now the buffer is loaded with the string, change the string to lowercase */
			for (j = 0; j < curlen; j++) {
				if (isupper(pTempStr[j])) {
					pTempStr[j] = (char)tolower(pTempStr[j]);
				}
			}
			pHashStr = pTempStr;	/* use the temp str for the hash function */
		} else {
			pHashStr = curaddress;	/* use the old string if low on memory */
		}
	}
	if (pHashStr && (entry = XP_Gethash(f->nicknameTable, pHashStr, NULL)) != NULL) {
	  found = TRUE;
	  if (expandfull) {
		bm_append_fulladdress_string(context, entry, &result);
	  } else {
		bm_append_address_string(context, entry, &result);
	  }
	} else {
	  if (result) NET_SACat(&result, ", ");
	  if (*curname) {
		NET_SACat(&result, curname);
		NET_SACat(&result, " <");
	  }
	  NET_SACat(&result, curaddress);
	  if (*curname) {
		NET_SACat(&result, ">");
	  }
	}
	curname += XP_STRLEN(curname) + 1;
	curaddress += XP_STRLEN(curaddress) + 1;
  }
  FREEIF(name);
  FREEIF(address);
  FREEIF(pTempStr);
  if (!found) {
	FREEIF(result);				/* Note this sets also result to NULL. */
  }
  return result;
}


	



void BM_ObeyCommand(MWContext* context, BM_CommandType command)
{
  BM_Frame* f = GETFRAME(context);
  BM_Entry*		firstSelected;

  CHKCONTEXTVOID(context);

  if (!BM_FindCommandStatus(context, command)) return;

  firstSelected = BM_FirstSelectedItem(context);

  bm_start_batch(context);

  switch (command) {
  case BM_Cmd_Invalid:
	break;

  case BM_Cmd_Open:
	FE_PromptForFileName(context, XP_GetString(XP_BKMKS_OPEN_BKMKS_FILE),
						 0, TRUE, FALSE, bm_open_file, NULL);
	break;

  case BM_Cmd_ImportBookmarks:
	FE_PromptForFileName(context, 
		XP_GetString(context->type == MWContextAddressBook ?
				XP_BKMKS_IMPORT_ADDRBOOK : XP_BKMKS_IMPORT_BKMKS_FILE),
				0, TRUE, FALSE, bm_import_file, NULL);
	break;

  case BM_Cmd_SaveAs:
	FE_PromptForFileName(context, 
		XP_GetString(context->type == MWContextAddressBook ?
					XP_BKMKS_SAVE_ADDRBOOK : XP_BKMKS_SAVE_BKMKS_FILE),
					0, FALSE, FALSE, bm_save_as_file, NULL);
	break;

  case BM_Cmd_Close:
	BM_SaveBookmarks(context, f->gFile);
	/* ### Maybe need to do more? */
	break;

  case BM_Cmd_Undo:
	UNDO_EndBatch(f->undo, NULL, NULL);
	UNDO_DoUndo(f->undo);
	UNDO_StartBatch(f->undo);
	bm_refresh(context, 1, BM_LAST_CELL);
	bm_SyncCount(context);
	break;

  case BM_Cmd_Redo:
	UNDO_EndBatch(f->undo, NULL, NULL);
	UNDO_DoRedo(f->undo);
	UNDO_StartBatch(f->undo);
	bm_refresh(context, 1, BM_LAST_CELL);
	bm_SyncCount(context);
	break;

  case BM_Cmd_Cut:
	bm_cut(context);
	break;

  case BM_Cmd_Copy:
	bm_copy(context);
	break;

  case BM_Cmd_Paste:
	bm_paste(context);
	break;

  case BM_Cmd_Delete:
	bm_delete(context);
	break;

  case BM_Cmd_SelectAllBookmarks:
	BM_SelectAll(context, TRUE);
	break;

  case BM_Cmd_Find:
	bm_CloseLastFind(context);
	bm_BeginFindBookmark(context);
	break;

  case BM_Cmd_FindAgain:
	bm_CloseLastFind(context);
	BM_DoFindBookmark(context, f->gFindInfo);
	break;

  case BM_Cmd_BookmarkProps:
	if (firstSelected) {
	  BMFE_OpenBookmarksWindow(context);
	  BMFE_EditItem(context, firstSelected);
	}
	break;

  case BM_Cmd_GotoBookmark:
	if (context->type == MWContextAddressBook) {
	  bm_ComposeMessageToSelected(context);
	} else if (firstSelected) {
	  BM_GotoBookmark(context, firstSelected);
	}
	break;

  case BM_Cmd_Sort_Name:
  case BM_Cmd_Sort_Name_Asc:
  case BM_Cmd_Sort_Address:
  case BM_Cmd_Sort_Address_Asc:
  case BM_Cmd_Sort_AddDate:
  case BM_Cmd_Sort_AddDate_Asc:
  case BM_Cmd_Sort_LastVisit:
  case BM_Cmd_Sort_LastVisit_Asc:
  case BM_Cmd_Sort_Natural:
	bm_SortSelected( context, command-BM_Cmd_Sort_Name );
	break;

  case BM_Cmd_InsertBookmark:
	bm_BeginEditNewUrl(context);
	break;

  case BM_Cmd_InsertHeader:
	bm_BeginEditNewHeader(context);
	break;

  case BM_Cmd_InsertSeparator:
	if (firstSelected) {
	  bm_InsertItemAfter(context, firstSelected, bm_NewSeparator(), TRUE);
	  bm_refresh(context, BM_GetIndex(context, firstSelected) + 1,
				 BM_LAST_CELL);
	}
	break;

  case BM_Cmd_MakeAlias:
	BM_MakeAliases(context);
	break;

  case BM_Cmd_SetAddHeader:
	if (firstSelected) {
	  BM_SetAddHeader(context, firstSelected);
	}
	break;

  case BM_Cmd_SetMenuHeader:
	if (firstSelected) {
	  BM_SetMenuHeader(context, firstSelected);
	}
	break;

  default:
	XP_ASSERT(0);

  }
  bm_end_batch(context);
}



/* Make sure that the given entry is a real entry, and not a pointer that has
   since become invalid. */

static XP_Bool
bm_validate_entry(MWContext* context, BM_Entry* entry, BM_Entry* search)
{
  for (; entry ; entry = entry->next) {
	if (entry == search) return TRUE;
	if (BM_ISHEADER(entry)) {
	  if (bm_validate_entry(context, entry->d.header.children, search)) {
		return TRUE;
	  }
	}
  }
  return FALSE;
}


static void
bm_urlcheck_finished(URL_Struct* url_struct, int status, MWContext* context)
{
  BM_Frame* f = GETFRAME(context);
  time_t now;
  char timestr[40];
  if (f) {
	struct BM_WhatsChangedInfo* w = (struct BM_WhatsChangedInfo *)&(f->whatschanged);
	BM_Entry* entry = (BM_Entry*) url_struct->fe_data;
	if (bm_validate_entry(context, BM_GetRoot(context), entry)) {
	  const char* url = BM_GetAddress(entry);
	  int32 oldstate = BM_GetChangedState(entry);
	  BM_CLEARFLAG(entry, BM_ATTR_CHECKING);
	  if (status >= 0) {
		if (url && XP_STRCMP(url_struct->address, url) == 0) {
		  w->numreached++;
		  entry->d.url.last_modified = url_struct->last_modified;
		  if (entry->d.url.last_modified > entry->d.url.last_visit) {
			w->numchanged++;
		  }
		}
	  } else {
		entry->d.url.last_modified = 0;
	  }
	  if (BM_GetChangedState(entry) != oldstate) {
		bm_entry_changed(context, entry);
		bm_SetModified(context, TRUE);
	  }

	  now = time ((time_t *) 0);

	  if (w->numreached == 0) {
		XP_STRCPY(timestr, "???");
	  } else {
		int32 estimate = (now - w->starttime) * (w->total - w->numreached) /
		  w->numreached;
		if (estimate < 2 * 60) {
		  PR_snprintf(timestr, sizeof(timestr), XP_GetString(XP_BKMKS_SECONDS), 
					  estimate);
		} else if (estimate < 2 * 60 * 60) {
		  PR_snprintf(timestr, sizeof(timestr), XP_GetString(XP_BKMKS_MINUTES), 
					  estimate / 60);
		} else {
		  PR_snprintf(timestr, sizeof(timestr), XP_GetString(XP_BKMKS_HOURS_MINUTES),
					  estimate / 3600, (estimate / 60) % 60);
		}
	  }
	  BMFE_UpdateWhatsChanged(context, url, w->numreached, w->total,
							  timestr);
	}

	/* Check to see if we're all done.  First check to see if we're in the
	   middle of a batch operation; if we are, then we must be still setting
	   things up and we got called here because we had an invalid bookmark
	   and netlib called the exit routine immediately.  In that case, we
	   don't want to say we're all done; we're probably still sending
	   URLs to netlib.

	   If we're not in the middle of a batch operation, then we're all done
	   if there are no more outstanding connections on our context. */
	if (f->batch_depth == 0 &&
		!NET_AreThereActiveConnectionsForWindow(context)) {
	  BMFE_FinishedWhatsChanged(context, w->total, w->numreached,
								w->numchanged);
	}
  }
}

#ifdef XP_WIN16
/* code segment is full, switch to a new segment */
#pragma code_seg("BKMKS2_TEXT","CODE")
#endif



static void
bm_urlcheck_start(MWContext* context, BM_Entry* entry)
{
  BM_Frame* f = GETFRAME(context);
  char* url;
  URL_Struct* url_struct;

  XP_ASSERT(entry);

  if (BM_ISALIAS(entry)) {
	entry = entry->d.alias.original;
  }
  if (!entry) return;

  if (entry->flags & BM_ATTR_CHECKING) return;

  url = BM_GetAddress(entry);
  if (!url) return;
  url_struct = NET_CreateURLStruct(url, NET_SUPER_RELOAD);
  if (!url_struct) return;
  BM_SETFLAG(entry, BM_ATTR_CHECKING);
  url_struct->method = URL_HEAD_METHOD;
  url_struct->fe_data = entry;
  f->whatschanged.total++;
  NET_GetURL(url_struct, FO_PRESENT, context, bm_urlcheck_finished);
}




static void
bm_start_whats_changed_1(MWContext* context, BM_Entry* entry,
						 XP_Bool do_only_selected)
{
  for ( ; entry ; entry = entry->next) {
	if (BM_ISURL(entry) || BM_ISALIAS(entry)) {
	  if (!do_only_selected || BM_ISSELECTED(entry)) {
		bm_urlcheck_start(context, entry);
	  } 
	} else if (BM_ISHEADER(entry)) {
	  /* Recur through the children.  If we are selected and folded, then
	     make sure we do all of our descendents. */
	  bm_start_whats_changed_1(context, entry->d.header.children,
							   (BM_ISSELECTED(entry) && BM_ISFOLDED(entry)) ?
							   FALSE : do_only_selected);
	}
  }
}


static void
bm_clear_check_attr(MWContext* context, BM_Entry* entry, void* closure)
{
  BM_CLEARFLAG(entry, BM_ATTR_CHECKING);
}


int
BM_StartWhatsChanged(MWContext* context, XP_Bool do_only_selected)
{
  BM_Frame* f = GETFRAME(context);
  struct BM_WhatsChangedInfo* w;
  XP_ASSERT(context && context->type == MWContextBookmarks && f);
  if (!context || context->type != MWContextBookmarks || !f) return -1;
  w = &(f->whatschanged);
  BM_CancelWhatsChanged(context);
  XP_MEMSET(w, 0, sizeof(*w));
  w->starttime = time ((time_t *) 0);

  BM_EachEntryDo(context, bm_clear_check_attr, NULL);

  bm_start_batch(context);
  bm_start_whats_changed_1(context, BM_GetRoot(context), do_only_selected);

#if 0
  minutes = f->whatschanged.total * 35 / 60;
								/* Assumes a maximum timeout of 35 seconds per
								   connection.  Need to not hard-code
								   this... #### */
  if (minutes < 60) {
	/* Fix i18n ### */
	PR_snprintf(w->totaltime, sizeof(w->totaltime), "%ld minutes", minutes);
  } else {
	/* Fix i18n ### */
	PR_snprintf(w->totaltime, sizeof(w->totaltime), "%ld hours",
				(minutes / 60) + 1);
  }
#endif

  BMFE_UpdateWhatsChanged(context, NULL, w->numreached, w->total,
						  "???"); /* Fix i18n ### */
  bm_refresh(context, 1, BM_LAST_CELL);
  bm_end_batch(context);
  if (!NET_AreThereActiveConnectionsForWindow(context)) {
	/* All done, already (probably because nothing was selected). */
	BMFE_FinishedWhatsChanged(context, w->total, w->numreached,
							  w->numchanged);
  }
  return 0;
}


int
BM_CancelWhatsChanged(MWContext* context)
{
  XP_InterruptContext(context);
  return 0;
}

void BM_ResetUndo(MWContext * context)
{
	BM_Frame * f = GETFRAME(context);
	UNDO_DiscardAll( f->undo );
}

