/*
Open Tracker License

Terms and Conditions

Copyright (c) 1991-2001, Be Incorporated. All rights reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice applies to all licensees
and shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Except as contained in this notice, the name of Be Incorporated shall not be
used in advertising or otherwise to promote the sale, use or other dealings in
this Software without prior written authorization from Be Incorporated.

BeMail(TM), Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or
registered trademarks of Be Incorporated in the United States and other
countries. Other brand product names are registered trademarks or trademarks
of their respective holders. All rights reserved.
*/


#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

#include <Alert.h>
#include <Beep.h>
#include <Clipboard.h>
#include <ControlLook.h>
#include <Debug.h>
#include <E-mail.h>
#include <Input.h>
#include <Locale.h>
#include <MenuItem.h>
#include <Mime.h>
#include <NodeInfo.h>
#include <NodeMonitor.h>
#include <Path.h>
#include <PopUpMenu.h>
#include <Region.h>
#include <Roster.h>
#include <ScrollView.h>
#include <TextView.h>
#include <UTF8.h>

#include <MailMessage.h>
#include <MailAttachment.h>
#include <mail_util.h>

#include "MailApp.h"
#include "MailSupport.h"
#include "MailWindow.h"
#include "Messages.h"
#include "Content.h"
#include "Utilities.h"
#include "FieldMsg.h"
#include "Words.h"


#define DEBUG_SPELLCHECK 0
#if DEBUG_SPELLCHECK
#	define DSPELL(x) x
#else
#	define DSPELL(x) ;
#endif


#define B_TRANSLATION_CONTEXT "Mail"


const rgb_color kNormalTextColor = {0, 0, 0, 255};
const rgb_color kSpellTextColor = {255, 0, 0, 255};
const rgb_color kHyperLinkColor = {0, 0, 255, 255};
const rgb_color kHeaderColor = {72, 72, 72, 255};

const rgb_color kQuoteColors[] = {
	{0, 0, 0x80, 0},		// 3rd, 6th, ... quote level color (blue)
	{0, 0x80, 0, 0},		// 1st, 4th, ... quote level color (green)
	{0x80, 0, 0, 0}			// 2nd, ... (red)
};
const int32 kNumQuoteColors = 3;

const rgb_color kDiffColors[] = {
	{0xb0, 0, 0, 0},		// '-', red
	{0, 0x90, 0, 0},		// '+', green
	{0x6a, 0x6a, 0x6a, 0}	// '@@', dark grey
};

void Unicode2UTF8(int32 c, char **out);


inline bool
IsInitialUTF8Byte(uchar b)
{
	return ((b & 0xC0) != 0x80);
}


void
Unicode2UTF8(int32 c, char **out)
{
	char *s = *out;

	ASSERT(c < 0x200000);

	if (c < 0x80)
		*(s++) = c;
	else if (c < 0x800) {
		*(s++) = 0xc0 | (c >> 6);
		*(s++) = 0x80 | (c & 0x3f);
	} else if (c < 0x10000) {
		*(s++) = 0xe0 | (c >> 12);
		*(s++) = 0x80 | ((c >> 6) & 0x3f);
		*(s++) = 0x80 | (c & 0x3f);
	} else if (c < 0x200000) {
		*(s++) = 0xf0 | (c >> 18);
		*(s++) = 0x80 | ((c >> 12) & 0x3f);
		*(s++) = 0x80 | ((c >> 6) & 0x3f);
		*(s++) = 0x80 | (c & 0x3f);
	}
	*out = s;
}


static bool
FilterHTMLTag(int32 &first, char **t, char *end)
{
	const char *newlineTags[] = {
		"br", "/p", "/div", "/table", "/tr",
		NULL};

	char *a = *t;

	// check for some common entities (in ISO-Latin-1)
	if (first == '&') {
		// filter out and convert decimal values
		if (a[1] == '#' && sscanf(a + 2, "%" B_SCNd32 ";", &first) == 1) {
			t[0] += strchr(a, ';') - a;
			return false;
		}

		const struct { const char *name; int32 code; } entities[] = {
			// this list is sorted alphabetically to be binary searchable
			// the current implementation doesn't do this, though

			// "name" is the entity name,
			// "code" is the corresponding unicode
			{"AElig;",	0x00c6},
			{"Aacute;",	0x00c1},
			{"Acirc;",	0x00c2},
			{"Agrave;",	0x00c0},
			{"Aring;",	0x00c5},
			{"Atilde;",	0x00c3},
			{"Auml;",	0x00c4},
			{"Ccedil;",	0x00c7},
			{"Eacute;",	0x00c9},
			{"Ecirc;",	0x00ca},
			{"Egrave;",	0x00c8},
			{"Euml;",	0x00cb},
			{"Iacute;", 0x00cd},
			{"Icirc;",	0x00ce},
			{"Igrave;", 0x00cc},
			{"Iuml;",	0x00cf},
			{"Ntilde;",	0x00d1},
			{"Oacute;", 0x00d3},
			{"Ocirc;",	0x00d4},
			{"Ograve;", 0x00d2},
			{"Ouml;",	0x00d6},
			{"Uacute;", 0x00da},
			{"Ucirc;",	0x00db},
			{"Ugrave;", 0x00d9},
			{"Uuml;",	0x00dc},
			{"aacute;", 0x00e1},
			{"acirc;",	0x00e2},
			{"aelig;",	0x00e6},
			{"agrave;", 0x00e0},
			{"amp;",	'&'},
			{"aring;",	0x00e5},
			{"atilde;", 0x00e3},
			{"auml;",	0x00e4},
			{"ccedil;",	0x00e7},
			{"copy;",	0x00a9},
			{"eacute;",	0x00e9},
			{"ecirc;",	0x00ea},
			{"egrave;",	0x00e8},
			{"euml;",	0x00eb},
			{"gt;",		'>'},
			{"iacute;", 0x00ed},
			{"icirc;",	0x00ee},
			{"igrave;", 0x00ec},
			{"iuml;",	0x00ef},
			{"lt;",		'<'},
			{"nbsp;",	' '},
			{"ntilde;",	0x00f1},
			{"oacute;", 0x00f3},
			{"ocirc;",	0x00f4},
			{"ograve;", 0x00f2},
			{"ouml;",	0x00f6},
			{"quot;",	'"'},
			{"szlig;",	0x00f6},
			{"uacute;", 0x00fa},
			{"ucirc;",	0x00fb},
			{"ugrave;", 0x00f9},
			{"uuml;",	0x00fc},
			{NULL, 0}
		};

		for (int32 i = 0; entities[i].name; i++) {
			// entities are case-sensitive
			int32 length = strlen(entities[i].name);
			if (!strncmp(a + 1, entities[i].name, length)) {
				t[0] += length;	// note that the '&' is included here
				first = entities[i].code;
				return false;
			}
		}
	}

	// no tag to filter
	if (first != '<')
		return false;

	a++;

	// is the tag one of the newline tags?

	bool newline = false;
	for (int i = 0; newlineTags[i]; i++) {
		int length = strlen(newlineTags[i]);
		if (!strncasecmp(a, (char *)newlineTags[i], length) && !isalnum(a[length])) {
			newline = true;
			break;
		}
	}

	// oh, it's not, so skip it!

	if (!strncasecmp(a, "head", 4)) {	// skip "head" completely
		for (; a[0] && a < end; a++) {
			// Find the end of the HEAD section, or the start of the BODY,
			// which happens for some malformed spam.
			if (strncasecmp (a, "</head", 6) == 0 ||
				strncasecmp (a, "<body", 5) == 0)
				break;
		}
	}

	// skip until tag end
	while (a[0] && a[0] != '>' && a < end)
		a++;

	t[0] = a;

	if (newline) {
		first = '\n';
		return false;
	}

	return true;
}


/** Returns the type and length of the URL in the string if it is one.
 *	If the "url" string is specified, it will fill it with the complete
 *	URL that might differ from the one in the text itself (i.e. because
 *	of an prepended "http://").
 */

static uint8
CheckForURL(const char *string, size_t &urlLength, BString *url = NULL)
{
	const char *urlPrefixes[] = {
		"http://",
		"ftp://",
		"shttp://",
		"https://",
		"finger://",
		"telnet://",
		"gopher://",
		"news://",
		"nntp://",
		"file://",
		NULL
	};

	//
	//	Search for URL prefix
	//
	uint8 type = 0;
	for (const char **prefix = urlPrefixes; *prefix != 0; prefix++) {
		if (!cistrncmp(string, *prefix, strlen(*prefix))) {
			type = TYPE_URL;
			break;
		}
	}

	//
	//	Not a URL? check for "mailto:" or "www."
	//
	if (type == 0 && !cistrncmp(string, "mailto:", 7))
		type = TYPE_MAILTO;
	if (type == 0 && !strncmp(string, "www.", 4)) {
		// this type will be special cased later (and a http:// is added
		// for the enclosure address)
		type = TYPE_URL;
	}
	if (type == 0) {
		// check for valid eMail addresses
		const char *at = strchr(string, '@');
		if (at) {
			const char *pos = string;
			bool readName = false;
			for (; pos < at; pos++) {
				// ToDo: are these all allowed characters?
				if (!isalnum(pos[0]) && pos[0] != '_' && pos[0] != '.' && pos[0] != '-')
					break;

				readName = true;
			}
			if (pos == at && readName)
				type = TYPE_MAILTO;
		}
	}

	if (type == 0)
		return 0;

	int32 index = strcspn(string, " \t<>)\"\\,\r\n");

	// filter out some punctuation marks if they are the last character
	char suffix = string[index - 1];
	if (suffix == '.'
		|| suffix == ','
		|| suffix == '?'
		|| suffix == '!'
		|| suffix == ':'
		|| suffix == ';')
		index--;

	if (type == TYPE_URL) {
		char *parenthesis = NULL;

		// filter out a trailing ')' if there is no left parenthesis before
		if (string[index - 1] == ')') {
			char *parenthesis = strchr(string, '(');
			if (parenthesis == NULL || parenthesis > string + index)
				index--;
		}

		// filter out a trailing ']' if there is no left bracket before
		if (parenthesis == NULL && string[index - 1] == ']') {
			char *parenthesis = strchr(string, '[');
			if (parenthesis == NULL || parenthesis > string + index)
				index--;
		}
	}

	if (url != NULL) {
		// copy the address to the specified string
		if (type == TYPE_URL && string[0] == 'w') {
			// URL starts with "www.", so add the protocol to it
			url->SetTo("http://");
			url->Append(string, index);
		} else if (type == TYPE_MAILTO && cistrncmp(string, "mailto:", 7)) {
			// eMail address had no "mailto:" prefix
			url->SetTo("mailto:");
			url->Append(string, index);
		} else
			url->SetTo(string, index);
	}
	urlLength = index;

	return type;
}


static void
CopyQuotes(const char *text, size_t length, char *outText, size_t &outLength)
{
	// count qoute level (to be able to wrap quotes correctly)

	const char *quote = QUOTE;
	int32 level = 0;
	for (size_t i = 0; i < length; i++) {
		if (text[i] == quote[0])
			level++;
		else if (text[i] != ' ' && text[i] != '\t')
			break;
	}

	// if there are too much quotes, try to preserve the quote color level
	if (level > 10)
		level = kNumQuoteColors * 3 + (level % kNumQuoteColors);

	// copy the quotes to outText

	const int32 quoteLength = strlen(QUOTE);
	outLength = 0;
	while (level-- > 0) {
		strcpy(outText + outLength, QUOTE);
		outLength += quoteLength;
	}
}


int32
diff_mode(char c)
{
	if (c == '+')
		return 2;
	if (c == '-')
		return 1;
	if (c == '@')
		return 3;
	if (c == ' ')
		return 0;

	// everything else ends the diff mode
	return -1;
}


bool
is_quote_char(char c)
{
	return c == '>' || c == '|';
}


/*!	Fills the specified text_run_array with the correct values for the
	specified text.
	If "view" is NULL, it will assume that "line" lies on a line break,
	if not, it will correctly retrieve the number of quotes the current
	line already has.
*/
void
FillInQuoteTextRuns(BTextView* view, quote_context* context, const char* line,
	int32 length, const BFont& font, text_run_array* style, int32 maxStyles)
{
	text_run* runs = style->runs;
	int32 index = style->count;
	bool begin;
	int32 pos = 0;
	int32 diffMode = 0;
	bool inDiff = false;
	bool wasDiff = false;
	int32 level = 0;

	// get index to the beginning of the current line

	if (context != NULL) {
		level = context->level;
		diffMode = context->diff_mode;
		begin = context->begin;
		inDiff = context->in_diff;
		wasDiff = context->was_diff;
	} else if (view != NULL) {
		int32 start, end;
		view->GetSelection(&end, &end);

		begin = view->TextLength() == 0
			|| view->ByteAt(view->TextLength() - 1) == '\n';

		// the following line works only reliable when text wrapping is set to
		// off; so the complicated version actually used here is necessary:
		// start = view->OffsetAt(view->CurrentLine());

		const char *text = view->Text();

		if (!begin) {
			// if the text is not the start of a new line, go back
			// to the first character in the current line
			for (start = end; start > 0; start--) {
				if (text[start - 1] == '\n')
					break;
			}
		}

		// get number of nested qoutes for current line

		if (!begin && start < end) {
			begin = true;
				// if there was no text in this line, there may come
				// more nested quotes

			diffMode = diff_mode(text[start]);
			if (diffMode == 0) {
				for (int32 i = start; i < end; i++) {
					if (is_quote_char(text[i]))
						level++;
					else if (text[i] != ' ' && text[i] != '\t') {
						begin = false;
						break;
					}
				}
			} else
				inDiff = true;

			if (begin) {
				// skip leading spaces (tabs & newlines aren't allowed here)
				while (line[pos] == ' ')
					pos++;
			}
		}
	} else
		begin = true;

	// set styles for all qoute levels in the text to be inserted

	for (int32 pos = 0; pos < length;) {
		int32 next;
		if (begin && is_quote_char(line[pos])) {
			begin = false;

			while (pos < length && line[pos] != '\n') {
				// insert style for each quote level
				level++;

				bool search = true;
				for (next = pos + 1; next < length; next++) {
					if ((search && is_quote_char(line[next]))
						|| line[next] == '\n')
						break;
					else if (search && line[next] != ' ' && line[next] != '\t')
						search = false;
				}

				runs[index].offset = pos;
				runs[index].font = font;
				runs[index].color = level > 0
					? kQuoteColors[level % kNumQuoteColors] : kNormalTextColor;

				pos = next;
				if (++index >= maxStyles)
					break;
			}
		} else {
			if (begin) {
				if (!inDiff) {
					inDiff = !strncmp(&line[pos], "--- ", 4);
					wasDiff = false;
				}
				if (inDiff) {
					diffMode = diff_mode(line[pos]);
					if (diffMode < 0) {
						inDiff = false;
						wasDiff = true;
					}
				}
			}

			runs[index].offset = pos;
			runs[index].font = font;
			if (wasDiff)
				runs[index].color = kDiffColors[diff_mode('@') - 1];
			else if (diffMode <= 0) {
				runs[index].color = level > 0
					? kQuoteColors[level % kNumQuoteColors] : kNormalTextColor;
			} else
				runs[index].color = kDiffColors[diffMode - 1];

			begin = false;

			for (next = pos; next < length; next++) {
				if (line[next] == '\n') {
					begin = true;
					wasDiff = false;
					break;
				}
			}

			pos = next;
			index++;
		}

		if (pos < length)
			begin = line[pos] == '\n';

		if (begin) {
			pos++;
			level = 0;
			wasDiff = false;

			// skip one leading space (tabs & newlines aren't allowed here)
			if (!inDiff && pos < length && line[pos] == ' ')
				pos++;
		}

		if (index >= maxStyles)
			break;
	}
	style->count = index;

	if (context) {
		// update context for next run
		context->level = level;
		context->diff_mode = diffMode;
		context->begin = begin;
		context->in_diff = inDiff;
		context->was_diff = wasDiff;
	}
}


//	#pragma mark -


TextRunArray::TextRunArray(size_t entries)
	:
	fNumEntries(entries)
{
	fArray = (text_run_array *)malloc(sizeof(int32) + sizeof(text_run) * entries);
	if (fArray != NULL)
		fArray->count = 0;
}


TextRunArray::~TextRunArray()
{
	free(fArray);
}


//	#pragma mark -


TContentView::TContentView(bool incoming, BFont* font,
	bool showHeader, bool coloredQuotes)
	:
	BView("m_content", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE),
	fFocus(false),
	fIncoming(incoming)
{
	SetViewUIColor(B_PANEL_BACKGROUND_COLOR);

	BGroupLayout* layout = new BGroupLayout(B_VERTICAL, 0);
	SetLayout(layout);

	fTextView = new TTextView(fIncoming, this, font, showHeader,
		coloredQuotes);

	BScrollView* scrollView = new BScrollView("", fTextView, 0, true, true);
	scrollView->SetBorders(BControlLook::B_TOP_BORDER);
	AddChild(scrollView);
}


void
TContentView::FindString(const char *str)
{
	int32	finish;
	int32	pass = 0;
	int32	start = 0;

	if (str == NULL)
		return;

	//
	//	Start from current selection or from the beginning of the pool
	//
	const char *text = fTextView->Text();
	int32 count = fTextView->TextLength();
	fTextView->GetSelection(&start, &finish);
	if (start != finish)
		start = finish;
	if (!count || text == NULL)
		return;

	//
	//	Do the find
	//
	while (pass < 2) {
		long found = -1;
		char lc = tolower(str[0]);
		char uc = toupper(str[0]);
		for (long i = start; i < count; i++) {
			if (text[i] == lc || text[i] == uc) {
				const char *s = str;
				const char *t = text + i;
				while (*s && (tolower(*s) == tolower(*t))) {
					s++;
					t++;
				}
				if (*s == 0) {
					found = i;
					break;
				}
			}
		}

		//
		//	Select the text if it worked
		//
		if (found != -1) {
			Window()->Activate();
			fTextView->Select(found, found + strlen(str));
			fTextView->ScrollToSelection();
			fTextView->MakeFocus(true);
			return;
		}
		else if (start) {
			start = 0;
			text = fTextView->Text();
			count = fTextView->TextLength();
			pass++;
		} else {
			beep();
			return;
		}
	}
}


void
TContentView::Focus(bool focus)
{
	if (fFocus != focus) {
		fFocus = focus;
		Draw(Frame());
	}
}


void
TContentView::MessageReceived(BMessage *msg)
{
	switch (msg->what) {
		case CHANGE_FONT:
		{
			BFont *font;
			msg->FindPointer("font", (void **)&font);
			fTextView->UpdateFont(font);
			fTextView->Invalidate(Bounds());
			break;
		}

		case M_QUOTE:
		{
			int32 start, finish;
			fTextView->GetSelection(&start, &finish);
			fTextView->AddQuote(start, finish);
			break;
		}
		case M_REMOVE_QUOTE:
		{
			int32 start, finish;
			fTextView->GetSelection(&start, &finish);
			fTextView->RemoveQuote(start, finish);
			break;
		}

		case M_SIGNATURE:
		{
			if (fTextView->IsReaderThreadRunning()) {
				// Do not add the signature until the reader thread
				// is finished. Resubmit the message for later processing
				Window()->PostMessage(msg);
				break;
			}

			entry_ref ref;
			msg->FindRef("ref", &ref);

			BFile file(&ref, B_READ_ONLY);
			if (file.InitCheck() == B_OK) {
				int32 start, finish;
				fTextView->GetSelection(&start, &finish);

				off_t size;
				file.GetSize(&size);
				if (size > 32768)	// safety against corrupt signatures
					break;

				char *signature = (char *)malloc(size);
				if (signature == NULL)
					break;
				ssize_t bytesRead = file.Read(signature, size);
				if (bytesRead < B_OK) {
					free (signature);
					break;
				}

				const char *text = fTextView->Text();
				int32 length = fTextView->TextLength();

				// reserve some empty lines before the signature
				const char* newLines = "\n\n\n\n";
				if (length && text[length - 1] == '\n')
					newLines++;

				fTextView->Select(length, length);
				fTextView->Insert(newLines, strlen(newLines));
				length += strlen(newLines);

				// append the signature
				fTextView->Select(length, length);
				fTextView->Insert(signature, bytesRead);
				fTextView->Select(length, length + bytesRead);
				fTextView->ScrollToSelection();

				// set the editing cursor position
				fTextView->Select(length - 2 , length - 2);
				fTextView->ScrollToSelection();
				free (signature);
			} else {
				beep();
				BAlert* alert = new BAlert("",
					B_TRANSLATE("An error occurred trying to open this "
						"signature."), B_TRANSLATE("Sorry"));
				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
				alert->Go();
			}
			break;
		}

		case M_FIND:
			FindString(msg->FindString("findthis"));
			break;

		default:
			BView::MessageReceived(msg);
	}
}


//	#pragma mark -


TTextView::TTextView(bool incoming, TContentView *view,
	BFont *font, bool showHeader, bool coloredQuotes)
	:
	BTextView("", B_WILL_DRAW | B_NAVIGABLE),

	fHeader(showHeader),
	fColoredQuotes(coloredQuotes),
	fReady(false),
	fYankBuffer(NULL),
	fLastPosition(-1),
	fMail(NULL),
	fFont(font),
	fParent(view),
	fStopLoading(false),
	fThread(0),
	fPanel(NULL),
	fIncoming(incoming),
	fSpellCheck(false),
	fRaw(false),
	fCursor(false),
	fFirstSpellMark(NULL)
{
	fStopSem = create_sem(1, "reader_sem");
	SetStylable(true);
	SetInsets(4, 4, 4, 4);
		// TODO: have some font size related value here
		// (ideally the same as in BTextControl, etc. from BControlLook)

	fEnclosures = new BList();

	// Enclosure pop up menu
	fEnclosureMenu = new BPopUpMenu("Enclosure", false, false);
	fEnclosureMenu->SetFont(be_plain_font);
	fEnclosureMenu->AddItem(new BMenuItem(
		B_TRANSLATE("Save attachment" B_UTF8_ELLIPSIS),	new BMessage(M_SAVE)));
	fEnclosureMenu->AddItem(new BMenuItem(B_TRANSLATE("Open attachment"),
		new BMessage(M_OPEN)));

	// Hyperlink pop up menu
	fLinkMenu = new BPopUpMenu("Link", false, false);
	fLinkMenu->SetFont(be_plain_font);
	fLinkMenu->AddItem(new BMenuItem(B_TRANSLATE("Open this link"),
		new BMessage(M_OPEN)));
	fLinkMenu->AddItem(new BMenuItem(B_TRANSLATE("Copy link location"),
		new BMessage(M_COPY)));

	SetDoesUndo(true);

	//Undo function
	fUndoBuffer.On();
	fInputMethodUndoBuffer.On();
	fUndoState.replaced = false;
	fUndoState.deleted = false;
	fInputMethodUndoState.active = false;
	fInputMethodUndoState.replace = false;
}


TTextView::~TTextView()
{
	ClearList();
	delete fPanel;

	if (fYankBuffer)
		free(fYankBuffer);

	delete_sem(fStopSem);
}


void
TTextView::UpdateFont(const BFont* newFont)
{
	fFont = *newFont;

	// update the text run array safely with new font
	text_run_array *runArray = RunArray(0, INT32_MAX);
	for (int i = 0; i < runArray->count; i++)
		runArray->runs[i].font = *newFont;

	SetRunArray(0, INT32_MAX, runArray);
	FreeRunArray(runArray);
}


void
TTextView::AttachedToWindow()
{
	BTextView::AttachedToWindow();
	fFont.SetSpacing(B_FIXED_SPACING);
	SetFontAndColor(&fFont);

	if (fMail != NULL) {
		LoadMessage(fMail, false, NULL);
		if (fIncoming)
			MakeEditable(false);
	}
}


void
TTextView::KeyDown(const char *key, int32 count)
{
	char		raw;
	int32		end;
	int32 		start;
	uint32		mods;
	BMessage	*msg;
	int32		textLen = TextLength();

	msg = Window()->CurrentMessage();
	mods = msg->FindInt32("modifiers");

	switch (key[0]) {
		case B_HOME:
			if (IsSelectable()) {
				if (IsEditable())
					BTextView::KeyDown(key, count);
				else {
					// scroll to the beginning
					Select(0, 0);
					ScrollToSelection();
				}
			}
			break;

		case B_END:
			if (IsSelectable()) {
				if (IsEditable())
					BTextView::KeyDown(key, count);
				else {
					// scroll to the end
					int32 length = TextLength();
					Select(length, length);
					ScrollToSelection();
				}
			}
			break;

		case 0x02:						// ^b - back 1 char
			if (IsSelectable()) {
				GetSelection(&start, &end);
				while (!IsInitialUTF8Byte(ByteAt(--start))) {
					if (start < 0) {
						start = 0;
						break;
					}
				}
				if (start >= 0) {
					Select(start, start);
					ScrollToSelection();
				}
			}
			break;

		case B_DELETE:
			if (IsSelectable()) {
				if ((key[0] == B_DELETE) || (mods & B_CONTROL_KEY)) {
					// ^d
					if (IsEditable()) {
						GetSelection(&start, &end);
						if (start != end)
							Delete();
						else {
							for (end = start + 1; !IsInitialUTF8Byte(ByteAt(end)); end++) {
								if (end > textLen) {
									end = textLen;
									break;
								}
							}
							Select(start, end);
							Delete();
						}
					}
				}
				else
					Select(textLen, textLen);
				ScrollToSelection();
			}
			break;

		case 0x05:						// ^e - end of line
			if (IsSelectable() && (mods & B_CONTROL_KEY)) {
				if (CurrentLine() == CountLines() - 1)
					Select(TextLength(), TextLength());
				else {
					GoToLine(CurrentLine() + 1);
					GetSelection(&start, &end);
					Select(start - 1, start - 1);
				}
			}
			break;

		case 0x06:						// ^f - forward 1 char
			if (IsSelectable()) {
				GetSelection(&start, &end);
				if (end > start)
					start = end;
				else {
					for (end = start + 1; !IsInitialUTF8Byte(ByteAt(end));
						end++) {
						if (end > textLen) {
							end = textLen;
							break;
						}
					}
					start = end;
				}
				Select(start, start);
				ScrollToSelection();
			}
			break;

		case 0x0e:						// ^n - next line
			if (IsSelectable()) {
				raw = B_DOWN_ARROW;
				BTextView::KeyDown(&raw, 1);
			}
			break;

		case 0x0f:						// ^o - open line
			if (IsEditable()) {
				GetSelection(&start, &end);
				Delete();

				char newLine = '\n';
				Insert(&newLine, 1);
				Select(start, start);
				ScrollToSelection();
			}
			break;

		case B_PAGE_UP:
			if (mods & B_CONTROL_KEY) {	// ^k kill text from cursor to e-o-line
				if (IsEditable()) {
					GetSelection(&start, &end);
					if ((start != fLastPosition) && (fYankBuffer)) {
						free(fYankBuffer);
						fYankBuffer = NULL;
					}
					fLastPosition = start;
					if (CurrentLine() < CountLines() - 1) {
						GoToLine(CurrentLine() + 1);
						GetSelection(&end, &end);
						end--;
					}
					else
						end = TextLength();
					if (end < start)
						break;
					if (start == end)
						end++;
					Select(start, end);
					if (fYankBuffer) {
						fYankBuffer = (char *)realloc(fYankBuffer,
									 strlen(fYankBuffer) + (end - start) + 1);
						GetText(start, end - start,
							    &fYankBuffer[strlen(fYankBuffer)]);
					} else {
						fYankBuffer = (char *)malloc(end - start + 1);
						GetText(start, end - start, fYankBuffer);
					}
					Delete();
					ScrollToSelection();
				}
				break;
			}

			BTextView::KeyDown(key, count);
			break;

		case 0x10:						// ^p goto previous line
			if (IsSelectable()) {
				raw = B_UP_ARROW;
				BTextView::KeyDown(&raw, 1);
			}
			break;

		case 0x19:						// ^y yank text
			if (IsEditable() && fYankBuffer) {
				Delete();
				Insert(fYankBuffer);
				ScrollToSelection();
			}
			break;

		default:
			BTextView::KeyDown(key, count);
	}
}


void
TTextView::MakeFocus(bool focus)
{
	if (!focus) {
		// ToDo: can someone please translate this? Otherwise I will remove it - axeld.
		// MakeFocus(false) は、IM も Inactive になり、そのまま確定される。
		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
		fInputMethodUndoState.active = false;
		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら（確定）正規のバッファへ追加
		if (fInputMethodUndoBuffer.CountItems() > 0) {
			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
			if (item->History == K_INSERTED) {
				fUndoBuffer.MakeNewUndoItem();
				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset, item->History, item->CursorPos);
				fUndoBuffer.MakeNewUndoItem();
			}
			fInputMethodUndoBuffer.MakeEmpty();
		}
	}
	BTextView::MakeFocus(focus);

	fParent->Focus(focus);
}


void
TTextView::MessageReceived(BMessage *msg)
{
	switch (msg->what) {
		case B_SIMPLE_DATA:
		{
			if (fIncoming)
				break;

			BMessage message(REFS_RECEIVED);
			bool isEnclosure = false;
			bool inserted = false;

			off_t len = 0;
			int32 end;
			int32 start;

			int32 index = 0;
			entry_ref ref;
			while (msg->FindRef("refs", index++, &ref) == B_OK) {
				BFile file(&ref, B_READ_ONLY);
				if (file.InitCheck() == B_OK) {
					BNodeInfo node(&file);
					char type[B_FILE_NAME_LENGTH];
					node.GetType(type);

					off_t size = 0;
					file.GetSize(&size);

					if (!strncasecmp(type, "text/", 5) && size > 0) {
						len += size;
						char *text = (char *)malloc(size);
						if (text == NULL) {
							puts("no memory!");
							return;
						}
						if (file.Read(text, size) < B_OK) {
							puts("could not read from file");
							free(text);
							continue;
						}
						if (!inserted) {
							GetSelection(&start, &end);
							Delete();
							inserted = true;
						}

						int32 offset = 0;
						for (int32 loop = 0; loop < size; loop++) {
							if (text[loop] == '\n') {
								Insert(&text[offset], loop - offset + 1);
								offset = loop + 1;
							} else if (text[loop] == '\r') {
								text[loop] = '\n';
								Insert(&text[offset], loop - offset + 1);
								if ((loop + 1 < size)
										&& (text[loop + 1] == '\n'))
									loop++;
								offset = loop + 1;
							}
						}
						free(text);
					} else {
						isEnclosure = true;
						message.AddRef("refs", &ref);
					}
				}
			}

			if (index == 1) {
				// message doesn't contain any refs - maybe the parent class likes it
				BTextView::MessageReceived(msg);
				break;
			}

			if (inserted)
				Select(start, start + len);
			if (isEnclosure)
				Window()->PostMessage(&message, Window());
			break;
		}

		case M_HEADER:
			msg->FindBool("header", &fHeader);
			SetText(NULL);
			LoadMessage(fMail, false, NULL);
			break;

		case M_RAW:
			StopLoad();

			msg->FindBool("raw", &fRaw);
			SetText(NULL);
			LoadMessage(fMail, false, NULL);
			break;

		case M_SELECT:
			if (IsSelectable())
				Select(0, TextLength());
			break;

		case M_SAVE:
			Save(msg);
			break;

		case B_NODE_MONITOR:
		{
			int32 opcode;
			if (msg->FindInt32("opcode", &opcode) == B_NO_ERROR) {
				dev_t device;
#if defined( HoG_GENODE )
				if (msg->FindInt64("device", &device) < B_OK)
#else
				if (msg->FindInt32("device", &device) < B_OK)
#endif
					break;
				ino_t inode;
#if defined( HoG_GENODE )
				if (msg->FindUInt64("node", &inode) < B_OK)
#else
				if (msg->FindInt64("node", &inode) < B_OK)
#endif
					break;

				hyper_text *enclosure;
				for (int32 index = 0;
						(enclosure = (hyper_text *)fEnclosures->ItemAt(index++)) != NULL;) {
					if (device == enclosure->node.device
						&& inode == enclosure->node.node) {
						if (opcode == B_ENTRY_REMOVED) {
							enclosure->saved = false;
							enclosure->have_ref = false;
						} else if (opcode == B_ENTRY_MOVED) {
							enclosure->ref.device = device;
#if defined( HoG_GENODE )
							msg->FindUInt64("to directory", &enclosure->ref.directory);
#else
							msg->FindInt64("to directory", &enclosure->ref.directory);
#endif

							const char *name;
							msg->FindString("name", &name);
							enclosure->ref.set_name(name);
						}
						break;
					}
				}
			}
			break;
		}

		//
		// Tracker has responded to a BMessage that was dragged out of
		// this email message.  It has created a file for us, we just have to
		// put the stuff in it.
		//
		case B_COPY_TARGET:
		{
			BMessage data;
			if (msg->FindMessage("be:originator-data", &data) == B_OK) {
				entry_ref directory;
				const char *name;
				hyper_text *enclosure;

				if (data.FindPointer("enclosure", (void **)&enclosure) == B_OK
					&& msg->FindString("name", &name) == B_OK
					&& msg->FindRef("directory", &directory) == B_OK) {
					switch (enclosure->type) {
						case TYPE_ENCLOSURE:
						case TYPE_BE_ENCLOSURE:
						{
							//
							//	Enclosure.  Decode the data and write it out.
							//
							BMessage saveMsg(M_SAVE);
							saveMsg.AddString("name", name);
							saveMsg.AddRef("directory", &directory);
							saveMsg.AddPointer("enclosure", enclosure);
							Save(&saveMsg, false);
							break;
						}

						case TYPE_URL:
						{
							const char *replyType;
							if (msg->FindString("be:filetypes", &replyType) != B_OK)
								// drag recipient didn't ask for any specific type,
								// create a bookmark file as default
								replyType = "application/x-vnd.Be-bookmark";

							BDirectory dir(&directory);
							BFile file(&dir, name, B_READ_WRITE);
							if (file.InitCheck() == B_OK) {
								if (strcmp(replyType, "application/x-vnd.Be-bookmark") == 0) {
									// we got a request to create a bookmark, stuff
									// it with the url attribute
									file.WriteAttr("META:url", B_STRING_TYPE, 0,
													enclosure->name, strlen(enclosure->name) + 1);
								} else if (strcasecmp(replyType, "text/plain") == 0) {
									// create a plain text file, stuff it with
									// the url as text
									file.Write(enclosure->name, strlen(enclosure->name));
								}

								BNodeInfo fileInfo(&file);
								fileInfo.SetType(replyType);
							}
							break;
						}

						case TYPE_MAILTO:
						{
							//
							//	Add some attributes to the already created
							//	person file.  Strip out the 'mailto:' if
							//  possible.
							//
							char *addrStart = enclosure->name;
							while (true) {
								if (*addrStart == ':') {
									addrStart++;
									break;
								}

								if (*addrStart == '\0') {
									addrStart = enclosure->name;
									break;
								}

								addrStart++;
							}

							const char *replyType;
							if (msg->FindString("be:filetypes", &replyType) != B_OK)
								// drag recipient didn't ask for any specific type,
								// create a bookmark file as default
								replyType = "application/x-vnd.Be-bookmark";

							BDirectory dir(&directory);
							BFile file(&dir, name, B_READ_WRITE);
							if (file.InitCheck() == B_OK) {
								if (!strcmp(replyType, "application/x-person")) {
									// we got a request to create a bookmark, stuff
									// it with the address attribute
									file.WriteAttr("META:email", B_STRING_TYPE, 0,
									  addrStart, strlen(enclosure->name) + 1);
								} else if (!strcasecmp(replyType, "text/plain")) {
									// create a plain text file, stuff it with the
									// email as text
									file.Write(addrStart, strlen(addrStart));
								}

								BNodeInfo fileInfo(&file);
								fileInfo.SetType(replyType);
							}
							break;
						}
					}
				} else {
					//
					// Assume this is handled by BTextView...
					// (Probably drag clipping.)
					//
					BTextView::MessageReceived(msg);
				}
			}
			break;
		}

		case B_INPUT_METHOD_EVENT:
		{
			int32 im_op;
			if (msg->FindInt32("be:opcode", &im_op) == B_OK){
				switch (im_op) {
					case B_INPUT_METHOD_STARTED:
						fInputMethodUndoState.replace = true;
						fInputMethodUndoState.active = true;
						break;
					case B_INPUT_METHOD_STOPPED:
						fInputMethodUndoState.active = false;
						if (fInputMethodUndoBuffer.CountItems() > 0) {
							KUndoItem *undo = fInputMethodUndoBuffer.ItemAt(
								fInputMethodUndoBuffer.CountItems() - 1);
							if (undo->History == K_INSERTED){
								fUndoBuffer.MakeNewUndoItem();
								fUndoBuffer.AddUndo(undo->RedoText, undo->Length,
									undo->Offset, undo->History, undo->CursorPos);
								fUndoBuffer.MakeNewUndoItem();
							}
							fInputMethodUndoBuffer.MakeEmpty();
						}
						break;
					case B_INPUT_METHOD_CHANGED:
						fInputMethodUndoState.active = true;
						break;
					case B_INPUT_METHOD_LOCATION_REQUEST:
						fInputMethodUndoState.active = true;
						break;
				}
			}
			BTextView::MessageReceived(msg);
			break;
		}

		case M_REDO:
			Redo();
			break;

		default:
			BTextView::MessageReceived(msg);
	}
}


void
TTextView::MouseDown(BPoint where)
{
	if (IsEditable()) {
		BPoint point;
		uint32 buttons;
		GetMouse(&point, &buttons);
		if (gDictCount && (buttons == B_SECONDARY_MOUSE_BUTTON)) {
			int32 offset, start, end, length;
			const char *text = Text();
			offset = OffsetAt(where);
			if (isalpha(text[offset])) {
				length = TextLength();

				//Find start and end of word
				//FindSpellBoundry(length, offset, &start, &end);

				char c;
				bool isAlpha, isApost, isCap;
				int32 first;

				for (first = offset;
					(first >= 0) && (((c = text[first]) == '\'') || isalpha(c));
					first--) {}
				isCap = isupper(text[++first]);

				for (start = offset, c = text[start], isAlpha = isalpha(c), isApost = (c=='\'');
					(start >= 0) && (isAlpha || (isApost
					&& (((c = text[start+1]) != 's') || !isCap) && isalpha(c)
					&& isalpha(text[start-1])));
					start--, c = text[start], isAlpha = isalpha(c), isApost = (c == '\'')) {}
				start++;

				for (end = offset, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'');
					(end < length) && (isAlpha || (isApost
					&& (((c = text[end + 1]) != 's') || !isCap) && isalpha(c)));
					end++, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'')) {}

				length = end - start;
				BString srcWord;
				srcWord.SetTo(text + start, length);

				bool		foundWord = false;
				BList 		matches;
				BString 	*string;

				BMenuItem *menuItem;
				BPopUpMenu menu("Words", false, false);

				for (int32 i = 0; i < gDictCount; i++)
					gWords[i]->FindBestMatches(&matches,
						srcWord.String());

				if (matches.CountItems()) {
					sort_word_list(&matches, srcWord.String());
					for (int32 i = 0; (string = (BString *)matches.ItemAt(i)) != NULL; i++) {
						menu.AddItem((menuItem = new BMenuItem(string->String(), NULL)));
						if (!strcasecmp(string->String(), srcWord.String())) {
							menuItem->SetEnabled(false);
							foundWord = true;
						}
						delete string;
					}
				} else {
					menuItem = new BMenuItem(B_TRANSLATE("No matches"), NULL);
					menuItem->SetEnabled(false);
					menu.AddItem(menuItem);
				}

				BMenuItem *addItem = NULL;
				if (!foundWord && gUserDict >= 0) {
					menu.AddSeparatorItem();
					addItem = new BMenuItem(B_TRANSLATE("Add"), NULL);
					menu.AddItem(addItem);
				}

				point = ConvertToScreen(where);
				if ((menuItem = menu.Go(point, false, false)) != NULL) {
					if (menuItem == addItem) {
						BString newItem(srcWord.String());
						newItem << "\n";
						gWords[gUserDict]->InitIndex();
						gExactWords[gUserDict]->InitIndex();
						gUserDictFile->Write(newItem.String(), newItem.Length());
						gWords[gUserDict]->BuildIndex();
						gExactWords[gUserDict]->BuildIndex();

						if (fSpellCheck)
							CheckSpelling(0, TextLength());
					} else {
						int32 len = strlen(menuItem->Label());
						Select(start, start);
						Delete(start, end);
						Insert(start, menuItem->Label(), len);
						Select(start+len, start+len);
					}
				}
			}
			return;
		} else if (fSpellCheck && IsEditable()) {
			int32 start, end;

			GetSelection(&start, &end);
			FindSpellBoundry(1, start, &start, &end);
			CheckSpelling(start, end);
		}
	} else {
		// is not editable, look for enclosures/links

		int32 clickOffset = OffsetAt(where);
		int32 items = fEnclosures->CountItems();
		for (int32 loop = 0; loop < items; loop++) {
			hyper_text *enclosure = (hyper_text*) fEnclosures->ItemAt(loop);
			if (clickOffset < enclosure->text_start || clickOffset >= enclosure->text_end)
				continue;

			//
			// The user is clicking on this attachment
			//

			int32 start;
			int32 finish;
			Select(enclosure->text_start, enclosure->text_end);
			GetSelection(&start, &finish);
			Window()->UpdateIfNeeded();

			bool drag = false;
			bool held = false;
			uint32 buttons = 0;
			if (Window()->CurrentMessage()) {
				Window()->CurrentMessage()->FindInt32("buttons",
				  (int32 *) &buttons);
			}

			//
			// If this is the primary button, wait to see if the user is going
			// to single click, hold, or drag.
			//
			if (buttons != B_SECONDARY_MOUSE_BUTTON) {
				BPoint point = where;
				bigtime_t popupDelay;
				get_click_speed(&popupDelay);
				popupDelay *= 2;
				popupDelay += system_time();
				while (buttons && abs((int)(point.x - where.x)) < 4
					&& abs((int)(point.y - where.y)) < 4
					&& system_time() < popupDelay) {
					snooze(10000);
					GetMouse(&point, &buttons);
				}

				if (system_time() < popupDelay) {
					//
					// The user either dragged this or released the button.
					// check if it was dragged.
					//
					if (!(abs((int)(point.x - where.x)) < 4
						&& abs((int)(point.y - where.y)) < 4) && buttons)
						drag = true;
				} else  {
					//
					//	The user held the button down.
					//
					held = true;
				}
			}

			//
			//	If the user has right clicked on this menu,
			// 	or held the button down on it for a while,
			//	pop up a context menu.
			//
			if (buttons == B_SECONDARY_MOUSE_BUTTON || held) {
				//
				// Right mouse click... Display a menu
				//
				BPoint point = where;
				ConvertToScreen(&point);

				BMenuItem *item;
				if ((enclosure->type != TYPE_ENCLOSURE)
					&& (enclosure->type != TYPE_BE_ENCLOSURE))
					item = fLinkMenu->Go(point, true);
				else
					item = fEnclosureMenu->Go(point, true);

				BMessage *msg;
				if (item && (msg = item->Message()) != NULL) {
					if (msg->what == M_SAVE) {
						if (fPanel)
							fPanel->SetEnclosure(enclosure);
						else {
							fPanel = new TSavePanel(enclosure, this);
							fPanel->Window()->Show();
						}
					} else if (msg->what == M_COPY) {
						// copy link location to clipboard

						if (be_clipboard->Lock()) {
							be_clipboard->Clear();

							BMessage *clip;
							if ((clip = be_clipboard->Data()) != NULL) {
								clip->AddData("text/plain", B_MIME_TYPE,
									enclosure->name, strlen(enclosure->name));
								be_clipboard->Commit();
							}
							be_clipboard->Unlock();
						}
					} else
						Open(enclosure);
				}
			} else {
				//
				// Left button.  If the user single clicks, open this link.
				// Otherwise, initiate a drag.
				//
				if (drag) {
					BMessage dragMessage(B_SIMPLE_DATA);
					dragMessage.AddInt32("be:actions", B_COPY_TARGET);
					dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
					switch (enclosure->type) {
						case TYPE_BE_ENCLOSURE:
						case TYPE_ENCLOSURE:
							//
							// Attachment.  The type is specified in the message.
							//
							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
							dragMessage.AddString("be:filetypes",
								enclosure->content_type ? enclosure->content_type : "");
							dragMessage.AddString("be:clip_name", enclosure->name);
							break;

						case TYPE_URL:
							//
							// URL.  The user can drag it into the tracker to
							// create a bookmark file.
							//
							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
							dragMessage.AddString("be:filetypes",
							  "application/x-vnd.Be-bookmark");
							dragMessage.AddString("be:filetypes", "text/plain");
							dragMessage.AddString("be:clip_name", "Bookmark");

							dragMessage.AddString("be:url", enclosure->name);
							break;

						case TYPE_MAILTO:
							//
							// Mailto address.  The user can drag it into the
							// tracker to create a people file.
							//
							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
							dragMessage.AddString("be:filetypes",
							  "application/x-person");
							dragMessage.AddString("be:filetypes", "text/plain");
							dragMessage.AddString("be:clip_name", "Person");

							dragMessage.AddString("be:email", enclosure->name);
							break;

						default:
							//
							// Otherwise it doesn't have a type that I know how
							// to save.  It won't have any types and if any
							// program wants to accept it, more power to them.
							// (tracker won't.)
							//
							dragMessage.AddString("be:clip_name", "Hyperlink");
					}

					BMessage data;
					data.AddPointer("enclosure", enclosure);
					dragMessage.AddMessage("be:originator-data", &data);

					BRegion selectRegion;
					GetTextRegion(start, finish, &selectRegion);
					DragMessage(&dragMessage, selectRegion.Frame(), this);
				} else {
					//
					//	User Single clicked on the attachment.  Open it.
					//
					Open(enclosure);
				}
			}
			return;
		}
	}
	BTextView::MouseDown(where);
}


void
TTextView::MouseMoved(BPoint where, uint32 code, const BMessage *msg)
{
	int32 start = OffsetAt(where);

	for (int32 loop = fEnclosures->CountItems(); loop-- > 0;) {
		hyper_text *enclosure = (hyper_text *)fEnclosures->ItemAt(loop);
		if ((start >= enclosure->text_start) && (start < enclosure->text_end)) {
			if (!fCursor)
				SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
			fCursor = true;
			return;
		}
	}

	if (fCursor) {
		SetViewCursor(B_CURSOR_I_BEAM);
		fCursor = false;
	}

	BTextView::MouseMoved(where, code, msg);
}


void
TTextView::ClearList()
{
	hyper_text *enclosure;
	while ((enclosure = (hyper_text *)fEnclosures->FirstItem()) != NULL) {
		fEnclosures->RemoveItem(enclosure);

		if (enclosure->name)
			free(enclosure->name);
		if (enclosure->content_type)
			free(enclosure->content_type);
		if (enclosure->encoding)
			free(enclosure->encoding);
		if (enclosure->have_ref && !enclosure->saved) {
			BEntry entry(&enclosure->ref);
			entry.Remove();
		}

		watch_node(&enclosure->node, B_STOP_WATCHING, this);
		free(enclosure);
	}
}


void
TTextView::LoadMessage(BEmailMessage *mail, bool quoteIt, const char *text)
{
	StopLoad();

	fMail = mail;

	ClearList();

	MakeSelectable(true);
	MakeEditable(false);
	if (text)
		Insert(text, strlen(text));

	//attr_info attrInfo;
	TTextView::Reader *reader = new TTextView::Reader(fHeader, fRaw, quoteIt, fIncoming,
				text != NULL, true,
				// I removed the following, because I absolutely can't imagine why it's
				// there (the mail kit should be able to deal with non-compliant mails)
				// -- axeld.
				// fFile->GetAttrInfo(B_MAIL_ATTR_MIME, &attrInfo) == B_OK,
				this, mail, fEnclosures, fStopSem);

	resume_thread(fThread = spawn_thread(Reader::Run, "reader", B_NORMAL_PRIORITY, reader));
}


void
TTextView::Open(hyper_text *enclosure)
{
	switch (enclosure->type) {
		case TYPE_URL:
		{
			const struct {const char *urlType, *handler; } handlerTable[] = {
				{"http",	B_URL_HTTP},
				{"https",	B_URL_HTTPS},
				{"ftp",		B_URL_FTP},
				{"gopher",	B_URL_GOPHER},
				{"mailto",	B_URL_MAILTO},
				{"news",	B_URL_NEWS},
				{"nntp",	B_URL_NNTP},
				{"telnet",	B_URL_TELNET},
				{"rlogin",	B_URL_RLOGIN},
				{"tn3270",	B_URL_TN3270},
				{"wais",	B_URL_WAIS},
				{"file",	B_URL_FILE},
				{NULL,		NULL}
			};
			const char *handlerToLaunch = NULL;

			const char *colonPos = strchr(enclosure->name, ':');
			if (colonPos) {
				int urlTypeLength = colonPos - enclosure->name;

				for (int32 index = 0; handlerTable[index].urlType; index++) {
					if (!strncasecmp(enclosure->name,
							handlerTable[index].urlType, urlTypeLength)) {
						handlerToLaunch = handlerTable[index].handler;
						break;
					}
				}
			}
			if (handlerToLaunch) {
				entry_ref appRef;
				if (be_roster->FindApp(handlerToLaunch, &appRef) != B_OK)
					handlerToLaunch = NULL;
			}
			if (!handlerToLaunch)
				handlerToLaunch = "application/x-vnd.Be-Bookmark";

			status_t result = be_roster->Launch(handlerToLaunch, 1, &enclosure->name);
			if (result != B_NO_ERROR && result != B_ALREADY_RUNNING) {
				beep();
				BAlert* alert = new BAlert("",
					B_TRANSLATE("There is no installed handler for "
						"URL links."), B_TRANSLATE("Sorry"));
				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
				alert->Go();
			}
			break;
		}

		case TYPE_MAILTO:
			if (be_roster->Launch(B_MAIL_TYPE, 1, &enclosure->name) < B_OK) {
				char *argv[] = {(char *)"Mail", enclosure->name};
				be_app->ArgvReceived(2, argv);
			}
			break;

		case TYPE_ENCLOSURE:
		case TYPE_BE_ENCLOSURE:
			if (!enclosure->have_ref) {
				BPath path;
				if (find_directory(B_SYSTEM_TEMP_DIRECTORY, &path) == B_NO_ERROR) {
					BDirectory dir(path.Path());
					if (dir.InitCheck() == B_NO_ERROR) {
						char name[B_FILE_NAME_LENGTH];
						char baseName[B_FILE_NAME_LENGTH];
						strcpy(baseName, enclosure->name ? enclosure->name : "enclosure");
						strcpy(name, baseName);
						for (int32 index = 0; dir.Contains(name); index++)
							sprintf(name, "%s_%" B_PRId32, baseName, index);

						BEntry entry(path.Path());
						entry_ref ref;
						entry.GetRef(&ref);

						BMessage save(M_SAVE);
						save.AddRef("directory", &ref);
						save.AddString("name", name);
						save.AddPointer("enclosure", enclosure);
						if (Save(&save) != B_NO_ERROR)
							break;
						enclosure->saved = false;
					}
				}
			}

			BMessenger tracker("application/x-vnd.Be-TRAK");
			if (tracker.IsValid()) {
				BMessage openMsg(B_REFS_RECEIVED);
				openMsg.AddRef("refs", &enclosure->ref);
				tracker.SendMessage(&openMsg);
			}
			break;
	}
}


status_t
TTextView::Save(BMessage *msg, bool makeNewFile)
{
	const char		*name;
	entry_ref		ref;
	BFile			file;
	BPath			path;
	hyper_text		*enclosure;
	status_t		result = B_NO_ERROR;
	char 			entry_name[B_FILE_NAME_LENGTH];

	msg->FindString("name", &name);
	msg->FindRef("directory", &ref);
	msg->FindPointer("enclosure", (void **)&enclosure);

	BDirectory dir;
	dir.SetTo(&ref);
	result = dir.InitCheck();

	if (result == B_OK) {
		if (makeNewFile) {
			//
			// Search for the file and delete it if it already exists.
			// (It may not, that's ok.)
			//
			BEntry entry;
			if (dir.FindEntry(name, &entry) == B_NO_ERROR)
				entry.Remove();

			if ((enclosure->have_ref) && (!enclosure->saved)) {
				entry.SetTo(&enclosure->ref);

				//
				// Added true arg and entry_name so MoveTo clobbers as
				// before. This may not be the correct behaviour, but
				// it's the preserved behaviour.
				//
				entry.GetName(entry_name);
				result = entry.MoveTo(&dir, entry_name, true);
				if (result == B_NO_ERROR) {
					entry.Rename(name);
					entry.GetRef(&enclosure->ref);
					entry.GetNodeRef(&enclosure->node);
					enclosure->saved = true;
					return result;
				}
			}

			if (result == B_NO_ERROR) {
				result = dir.CreateFile(name, &file);
				if (result == B_NO_ERROR && enclosure->content_type) {
					char type[B_MIME_TYPE_LENGTH];

					if (!strcasecmp(enclosure->content_type, "message/rfc822"))
						strcpy(type, "text/x-email");
					else if (!strcasecmp(enclosure->content_type, "message/delivery-status"))
						strcpy(type, "text/plain");
					else
						strcpy(type, enclosure->content_type);

					BNodeInfo info(&file);
					info.SetType(type);
				}
			}
		} else {
			//
			// 	This file was dragged into the tracker or desktop.  The file
			//	already exists.
			//
			result = file.SetTo(&dir, name, B_WRITE_ONLY);
		}
	}

	if (enclosure->component == NULL)
		result = B_ERROR;

	if (result == B_NO_ERROR) {
		//
		// Write the data
		//
		enclosure->component->GetDecodedData(&file);

		BEntry entry;
		dir.FindEntry(name, &entry);
		entry.GetRef(&enclosure->ref);
		enclosure->have_ref = true;
		enclosure->saved = true;
		entry.GetPath(&path);
		update_mime_info(path.Path(), false, true,
			!cistrcmp("application/octet-stream", enclosure->content_type ? enclosure->content_type : B_EMPTY_STRING));
		entry.GetNodeRef(&enclosure->node);
		watch_node(&enclosure->node, B_WATCH_NAME, this);
	}

	if (result != B_NO_ERROR) {
		beep();
		BAlert* alert = new BAlert("", B_TRANSLATE("An error occurred trying to save "
				"the attachment."),	B_TRANSLATE("Sorry"));
		alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
		alert->Go();
	}

	return result;
}


void
TTextView::StopLoad()
{
	Window()->Unlock();

	thread_info	info;
	if (fThread != 0 && get_thread_info(fThread, &info) == B_NO_ERROR) {
		fStopLoading = true;
		acquire_sem(fStopSem);
		int32 result;
		wait_for_thread(fThread, &result);
		fThread = 0;
		release_sem(fStopSem);
		fStopLoading = false;
	}

	Window()->Lock();
}


bool
TTextView::IsReaderThreadRunning()
{
	if (fThread == 0)
		return false;

	thread_info info;
	for (int i = 5; i > 0; i--, usleep(100000))
		if (get_thread_info(fThread, &info) != B_OK)
			return false;
	return true;
}


void
TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_encoding encoding)
{
	if (mail == NULL)
		return;

	int32 textLength = TextLength();
	const char *text = Text();

	BTextMailComponent *body = mail->Body();
	if (body == NULL) {
		if (mail->SetBody(body = new BTextMailComponent()) < B_OK)
			return;
	}
	body->SetEncoding(encoding, charset);

	// Just add the text as a whole if we can, or ...
	if (!wrap) {
		body->AppendText(text);
		return;
	}

	// ... do word wrapping.

	BWindow	*window = Window();
	char *saveText = strdup(text);
	BRect saveTextRect = TextRect();

	// do this before we start messing with the fonts
	// the user will never know...
	window->DisableUpdates();
	Hide();
	BScrollBar *vScroller = ScrollBar(B_VERTICAL);
	BScrollBar *hScroller = ScrollBar(B_HORIZONTAL);
	if (vScroller != NULL)
		vScroller->SetTarget((BView *)NULL);
	if (hScroller != NULL)
		hScroller->SetTarget((BView *)NULL);

	// Temporarily set the font to a fixed width font for line wrapping
	// calculations.  If the font doesn't have as many of the symbols as
	// the preferred font, go back to using the user's preferred font.

	bool *boolArray;
	int missingCharactersFixedWidth = 0;
	int missingCharactersPreferredFont = 0;
	int32 numberOfCharacters;

	numberOfCharacters = BString(text).CountChars();
	if (numberOfCharacters > 0
		&& (boolArray = (bool *)malloc(sizeof(bool) * numberOfCharacters)) != NULL) {
		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
		be_fixed_font->GetHasGlyphs(text, numberOfCharacters, boolArray);
		for (int i = 0; i < numberOfCharacters; i++) {
			if (!boolArray[i])
				missingCharactersFixedWidth += 1;
		}

		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
		fFont.GetHasGlyphs(text, numberOfCharacters, boolArray);
		for (int i = 0; i < numberOfCharacters; i++) {
			if (!boolArray[i])
				missingCharactersPreferredFont += 1;
		}

		free(boolArray);
	}

	if (missingCharactersFixedWidth > missingCharactersPreferredFont)
		SetFontAndColor(0, textLength, &fFont);
	else // All things being equal, the fixed font is better for wrapping.
		SetFontAndColor(0, textLength, be_fixed_font);

	// calculate a text rect that is 72 columns wide
	BRect newTextRect = saveTextRect;
	newTextRect.right = newTextRect.left + be_fixed_font->StringWidth("m") * 72;
	SetTextRect(newTextRect);

	// hard-wrap, based on TextView's soft-wrapping
	int32 numLines = CountLines();
	bool spaceMoved = false;
	char *content = (char *)malloc(textLength + numLines * 72);	// more we'll ever need
	if (content != NULL) {
		int32 contentLength = 0;

		for (int32 i = 0; i < numLines; i++) {
			int32 startOffset = OffsetAt(i);
			if (spaceMoved) {
				startOffset++;
				spaceMoved = false;
			}
			int32 endOffset = OffsetAt(i + 1);
			int32 lineLength = endOffset - startOffset;

			// quick hack to not break URLs into several parts
			for (int32 pos = startOffset; pos < endOffset; pos++) {
				size_t urlLength;
				uint8 type = CheckForURL(text + pos, urlLength);
				if (type != 0)
					pos += urlLength;

				if (pos > endOffset) {
					// find first break character after the URL
					for (; text[pos]; pos++) {
						if (isalnum(text[pos]) || isspace(text[pos]))
							break;
					}
					if (text[pos] && isspace(text[pos]) && text[pos] != '\n')
						pos++;

					endOffset += pos - endOffset;
					lineLength = endOffset - startOffset;

					// insert a newline (and the same number of quotes) after the
					// URL to make sure the rest of the text is properly wrapped

					char buffer[64];
					if (text[pos] == '\n')
						buffer[0] = '\0';
					else
						strcpy(buffer, "\n");

					size_t quoteLength;
					CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength);

					Insert(pos, buffer, strlen(buffer));
					numLines = CountLines();
					text = Text();
					i++;
				}
			}
			if (text[endOffset - 1] != ' '
				&& text[endOffset - 1] != '\n'
				&& text[endOffset] == ' ') {
				// make sure spaces will be part of this line
				endOffset++;
				lineLength++;
				spaceMoved = true;
			}

			memcpy(content + contentLength, text + startOffset, lineLength);
			contentLength += lineLength;

			// add a newline to every line except for the ones
			// that already end in newlines, and the last line
			if ((text[endOffset - 1] != '\n') && (i < numLines - 1)) {
				content[contentLength++] = '\n';

				// copy quote level of the first line
				size_t quoteLength;
				CopyQuotes(text + startOffset, lineLength, content + contentLength, quoteLength);
				contentLength += quoteLength;
			}
		}
		content[contentLength] = '\0';

		body->AppendText(content);
		free(content);
	}

	// reset the text rect and font
	SetTextRect(saveTextRect);
	SetText(saveText);
	free(saveText);
	SetFontAndColor(0, textLength, &fFont);

	// should be OK to hook these back up now
	if (vScroller != NULL)
		vScroller->SetTarget(this);
	if (hScroller != NULL)
		hScroller->SetTarget(this);

	Show();
	window->EnableUpdates();
}


//	#pragma mark -


TTextView::Reader::Reader(bool header, bool raw, bool quote, bool incoming,
		bool stripHeader, bool mime, TTextView *view, BEmailMessage *mail,
		BList *list, sem_id sem)
	:
	fHeader(header),
	fRaw(raw),
	fQuote(quote),
	fIncoming(incoming),
	fStripHeader(stripHeader),
	fMime(mime),
	fView(view),
	fMail(mail),
	fEnclosures(list),
	fStopSem(sem)
{
}


bool
TTextView::Reader::ParseMail(BMailContainer *container,
	BTextMailComponent *ignore)
{
	int32 count = 0;
	for (int32 i = 0; i < container->CountComponents(); i++) {
		if (fView->fStopLoading)
			return false;

		BMailComponent *component;
		if ((component = container->GetComponent(i)) == NULL) {
			if (fView->fStopLoading)
				return false;

			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
			if (enclosure == NULL)
				return false;

			memset(enclosure, 0, sizeof(hyper_text));

			enclosure->type = TYPE_ENCLOSURE;

			const char *name = "\n<Attachment: could not handle>\n";

			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
			enclosure->text_start++;
			enclosure->text_end += strlen(name) - 1;

			Insert(name, strlen(name), true);
			fEnclosures->AddItem(enclosure);
			continue;
		}

		count++;
		if (component == ignore)
			continue;

		if (component->ComponentType() == B_MAIL_MULTIPART_CONTAINER) {
			BMIMEMultipartMailContainer *c = dynamic_cast<BMIMEMultipartMailContainer *>(container->GetComponent(i));
			ASSERT(c != NULL);

			if (!ParseMail(c, ignore))
				count--;
		} else if (fIncoming) {
			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
			if (enclosure == NULL)
				return false;

			memset(enclosure, 0, sizeof(hyper_text));

			enclosure->type = TYPE_ENCLOSURE;
			enclosure->component = component;

			BString name;
			char fileName[B_FILE_NAME_LENGTH];
			strcpy(fileName, "untitled");
			if (BMailAttachment *attachment = dynamic_cast <BMailAttachment *> (component))
				attachment->FileName(fileName);

			BPath path(fileName);
			enclosure->name = strdup(path.Leaf());

			BMimeType type;
			component->MIMEType(&type);
			enclosure->content_type = strdup(type.Type());

			char typeDescription[B_MIME_TYPE_LENGTH];
			if (type.GetShortDescription(typeDescription) != B_OK)
				strcpy(typeDescription, type.Type() ? type.Type() : B_EMPTY_STRING);

			name = "\n<";
			name.Append(B_TRANSLATE_COMMENT("Enclosure: %name% (Type: %type%)",
				"Don't translate the variables %name% and %type%."));
			name.Append(">\n");
			name.ReplaceFirst("%name%", enclosure->name);
			name.ReplaceFirst("%type%", typeDescription);

			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
			enclosure->text_start++;
			enclosure->text_end += strlen(name.String()) - 1;

			Insert(name.String(), name.Length(), true);
			fEnclosures->AddItem(enclosure);
		}
//			default:
//			{
//				PlainTextBodyComponent *body = dynamic_cast<PlainTextBodyComponent *>(container->GetComponent(i));
//				const char *text;
//				if (body && (text = body->Text()) != NULL)
//					Insert(text, strlen(text), false);
//			}
	}
	return count > 0;
}


bool
TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
{
	char line[522];
	int32 count = 0;

	for (int32 loop = 0; loop < data_len; loop++) {
		if (fView->fStopLoading)
			return false;

		if (fQuote && (!loop || (loop && data[loop - 1] == '\n'))) {
			strcpy(&line[count], QUOTE);
			count += strlen(QUOTE);
		}
		if (!fRaw && fIncoming && (loop < data_len - 7)) {
			size_t urlLength;
			BString url;
			uint8 type = CheckForURL(data + loop, urlLength, &url);

			if (type) {
				if (!Insert(line, count, false, isHeader))
					return false;
				count = 0;

				hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
				if (enclosure == NULL)
					return false;

				memset(enclosure, 0, sizeof(hyper_text));
				fView->GetSelection(&enclosure->text_start,
									&enclosure->text_end);
				enclosure->type = type;
				enclosure->name = strdup(url.String());
				if (enclosure->name == NULL) {
					free(enclosure);
					return false;
				}

				Insert(&data[loop], urlLength, true, isHeader);
				enclosure->text_end += urlLength;
				loop += urlLength - 1;

				fEnclosures->AddItem(enclosure);
				continue;
			}
		}
		if (!fRaw && fMime && data[loop] == '=') {
			if ((loop) && (loop < data_len - 1) && (data[loop + 1] == '\r'))
				loop += 2;
			else
				line[count++] = data[loop];
		} else if (data[loop] != '\r')
			line[count++] = data[loop];

		if (count > 511 || (count && loop == data_len - 1)) {
			if (!Insert(line, count, false, isHeader))
				return false;
			count = 0;
		}
	}
	return true;
}


bool
TTextView::Reader::Insert(const char *line, int32 count, bool isHyperLink,
	bool isHeader)
{
	if (!count)
		return true;

	BFont font(fView->Font());
	TextRunArray style(count / 8 + 8);

	if (fView->fColoredQuotes && !isHeader && !isHyperLink) {
		FillInQuoteTextRuns(fView, &fQuoteContext, line, count, font,
			&style.Array(), style.MaxEntries());
	} else {
		text_run_array &array = style.Array();
		array.count = 1;
		array.runs[0].offset = 0;
		if (isHeader) {
			array.runs[0].color = isHyperLink ? kHyperLinkColor : kHeaderColor;
			font.SetSize(font.Size() * 0.9);
		} else {
			array.runs[0].color = isHyperLink
				? kHyperLinkColor : kNormalTextColor;
		}
		array.runs[0].font = font;
	}

	if (!fView->Window()->Lock())
		return false;

	fView->Insert(fView->TextLength(), line, count, &style.Array());

	fView->Window()->Unlock();
	return true;
}


status_t
TTextView::Reader::Run(void *_this)
{
	Reader *reader = (Reader *)_this;
	TTextView *view = reader->fView;
	char *msg = NULL;
	off_t size = 0;
	int32 len = 0;

	if (!reader->Lock())
		return B_INTERRUPTED;

	BFile *file = dynamic_cast<BFile *>(reader->fMail->Data());
	if (file != NULL) {
		len = header_len(file);

		if (reader->fHeader)
			size = len;
		if (reader->fRaw || !reader->fMime)
			file->GetSize(&size);

		if (size != 0 && (msg = (char *)malloc(size)) == NULL)
			goto done;
		file->Seek(0, 0);

		if (msg)
			size = file->Read(msg, size);
	}

	// show the header?
	if (reader->fHeader && len) {
		// strip all headers except "From", "To", "Reply-To", "Subject", and "Date"
	 	if (reader->fStripHeader) {
		 	const char *header = msg;
		 	char *buffer = NULL;

			while (strncmp(header, "\r\n", 2)) {
				const char *eol = header;
				while ((eol = strstr(eol, "\r\n")) != NULL && isspace(eol[2]))
					eol += 2;
				if (eol == NULL)
					break;

				eol += 2;	// CR+LF belong to the line
				size_t length = eol - header;

		 		buffer = (char *)realloc(buffer, length + 1);
		 		if (buffer == NULL)
		 			goto done;

		 		memcpy(buffer, header, length);

				length = rfc2047_to_utf8(&buffer, &length, length);

		 		if (!strncasecmp(header, "Reply-To: ", 10)
		 			|| !strncasecmp(header, "To: ", 4)
		 			|| !strncasecmp(header, "From: ", 6)
		 			|| !strncasecmp(header, "Subject: ", 8)
		 			|| !strncasecmp(header, "Date: ", 6))
		 			reader->Process(buffer, length, true);

		 		header = eol;
	 		}
		 	free(buffer);
	 		reader->Process("\r\n", 2, true);
	 	}
	 	else if (!reader->Process(msg, len, true))
			goto done;
	}

	if (reader->fRaw) {
		if (!reader->Process((const char *)msg + len, size - len))
			goto done;
	} else {
		//reader->fFile->Seek(0, 0);
		//BEmailMessage *mail = new BEmailMessage(reader->fFile);
		BEmailMessage *mail = reader->fMail;

		// at first, insert the mail body
		BTextMailComponent *body = NULL;
		if (mail->BodyText() && !view->fStopLoading) {
			char *bodyText = const_cast<char *>(mail->BodyText());
			int32 bodyLength = strlen(bodyText);
			body = mail->Body();
			bool isHTML = false;

			BMimeType type;
			if (body->MIMEType(&type) == B_OK && type == "text/html") {
				// strip out HTML tags
				char *t = bodyText, *a, *end = bodyText + bodyLength;
				bodyText = (char *)malloc(bodyLength + 1);
				isHTML = true;

				// TODO: is it correct to assume that the text is in Latin-1?
				//		because if it isn't, the code below won't work correctly...

				for (a = bodyText; t < end; t++) {
					int32 c = *t;

					// compact spaces
					bool space = false;
					while (c && (c == ' ' || c == '\t')) {
						c = *(++t);
						space = true;
					}
					if (space) {
						c = ' ';
						t--;
					} else if (FilterHTMLTag(c, &t, end))	// the tag filter
						continue;

					Unicode2UTF8(c, &a);
				}

				*a = 0;
				bodyLength = strlen(bodyText);
				body = NULL;	// to add the HTML text as enclosure
			}
			if (!reader->Process(bodyText, bodyLength))
				goto done;

			if (isHTML)
				free(bodyText);
		}

		if (!reader->ParseMail(mail, body))
			goto done;

		//reader->fView->fMail = mail;
	}

	if (!view->fStopLoading && view->Window()->Lock()) {
		view->Select(0, 0);
		view->MakeSelectable(true);
		if (!reader->fIncoming)
			view->MakeEditable(true);

		view->Window()->Unlock();
	}

done:
	// restore the reading position if available
	view->Window()->PostMessage(M_READ_POS);

	reader->Unlock();

	delete reader;
	free(msg);

	return B_NO_ERROR;
}


status_t
TTextView::Reader::Unlock()
{
	return release_sem(fStopSem);
}


bool
TTextView::Reader::Lock()
{
	if (acquire_sem_etc(fStopSem, 1, B_TIMEOUT, 0) != B_NO_ERROR)
		return false;

	return true;
}


//====================================================================
//	#pragma mark -


TSavePanel::TSavePanel(hyper_text *enclosure, TTextView *view)
	: BFilePanel(B_SAVE_PANEL)
{
	fEnclosure = enclosure;
	fView = view;
	if (enclosure->name)
		SetSaveText(enclosure->name);
}


void
TSavePanel::SendMessage(const BMessenger * /* messenger */, BMessage *msg)
{
	const char	*name = NULL;
	BMessage	save(M_SAVE);
	entry_ref	ref;

	if ((!msg->FindRef("directory", &ref)) && (!msg->FindString("name", &name))) {
		save.AddPointer("enclosure", fEnclosure);
		save.AddString("name", name);
		save.AddRef("directory", &ref);
		fView->Window()->PostMessage(&save, fView);
	}
}


void
TSavePanel::SetEnclosure(hyper_text *enclosure)
{
	fEnclosure = enclosure;
	if (enclosure->name)
		SetSaveText(enclosure->name);
	else
		SetSaveText("");

	if (!IsShowing())
		Show();
	Window()->Activate();
}


//--------------------------------------------------------------------
//	#pragma mark -


void
TTextView::InsertText(const char *insertText, int32 length, int32 offset,
	const text_run_array *runs)
{
	ContentChanged();

	// Undo function

	int32 cursorPos, dummy;
	GetSelection(&cursorPos, &dummy);

	if (fInputMethodUndoState.active) {
		// IMアクティブ時は、一旦別のバッファへ記憶
		fInputMethodUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
		fInputMethodUndoState.replace = false;
	} else {
		if (fUndoState.replaced) {
			fUndoBuffer.AddUndo(insertText, length, offset, K_REPLACED, cursorPos);
		} else {
			if (length == 1 && insertText[0] == 0x0a)
				fUndoBuffer.MakeNewUndoItem();

			fUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);

			if (length == 1 && insertText[0] == 0x0a)
				fUndoBuffer.MakeNewUndoItem();
		}
	}

	fUndoState.replaced = false;
	fUndoState.deleted = false;

	struct text_runs : text_run_array { text_run _runs[1]; } style;
	if (runs == NULL && IsEditable()) {
		style.count = 1;
		style.runs[0].offset = 0;
		style.runs[0].font = fFont;
		style.runs[0].color = kNormalTextColor;
		runs = &style;
	}

	BTextView::InsertText(insertText, length, offset, runs);

	if (fSpellCheck && IsEditable())
	{
		UpdateSpellMarks(offset, length);

		rgb_color color;
		GetFontAndColor(offset - 1, NULL, &color);
		const char *text = Text();

		if (length > 1
			|| isalpha(text[offset + 1])
			|| (!isalpha(text[offset]) && text[offset] != '\'')
			|| (color.red == kSpellTextColor.red
				&& color.green == kSpellTextColor.green
				&& color.blue == kSpellTextColor.blue))
		{
			int32 start, end;
			FindSpellBoundry(length, offset, &start, &end);

			DSPELL(printf("Offset %ld, start %ld, end %ld\n", offset, start, end));
			DSPELL(printf("\t\"%10.10s...\"\n", text + start));

			CheckSpelling(start, end);
		}
	}
}


void
TTextView::DeleteText(int32 start, int32 finish)
{
	ContentChanged();

	// Undo function
	int32 cursorPos, dummy;
	GetSelection(&cursorPos, &dummy);
	if (fInputMethodUndoState.active) {
		if (fInputMethodUndoState.replace) {
			fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
			fInputMethodUndoState.replace = false;
		} else {
			fInputMethodUndoBuffer.AddUndo(&Text()[start], finish - start, start,
				K_DELETED, cursorPos);
		}
	} else
		fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);

	fUndoState.deleted = true;
	fUndoState.replaced = true;

	BTextView::DeleteText(start, finish);
	if (fSpellCheck && IsEditable()) {
		UpdateSpellMarks(start, start - finish);

		int32 s, e;
		FindSpellBoundry(1, start, &s, &e);
		CheckSpelling(s, e);
	}
}


void
TTextView::ContentChanged(void)
{
	BLooper *looper = Looper();
	if (looper == NULL)
		return;

	BMessage msg(FIELD_CHANGED);
	msg.AddInt32("bitmask", FIELD_BODY);
	msg.AddPointer("source", this);
	looper->PostMessage(&msg);
}


void
TTextView::CheckSpelling(int32 start, int32 end, int32 flags)
{
	const char 	*text = Text();
	const char 	*next, *endPtr, *word = NULL;
	int32 		wordLength = 0, wordOffset;
	int32		nextHighlight = start;
	BString 	testWord;
	bool		isCap = false;
	bool		isAlpha;
	bool		isApost;

	for (next = text + start, endPtr = text + end; next <= endPtr; next++) {
		//printf("next=%c\n", *next);
		// ToDo: this has to be refined to other languages...
		// Alpha signifies the start of a word
		isAlpha = isalpha(*next);
		isApost = (*next == '\'');
		if (!word && isAlpha) {
			//printf("Found word start\n");
			word = next;
			wordLength++;
			isCap = isupper(*word);
		} else if (word && (isAlpha || isApost) && !(isApost && !isalpha(next[1]))
					&& !(isCap && isApost && (next[1] == 's'))) {
			// Word continues check
			wordLength++;
			//printf("Word continues...\n");
		} else if (word) {
			// End of word reached

			//printf("Word End\n");
			// Don't check single characters
			if (wordLength > 1) {
				bool isUpper = true;

				// Look for all uppercase
				for (int32 i = 0; i < wordLength; i++) {
					if (word[i] == '\'')
						break;

					if (islower(word[i])) {
						isUpper = false;
						break;
					}
				}

				// Don't check all uppercase words
				if (!isUpper) {
					bool foundMatch = false;
					wordOffset = word - text;
					testWord.SetTo(word, wordLength);

					testWord = testWord.ToLower();
					DSPELL(printf("Testing: \"%s\"\n", testWord.String()));

					int32 key = -1;
					if (gDictCount)
						key = gExactWords[0]->GetKey(testWord.String());

					// Search all dictionaries
					for (int32 i = 0; i < gDictCount; i++) {
						if (gExactWords[i]->Lookup(key) >= 0) {
							foundMatch = true;
							break;
						}
					}

					if (!foundMatch) {
						if (flags & S_CLEAR_ERRORS)
							RemoveSpellMark(nextHighlight, wordOffset);

						if (flags & S_SHOW_ERRORS)
							AddSpellMark(wordOffset, wordOffset + wordLength);
					} else if (flags & S_CLEAR_ERRORS)
						RemoveSpellMark(nextHighlight, wordOffset + wordLength);

					nextHighlight = wordOffset + wordLength;
				}
			}
			// Reset state to looking for word
			word = NULL;
			wordLength = 0;
		}
	}

	if (nextHighlight <= end
		&& (flags & S_CLEAR_ERRORS) != 0
		&& nextHighlight < TextLength())
		SetFontAndColor(nextHighlight, end, NULL, B_FONT_ALL, &kNormalTextColor);
}


void
TTextView::FindSpellBoundry(int32 length, int32 offset, int32 *_start, int32 *_end)
{
	int32 start, end, textLength;
	const char *text = Text();
	textLength = TextLength();

	for (start = offset - 1; start >= 0
		&& (isalpha(text[start]) || text[start] == '\''); start--) {}

	start++;

	for (end = offset + length; end < textLength
		&& (isalpha(text[end]) || text[end] == '\''); end++) {}

	*_start = start;
	*_end = end;
}


TTextView::spell_mark *
TTextView::FindSpellMark(int32 start, int32 end, spell_mark **_previousMark)
{
	spell_mark *lastMark = NULL;

	for (spell_mark *spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
		if (spellMark->start < end && spellMark->end > start) {
			if (_previousMark)
				*_previousMark = lastMark;
			return spellMark;
		}

		lastMark = spellMark;
	}
	return NULL;
}


void
TTextView::UpdateSpellMarks(int32 offset, int32 length)
{
	DSPELL(printf("UpdateSpellMarks: offset = %ld, length = %ld\n", offset, length));

	spell_mark *spellMark;
	for (spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
		DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));

		if (spellMark->end < offset)
			continue;

		if (spellMark->start > offset)
			spellMark->start += length;

		spellMark->end += length;

		DSPELL(printf("\t-> reset: %ld - %ld\n", spellMark->start, spellMark->end));
	}
}


status_t
TTextView::AddSpellMark(int32 start, int32 end)
{
	DSPELL(printf("AddSpellMark: start = %ld, end = %ld\n", start, end));

	// check if there is already a mark for this passage
	spell_mark *spellMark = FindSpellMark(start, end);
	if (spellMark) {
		if (spellMark->start == start && spellMark->end == end) {
			DSPELL(printf("\tfound one\n"));
			return B_OK;
		}

		DSPELL(printf("\tremove old one\n"));
		RemoveSpellMark(start, end);
	}

	spellMark = (spell_mark *)malloc(sizeof(spell_mark));
	if (spellMark == NULL)
		return B_NO_MEMORY;

	spellMark->start = start;
	spellMark->end = end;
	spellMark->style = RunArray(start, end);

	// set the spell marks appearance
	BFont font(fFont);
	font.SetFace(B_BOLD_FACE | B_ITALIC_FACE);
	SetFontAndColor(start, end, &font, B_FONT_ALL, &kSpellTextColor);

	// add it to the queue
	spellMark->next = fFirstSpellMark;
	fFirstSpellMark = spellMark;

	return B_OK;
}


bool
TTextView::RemoveSpellMark(int32 start, int32 end)
{
	DSPELL(printf("RemoveSpellMark: start = %ld, end = %ld\n", start, end));

	// find spell mark
	spell_mark *lastMark = NULL;
	spell_mark *spellMark = FindSpellMark(start, end, &lastMark);
	if (spellMark == NULL) {
		DSPELL(printf("\tnot found!\n"));
		return false;
	}

	DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));

	// dequeue the spell mark
	if (lastMark)
		lastMark->next = spellMark->next;
	else
		fFirstSpellMark = spellMark->next;

	if (spellMark->start < start)
		start = spellMark->start;
	if (spellMark->end > end)
		end = spellMark->end;

	// reset old text run array
	SetRunArray(start, end, spellMark->style);

	free(spellMark->style);
	free(spellMark);

	return true;
}


void
TTextView::RemoveSpellMarks()
{
	spell_mark *spellMark, *nextMark;

	for (spellMark = fFirstSpellMark; spellMark; spellMark = nextMark) {
		nextMark = spellMark->next;

		// reset old text run array
		SetRunArray(spellMark->start, spellMark->end, spellMark->style);

		free(spellMark->style);
		free(spellMark);
	}

	fFirstSpellMark = NULL;
}


void
TTextView::EnableSpellCheck(bool enable)
{
	if (fSpellCheck == enable)
		return;

	fSpellCheck = enable;
	int32 textLength = TextLength();
	if (fSpellCheck) {
		// work-around for a bug in the BTextView class
		// which causes lots of flicker
		int32 start, end;
		GetSelection(&start, &end);
		if (start != end)
			Select(start, start);

		CheckSpelling(0, textLength);

		if (start != end)
			Select(start, end);
	}
	else
		RemoveSpellMarks();
}


void
TTextView::WindowActivated(bool flag)
{
	if (!flag) {
		// WindowActivated(false) は、IM も Inactive になり、そのまま確定される。
		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
		// OpenBeOSで修正されることを願って暫定処置としている。
		fInputMethodUndoState.active = false;
		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら（確定）正規のバッファへ追加
		if (fInputMethodUndoBuffer.CountItems() > 0) {
			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
			if (item->History == K_INSERTED) {
				fUndoBuffer.MakeNewUndoItem();
				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset,
					item->History, item->CursorPos);
				fUndoBuffer.MakeNewUndoItem();
			}
			fInputMethodUndoBuffer.MakeEmpty();
		}
	}
	BTextView::WindowActivated(flag);
}


void
TTextView::AddQuote(int32 start, int32 finish)
{
	BRect rect = Bounds();

	int32 lineStart;
	GoToLine(CurrentLine());
	GetSelection(&lineStart, &lineStart);
	lineStart = LineStart(lineStart);

	// make sure that we're changing the whole last line, too
	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
	{
		const char *text = Text();
		while (text[lineEnd] && text[lineEnd] != '\n')
			lineEnd++;
	}
	Select(lineStart, lineEnd);

	int32 textLength = lineEnd - lineStart;
	char *text = (char *)malloc(textLength + 1);
	if (text == NULL)
		return;

	GetText(lineStart, textLength, text);

	int32 quoteLength = strlen(QUOTE);
	int32 targetLength = 0;
	char *target = NULL;
	int32 lastLine = 0;

	for (int32 index = 0; index < textLength; index++) {
		if (text[index] == '\n' || index == textLength - 1) {
			// add quote to this line
			int32 lineLength = index - lastLine + 1;

			target = (char *)realloc(target, targetLength + lineLength + quoteLength);
			if (target == NULL) {
				// free the old buffer?
				free(text);
				return;
			}

			// copy the quote sign
			memcpy(&target[targetLength], QUOTE, quoteLength);
			targetLength += quoteLength;

			// copy the rest of the line
			memcpy(&target[targetLength], &text[lastLine], lineLength);
			targetLength += lineLength;

			lastLine = index + 1;
		}
	}

	// replace with quoted text
	free(text);
	Delete();

	if (fColoredQuotes) {
		const BFont *font = Font();
		TextRunArray style(targetLength / 8 + 8);

		FillInQuoteTextRuns(NULL, NULL, target, targetLength, font,
			&style.Array(), style.MaxEntries());
		Insert(target, targetLength, &style.Array());
	} else
		Insert(target, targetLength);

	free(target);

	// redo the old selection (compute the new start if necessary)
	Select(start + quoteLength, finish + (targetLength - textLength));

	ScrollTo(rect.LeftTop());
}


void
TTextView::RemoveQuote(int32 start, int32 finish)
{
	BRect rect = Bounds();

	GoToLine(CurrentLine());
	int32 lineStart;
	GetSelection(&lineStart, &lineStart);
	lineStart = LineStart(lineStart);

	// make sure that we're changing the whole last line, too
	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
	const char *text = Text();
	while (text[lineEnd] && text[lineEnd] != '\n')
		lineEnd++;

	Select(lineStart, lineEnd);

	int32 length = lineEnd - lineStart;
	char *target = (char *)malloc(length + 1);
	if (target == NULL)
		return;

	int32 quoteLength = strlen(QUOTE);
	int32 removed = 0;
	text += lineStart;

	for (int32 index = 0; index < length;) {
		// find out the length of the current line
		int32 lineLength = 0;
		while (index + lineLength < length && text[lineLength] != '\n')
			lineLength++;

		// include the newline to be part of this line
		if (text[lineLength] == '\n' && index + lineLength + 1 < length)
			lineLength++;

		if (!strncmp(text, QUOTE, quoteLength)) {
			// remove quote
			length -= quoteLength;
			removed += quoteLength;

			lineLength -= quoteLength;
			text += quoteLength;
		}

		if (lineLength == 0) {
			target[index] = '\0';
			break;
		}

		memcpy(&target[index], text, lineLength);

		text += lineLength;
		index += lineLength;
	}

	if (removed) {
		Delete();

		if (fColoredQuotes) {
			const BFont *font = Font();
			TextRunArray style(length / 8 + 8);

			FillInQuoteTextRuns(NULL, NULL, target, length, font,
				&style.Array(), style.MaxEntries());
			Insert(target, length, &style.Array());
		} else
			Insert(target, length);

		// redo old selection
		bool noSelection = start == finish;

		if (start > lineStart + quoteLength)
			start -= quoteLength;
		else
			start = lineStart;

		if (noSelection)
			finish = start;
		else
			finish -= removed;
	}

	free(target);

	Select(start, finish);
	ScrollTo(rect.LeftTop());
}


int32
TTextView::LineStart(int32 offset)
{
	if (offset <= 0)
		return 0;

	while (offset > 0) {
		offset = PreviousByte(offset);
		if (ByteAt(offset) == B_ENTER)
			return offset + 1;
	}

	return offset;
}


int32
TTextView::PreviousByte(int32 offset) const
{
	if (offset <= 0)
		return 0;

	int32 count = 6;

	for (--offset; offset > 0 && count; --offset, --count) {
		if ((ByteAt(offset) & 0xC0) != 0x80)
			break;
	}

	return count ? offset : 0;
}


void
TTextView::Undo(BClipboard */*clipboard*/)
{
	if (fInputMethodUndoState.active)
		return;

	int32 length, offset, cursorPos;
	undo_type history;
	char *text;
	status_t status;

	status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
	if (status == B_OK) {
		fUndoBuffer.Off();

		switch (history) {
			case K_INSERTED:
				BTextView::Delete(offset, offset + length);
				Select(offset, offset);
				break;

			case K_DELETED:
				BTextView::Insert(offset, text, length);
				Select(offset, offset + length);
				break;

			case K_REPLACED:
				BTextView::Delete(offset, offset + length);
				status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
				if (status == B_OK && history == K_DELETED) {
					BTextView::Insert(offset, text, length);
					Select(offset, offset + length);
				} else {
					::beep();
					BAlert* alert = new BAlert("",
						B_TRANSLATE("Inconsistency occurred in the undo/redo "
							"buffer."),	B_TRANSLATE("OK"));
					alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
					alert->Go();
				}
				break;
		}
		ScrollToSelection();
		ContentChanged();
		fUndoBuffer.On();
	}
}


void
TTextView::Redo()
{
	if (fInputMethodUndoState.active)
		return;

	int32 length, offset, cursorPos;
	undo_type history;
	char *text;
	status_t status;
	bool replaced;

	status = fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
	if (status == B_OK) {
		fUndoBuffer.Off();

		switch (history) {
			case K_INSERTED:
				BTextView::Insert(offset, text, length);
				Select(offset, offset + length);
				break;

			case K_DELETED:
				BTextView::Delete(offset, offset + length);
				if (replaced) {
					fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
					BTextView::Insert(offset, text, length);
				}
				Select(offset, offset + length);
				break;

			case K_REPLACED:
				::beep();
				BAlert* alert = new BAlert("",
					B_TRANSLATE("Inconsistency occurred in the undo/redo "
						"buffer."),	B_TRANSLATE("OK"));
				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
				alert->Go();
				break;
		}
		ScrollToSelection();
		ContentChanged();
		fUndoBuffer.On();
	}
}
