/*
 * Copyright 2010-2017, Haiku, Inc. All Rights Reserved.
 * Copyright 2008-2009, Pier Luigi Fiorini. All Rights Reserved.
 * Copyright 2004-2008, Michael Davidson. All Rights Reserved.
 * Copyright 2004-2007, Mikael Eiman. All Rights Reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Michael Davidson, slaad@bong.com.au
 *		Mikael Eiman, mikael@eiman.tv
 *		Pier Luigi Fiorini, pierluigi.fiorini@gmail.com
 *		Stephan Aßmus <superstippi@gmx.de>
 *		Adrien Destugues <pulkomandy@pulkomandy.ath.cx>
 *		Brian Hill, supernova@tycho.email
 */
 
 
#include "NotificationView.h"
 
 
#include <Bitmap.h>
#include <ControlLook.h>
#include <GroupLayout.h>
#include <LayoutUtils.h>
#include <MessageRunner.h>
#include <Messenger.h>
#include <Notification.h>
#include <Path.h>
#include <PropertyInfo.h>
#include <Roster.h>
#include <StatusBar.h>
 
#include <Notifications.h>
 
#include "AppGroupView.h"
#include "NotificationWindow.h"
 
 
const int kIconStripeWidth			= 32;
const float kCloseSize				= 6;
const float kEdgePadding			= 2;
const float kSmallPadding			= 2;
 
property_info message_prop_list[] = {
	{ "type", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
		{B_DIRECT_SPECIFIER, 0}, "get the notification type"},
	{ "app", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
		{B_DIRECT_SPECIFIER, 0}, "get notification's app"},
	{ "title", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
		{B_DIRECT_SPECIFIER, 0}, "get notification's title"},
	{ "content", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
		{B_DIRECT_SPECIFIER, 0}, "get notification's contents"},
	{ "icon", {B_GET_PROPERTY, 0},
		{B_DIRECT_SPECIFIER, 0}, "get icon as an archived bitmap"},
	{ "progress", {B_GET_PROPERTY, B_SET_PROPERTY, 0},
		{B_DIRECT_SPECIFIER, 0}, "get the progress (between 0.0 and 1.0)"},
 
	{ 0 }
};
 
 
NotificationView::NotificationView(BNotification* notification, bigtime_t timeout,
	float iconSize, bool disableTimeout)
	:
	BView("NotificationView", B_WILL_DRAW),
	fNotification(notification),
	fTimeout(timeout),
	fIconSize(iconSize),
	fDisableTimeout(disableTimeout),
	fRunner(NULL),
	fBitmap(NULL),
	fCloseClicked(false),
	fPreviewModeOn(false)
{
	if (fNotification->Icon() != NULL)
		fBitmap = new BBitmap(fNotification->Icon());
 
	BGroupLayout* layout = new BGroupLayout(B_VERTICAL);
	SetLayout(layout);
 
	SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
	SetLowColor(ui_color(B_PANEL_BACKGROUND_COLOR));
 
	switch (fNotification->Type()) {
		case B_IMPORTANT_NOTIFICATION:
			fStripeColor = ui_color(B_CONTROL_HIGHLIGHT_COLOR);
			break;
		case B_ERROR_NOTIFICATION:
			fStripeColor = ui_color(B_FAILURE_COLOR);
			break;
		case B_PROGRESS_NOTIFICATION:
		{
			BStatusBar* progress = new BStatusBar("progress");
			progress->SetBarHeight(12.0f);
			progress->SetMaxValue(1.0f);
			progress->Update(fNotification->Progress());
 
			BString label = "";
			label << (int)(fNotification->Progress() * 100) << " %";
			progress->SetTrailingText(label);
 
			layout->AddView(progress);
		}
		// fall through.
		case B_INFORMATION_NOTIFICATION:
			fStripeColor = tint_color(ui_color(B_PANEL_BACKGROUND_COLOR),
				B_DARKEN_1_TINT);
			break;
	}
}
 
 
NotificationView::~NotificationView()
{
	delete fRunner;
	delete fBitmap;
	delete fNotification;
 
	LineInfoList::iterator lIt;
	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
		delete (*lIt);
}
 
 
void
NotificationView::AttachedToWindow()
{
	SetText();
	
	if (!fDisableTimeout) {
		BMessage msg(kRemoveView);
		msg.AddPointer("view", this);
		fRunner = new BMessageRunner(BMessenger(Parent()), &msg, fTimeout, 1);
	}
}
 
 
void
NotificationView::MessageReceived(BMessage* msg)
{
	switch (msg->what) {
		case B_GET_PROPERTY:
		{
			BMessage specifier;
			const char* property;
			BMessage reply(B_REPLY);
			bool msgOkay = true;
 
			if (msg->FindMessage("specifiers", 0, &specifier) != B_OK)
				msgOkay = false;
			if (specifier.FindString("property", &property) != B_OK)
				msgOkay = false;
 
			if (msgOkay) {
				if (strcmp(property, "type") == 0)
					reply.AddInt32("result", fNotification->Type());
 
				if (strcmp(property, "group") == 0)
					reply.AddString("result", fNotification->Group());
 
				if (strcmp(property, "title") == 0)
					reply.AddString("result", fNotification->Title());
 
				if (strcmp(property, "content") == 0)
					reply.AddString("result", fNotification->Content());
 
				if (strcmp(property, "progress") == 0)
					reply.AddFloat("result", fNotification->Progress());
 
				if ((strcmp(property, "icon") == 0) && fBitmap) {
					BMessage archive;
					if (fBitmap->Archive(&archive) == B_OK)
						reply.AddMessage("result", &archive);
				}
 
				reply.AddInt32("error", B_OK);
			} else {
				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
				reply.AddInt32("error", B_ERROR);
			}
 
			msg->SendReply(&reply);
			break;
		}
		case B_SET_PROPERTY:
		{
			BMessage specifier;
			const char* property;
			BMessage reply(B_REPLY);
			bool msgOkay = true;
 
			if (msg->FindMessage("specifiers", 0, &specifier) != B_OK)
				msgOkay = false;
			if (specifier.FindString("property", &property) != B_OK)
				msgOkay = false;
 
			if (msgOkay) {
				const char* value = NULL;
 
				if (strcmp(property, "group") == 0)
					if (msg->FindString("data", &value) == B_OK)
						fNotification->SetGroup(value);
 
				if (strcmp(property, "title") == 0)
					if (msg->FindString("data", &value) == B_OK)
						fNotification->SetTitle(value);
 
				if (strcmp(property, "content") == 0)
					if (msg->FindString("data", &value) == B_OK)
						fNotification->SetContent(value);
 
				if (strcmp(property, "icon") == 0) {
					BMessage archive;
					if (msg->FindMessage("data", &archive) == B_OK) {
						delete fBitmap;
						fBitmap = new BBitmap(&archive);
					}
				}
 
				SetText();
				Invalidate();
 
				reply.AddInt32("error", B_OK);
			} else {
				reply.what = B_MESSAGE_NOT_UNDERSTOOD;
				reply.AddInt32("error", B_ERROR);
			}
 
			msg->SendReply(&reply);
			break;
		}
		default:
			BView::MessageReceived(msg);
	}
}
 
 
void
NotificationView::Draw(BRect updateRect)
{
	BRect progRect;
 
	SetDrawingMode(B_OP_ALPHA);
	SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY);
 
	BRect stripeRect = Bounds();
	stripeRect.right = kIconStripeWidth;
	SetHighColor(tint_color(ui_color(B_PANEL_BACKGROUND_COLOR),
		B_DARKEN_1_TINT));
	FillRect(stripeRect);
 
	SetHighColor(fStripeColor);
	stripeRect.right = 2;
	FillRect(stripeRect);
 
	SetHighColor(ui_color(B_PANEL_TEXT_COLOR));
	// Rectangle for icon and overlay icon
	BRect iconRect(0, 0, 0, 0);
 
	// Draw icon
	if (fBitmap) {
		float ix = 18;
		float iy = (Bounds().Height() - fIconSize) / 4.0;
			// Icon is vertically centered in view
 
		if (fNotification->Type() == B_PROGRESS_NOTIFICATION) {
			// Move icon up by half progress bar height if it's present
			iy -= (progRect.Height() + kEdgePadding);
		}
 
		iconRect.Set(ix, iy, ix + fIconSize - 1.0, iy + fIconSize - 1.0);
		DrawBitmapAsync(fBitmap, fBitmap->Bounds(), iconRect);
	}
 
	// Draw content
	LineInfoList::iterator lIt;
	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++) {
		LineInfo *l = (*lIt);
 
		SetFont(&l->font);
		// Truncate the string. We have already line-wrapped the text but if
		// there is a very long 'word' we can only truncate it.
		BString text(l->text);
		TruncateString(&text, B_TRUNCATE_END,
			Bounds().Width() - l->location.x);
		DrawString(text.String(), text.Length(), l->location);
	}
 
	AppGroupView* groupView = dynamic_cast<AppGroupView*>(Parent());
	if (groupView != NULL && groupView->ChildrenCount() > 1)
		_DrawCloseButton(updateRect);
 
	SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
	BPoint left(Bounds().left, Bounds().top);
	BPoint right(Bounds().right, Bounds().top);
	StrokeLine(left, right);
 
	Sync();
}
 
 
void
NotificationView::_DrawCloseButton(const BRect& updateRect)
{
	PushState();
	BRect closeRect = Bounds();
 
	closeRect.InsetBy(3 * kEdgePadding, 3 * kEdgePadding);
	closeRect.left = closeRect.right - kCloseSize;
	closeRect.bottom = closeRect.top + kCloseSize;
 
	rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
	float tint = B_DARKEN_2_TINT;
 
	if (fCloseClicked) {
		BRect buttonRect(closeRect.InsetByCopy(-4, -4));
		be_control_look->DrawButtonFrame(this, buttonRect, updateRect,
			base, base,
			BControlLook::B_ACTIVATED | BControlLook::B_BLEND_FRAME);
		be_control_look->DrawButtonBackground(this, buttonRect, updateRect,
			base, BControlLook::B_ACTIVATED);
		tint *= 1.2;
		closeRect.OffsetBy(1, 1);
	}
 
	base = tint_color(base, tint);
	SetHighColor(base);
	SetPenSize(2);
	StrokeLine(closeRect.LeftTop(), closeRect.RightBottom());
	StrokeLine(closeRect.LeftBottom(), closeRect.RightTop());
	PopState();
}
 
 
void
NotificationView::MouseDown(BPoint point)
{
	// Preview Mode ignores any mouse clicks
	if (fPreviewModeOn)
		return;
 
	int32 buttons;
	Window()->CurrentMessage()->FindInt32("buttons", &buttons);
 
	switch (buttons) {
		case B_PRIMARY_MOUSE_BUTTON:
		{
			BRect closeRect = Bounds().InsetByCopy(2,2);
			closeRect.left = closeRect.right - kCloseSize;
			closeRect.bottom = closeRect.top + kCloseSize;
 
			if (!closeRect.Contains(point)) {
				entry_ref launchRef;
				BString launchString;
				BMessage argMsg(B_ARGV_RECEIVED);
				BMessage refMsg(B_REFS_RECEIVED);
				entry_ref appRef;
				bool useArgv = false;
				BList messages;
				entry_ref ref;
 
				if (fNotification->OnClickApp() != NULL
					&& be_roster->FindApp(fNotification->OnClickApp(), &appRef)
				   		== B_OK) {
					useArgv = true;
				}
 
				if (fNotification->OnClickFile() != NULL
					&& be_roster->FindApp(
							(entry_ref*)fNotification->OnClickFile(), &appRef)
				   		== B_OK) {
					useArgv = true;
				}
 
				for (int32 i = 0; i < fNotification->CountOnClickRefs(); i++)
					refMsg.AddRef("refs", fNotification->OnClickRefAt(i));
				messages.AddItem((void*)&refMsg);
 
				if (useArgv) {
					int32 argc = fNotification->CountOnClickArgs() + 1;
					BString arg;
 
					BPath p(&appRef);
					argMsg.AddString("argv", p.Path());
 
					argMsg.AddInt32("argc", argc);
 
					for (int32 i = 0; i < argc - 1; i++) {
						argMsg.AddString("argv",
							fNotification->OnClickArgAt(i));
					}
 
					messages.AddItem((void*)&argMsg);
				}
 
				if (fNotification->OnClickApp() != NULL)
					be_roster->Launch(fNotification->OnClickApp(), &messages);
				else
					be_roster->Launch(fNotification->OnClickFile(), &messages);
			} else {
				fCloseClicked = true;
			}
 
			// Remove the info view after a click
			BMessage remove_msg(kRemoveView);
			remove_msg.AddPointer("view", this);
 
			BMessenger msgr(Parent());
			msgr.SendMessage(&remove_msg);
			break;
		}
	}
}
 
 
BHandler*
NotificationView::ResolveSpecifier(BMessage* msg, int32 index, BMessage* spec,
	int32 form, const char* prop)
{
	BPropertyInfo prop_info(message_prop_list);
	if (prop_info.FindMatch(msg, index, spec, form, prop) >= 0) {
		msg->PopSpecifier();
		return this;
	}
 
	return BView::ResolveSpecifier(msg, index, spec, form, prop);
}
 
 
status_t
NotificationView::GetSupportedSuites(BMessage* msg)
{
	msg->AddString("suites", "suite/x-vnd.Haiku-notification_server");
	BPropertyInfo prop_info(message_prop_list);
	msg->AddFlat("messages", &prop_info);
	return BView::GetSupportedSuites(msg);
}
 
 
void
NotificationView::SetText(float newMaxWidth)
{
	if (newMaxWidth < 0 && Parent())
		newMaxWidth = Parent()->Bounds().IntegerWidth();
	if (newMaxWidth <= 0)
		newMaxWidth = kDefaultWidth;
 
	// Delete old lines
	LineInfoList::iterator lIt;
	for (lIt = fLines.begin(); lIt != fLines.end(); lIt++)
		delete (*lIt);
	fLines.clear();
 
	float iconRight = kIconStripeWidth;
	if (fBitmap != NULL)
		iconRight += fIconSize;
	else
		iconRight += 32;
 
	font_height fh;
	be_bold_font->GetHeight(&fh);
	float fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
		+ ceilf(fh.ascent);
	float y = fontHeight + kEdgePadding * 2;
 
	// Title
	LineInfo* titleLine = new LineInfo;
	titleLine->text = fNotification->Title();
	titleLine->font = *be_bold_font;
 
	titleLine->location = BPoint(iconRight + kEdgePadding, y);
 
	fLines.push_front(titleLine);
	y += fontHeight;
 
	// Rest of text is rendered with be_plain_font.
	be_plain_font->GetHeight(&fh);
	fontHeight = ceilf(fh.leading) + ceilf(fh.descent)
		+ ceilf(fh.ascent);
 
	// Split text into chunks between certain characters and compose the lines.
	const char kSeparatorCharacters[] = " \n-\\";
	BString textBuffer = fNotification->Content();
	textBuffer.ReplaceAll("\t", "    ");
	const char* chunkStart = textBuffer.String();
	float maxWidth = newMaxWidth - kEdgePadding - iconRight;
	LineInfo* line = NULL;
	ssize_t length = textBuffer.Length();
	while (chunkStart - textBuffer.String() < length) {
		size_t chunkLength = strcspn(chunkStart, kSeparatorCharacters) + 1;
 
		// Start a new line if we didn't start one before
		BString tempText;
		if (line != NULL)
			tempText.SetTo(line->text);
		tempText.Append(chunkStart, chunkLength);
 
		if (line == NULL || chunkStart[0] == '\n'
			|| StringWidth(tempText) > maxWidth) {
			line = new LineInfo;
			line->font = *be_plain_font;
			line->location = BPoint(iconRight + kEdgePadding, y);
 
			fLines.push_front(line);
			y += fontHeight;
 
			// Skip the eventual new-line character at the beginning of this chunk
			if (chunkStart[0] == '\n') {
				chunkStart++;
				chunkLength--;
			}
 
			// Skip more new-line characters and move the line further down
			while (chunkStart[0] == '\n') {
				chunkStart++;
				chunkLength--;
				line->location.y += fontHeight;
				y += fontHeight;
			}
 
			// Strip space at beginning of a new line
			while (chunkStart[0] == ' ') {
				chunkLength--;
				chunkStart++;
			}
		}
 
		if (chunkStart[0] == '\0')
			break;
 
		// Append the chunk to the current line, which was either a new
		// line or the one from the previous iteration
		line->text.Append(chunkStart, chunkLength);
 
		chunkStart += chunkLength;
	}
 
	fHeight = y + (kEdgePadding * 2);
 
	// Make sure icon fits
	if (fBitmap != NULL) {
		float minHeight = fBitmap->Bounds().Height() + 2 * kEdgePadding;
 
		if (fHeight < minHeight)
			fHeight = minHeight;
	}
 
	// Make sure the progress bar is below the text, and the window is big
	// enough.
	static_cast<BGroupLayout*>(GetLayout())->SetInsets(kIconStripeWidth + 8,
		fHeight, 8, 8);
 
	_CalculateSize();
}
 
 
void
NotificationView::SetPreviewModeOn(bool enabled)
{
	fPreviewModeOn = enabled;
}
 
 
const char*
NotificationView::MessageID() const
{
	return fNotification->MessageID();
}
 
 
void
NotificationView::_CalculateSize()
{
	float height = fHeight;
 
	if (fNotification->Type() == B_PROGRESS_NOTIFICATION) {
		font_height fh;
		be_plain_font->GetHeight(&fh);
		float fontHeight = fh.ascent + fh.descent + fh.leading;
		height += 9 + (kSmallPadding * 2) + (kEdgePadding * 1)
			+ fontHeight * 2;
	}
 
	SetExplicitMinSize(BSize(0, height));
	SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, height));
}

V730 Not all members of a class are initialized inside the constructor. Consider inspecting: fHeight.