/*
 * Copyright 2007-2010, Haiku. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Stephan Aßmus 	<superstippi@gmx.de>
 *		Fredrik Modéen	<fredrik@modeen.se>
 */
 
 
#include "PlaylistWindow.h"
 
#include <stdio.h>
 
#include <Alert.h>
#include <Application.h>
#include <Autolock.h>
#include <Box.h>
#include <Button.h>
#include <Catalog.h>
#include <Entry.h>
#include <File.h>
#include <FilePanel.h>
#include <Locale.h>
#include <Menu.h>
#include <MenuBar.h>
#include <MenuItem.h>
#include <NodeInfo.h>
#include <Path.h>
#include <Roster.h>
#include <ScrollBar.h>
#include <ScrollView.h>
#include <String.h>
#include <StringView.h>
 
#include "CommandStack.h"
#include "DurationToString.h"
#include "MainApp.h"
#include "PlaylistListView.h"
#include "RWLocker.h"
 
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "MediaPlayer-PlaylistWindow"
 
 
// TODO:
// Maintaining a playlist file on disk is a bit tricky. The playlist ref should
// be discarded when the user
// * loads a new playlist via Open,
// * loads a new playlist via dropping it on the MainWindow,
// * loads a new playlist via dropping it into the ListView while replacing
//   the contents,
// * replacing the contents by other stuff.
 
 
static void
display_save_alert(const char* message)
{
	BAlert* alert = new BAlert(B_TRANSLATE("Save error"), message, 
		B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT);
	alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
	alert->Go(NULL);
}
 
 
static void
display_save_alert(status_t error)
{
	BString errorMessage(B_TRANSLATE("Saving the playlist failed.\n\nError: "));
	errorMessage << strerror(error);
	display_save_alert(errorMessage.String());
}
 
 
// #pragma mark -
 
 
PlaylistWindow::PlaylistWindow(BRect frame, Playlist* playlist,
		Controller* controller)
	:
	BWindow(frame, B_TRANSLATE("Playlist"), B_DOCUMENT_WINDOW_LOOK,
		B_NORMAL_WINDOW_FEEL, B_ASYNCHRONOUS_CONTROLS),
	fPlaylist(playlist),
	fLocker(new RWLocker("command stack lock")),
	fCommandStack(new CommandStack(fLocker)),
	fCommandStackListener(this),
	fDurationListener(new DurationListener(*this))
{
	frame = Bounds();
 
	_CreateMenu(frame);
		// will adjust frame to account for menubar
 
	frame.right -= B_V_SCROLL_BAR_WIDTH;
	frame.bottom -= B_H_SCROLL_BAR_HEIGHT;
	fListView = new PlaylistListView(frame, playlist, controller,
		fCommandStack);
 
	BScrollView* scrollView = new BScrollView("playlist scrollview", fListView,
		B_FOLLOW_ALL_SIDES, 0, false, true, B_NO_BORDER);
 
	fTopView = 	scrollView;
	AddChild(fTopView);
 
	// small visual tweak
	if (BScrollBar* scrollBar = scrollView->ScrollBar(B_VERTICAL)) {
		// make it so the frame of the menubar is also the frame of
		// the scroll bar (appears to be)
		scrollBar->MoveBy(0, -1);
		scrollBar->ResizeBy(0, 2);
	}
 
	frame.top += frame.Height();
	frame.bottom += B_H_SCROLL_BAR_HEIGHT;
 
	fTotalDuration = new BStringView(frame, "fDuration", "",
		B_FOLLOW_BOTTOM | B_FOLLOW_LEFT_RIGHT);
	fTotalDuration->SetAlignment(B_ALIGN_RIGHT);
	fTotalDuration->SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
	AddChild(fTotalDuration);
 
	_UpdateTotalDuration(0);
 
	{
		BAutolock _(fPlaylist);
 
		_QueryInitialDurations();
		fPlaylist->AddListener(fDurationListener);
	}
 
	fCommandStack->AddListener(&fCommandStackListener);
	_ObjectChanged(fCommandStack);
}
 
 
PlaylistWindow::~PlaylistWindow()
{
	// give listeners a chance to detach themselves
	fTopView->RemoveSelf();
	delete fTopView;
 
	fCommandStack->RemoveListener(&fCommandStackListener);
	delete fCommandStack;
	delete fLocker;
 
	fPlaylist->RemoveListener(fDurationListener);
	BMessenger(fDurationListener).SendMessage(B_QUIT_REQUESTED);
}
 
 
bool
PlaylistWindow::QuitRequested()
{
	Hide();
	return false;
}
 
 
void
PlaylistWindow::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case B_MODIFIERS_CHANGED:
			if (LastMouseMovedView())
				PostMessage(message, LastMouseMovedView());
			break;
 
		case B_UNDO:
			fCommandStack->Undo();
			break;
		case B_REDO:
			fCommandStack->Redo();
			break;
 
		case MSG_OBJECT_CHANGED: {
			Notifier* notifier;
			if (message->FindPointer("object", (void**)&notifier) == B_OK)
				_ObjectChanged(notifier);
			break;
		}
 
		case M_URL_RECEIVED:
		case B_REFS_RECEIVED:
			// Used for when we open a playlist from playlist window
			if (!message->HasInt32("append_index")) {
				message->AddInt32("append_index",
					APPEND_INDEX_REPLACE_PLAYLIST);
			}
			// supposed to fall through
		case B_SIMPLE_DATA:
		{
			// only accept this message when it comes from the
			// player window, _not_ when it is dropped in this window
			// outside of the playlist!
			int32 appendIndex;
			if (message->FindInt32("append_index", &appendIndex) == B_OK)
				fListView->ItemsReceived(message, appendIndex);
			break;
		}
 
		case M_PLAYLIST_OPEN:
		{
			BMessenger target(this);
			BMessage result(B_REFS_RECEIVED);
			BMessage appMessage(M_SHOW_OPEN_PANEL);
			appMessage.AddMessenger("target", target);
			appMessage.AddMessage("message", &result);
			appMessage.AddString("title", B_TRANSLATE("Open Playlist"));
			appMessage.AddString("label", B_TRANSLATE("Open"));
			be_app->PostMessage(&appMessage);
			break;
		}
 
		case M_PLAYLIST_SAVE:
			if (fSavedPlaylistRef != entry_ref()) {
				_SavePlaylist(fSavedPlaylistRef);
				break;
			}
			// supposed to fall through
		case M_PLAYLIST_SAVE_AS:
		{
			BMessenger target(this);
			BMessage result(M_PLAYLIST_SAVE_RESULT);
			BMessage appMessage(M_SHOW_SAVE_PANEL);
			appMessage.AddMessenger("target", target);
			appMessage.AddMessage("message", &result);
			appMessage.AddString("title", B_TRANSLATE("Save Playlist"));
			appMessage.AddString("label", B_TRANSLATE("Save"));
			be_app->PostMessage(&appMessage);
			break;
		}
 
		case M_PLAYLIST_SAVE_RESULT:
			_SavePlaylist(message);
			break;
 
		case B_SELECT_ALL:
			fListView->SelectAll();
			break;
 
		case M_PLAYLIST_RANDOMIZE:
			fListView->Randomize();
			break;
 
		case M_PLAYLIST_REMOVE:
			fListView->RemoveSelected();
			break;
 
		case M_PLAYLIST_MOVE_TO_TRASH:
		{
			int32 index;
			if (message->FindInt32("playlist index", &index) == B_OK)
				fListView->RemoveToTrash(index);
			else
				fListView->RemoveSelectionToTrash();
			break;
		}
 
		default:
			BWindow::MessageReceived(message);
			break;
	}
}
 
 
// #pragma mark -
 
 
void
PlaylistWindow::_CreateMenu(BRect& frame)
{
	frame.bottom = 15;
	BMenuBar* menuBar = new BMenuBar(frame, "main menu");
	BMenu* fileMenu = new BMenu(B_TRANSLATE("Playlist"));
	menuBar->AddItem(fileMenu);
	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Open" B_UTF8_ELLIPSIS),
		new BMessage(M_PLAYLIST_OPEN), 'O'));
	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Save as" B_UTF8_ELLIPSIS),
		new BMessage(M_PLAYLIST_SAVE_AS), 'S', B_SHIFT_KEY));
//	fileMenu->AddItem(new BMenuItem("Save",
//		new BMessage(M_PLAYLIST_SAVE), 'S'));
 
	fileMenu->AddSeparatorItem();
 
	fileMenu->AddItem(new BMenuItem(B_TRANSLATE("Close"),
		new BMessage(B_QUIT_REQUESTED), 'W'));
 
	BMenu* editMenu = new BMenu(B_TRANSLATE("Edit"));
	fUndoMI = new BMenuItem(B_TRANSLATE("Undo"), new BMessage(B_UNDO), 'Z');
	editMenu->AddItem(fUndoMI);
	fRedoMI = new BMenuItem(B_TRANSLATE("Redo"), new BMessage(B_REDO), 'Z', 
		B_SHIFT_KEY);
	editMenu->AddItem(fRedoMI);
	editMenu->AddSeparatorItem();
	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Select all"),
		new BMessage(B_SELECT_ALL), 'A'));
	editMenu->AddSeparatorItem();
	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Randomize"),
		new BMessage(M_PLAYLIST_RANDOMIZE), 'R'));
	editMenu->AddSeparatorItem();
	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Remove"),
		new BMessage(M_PLAYLIST_REMOVE)/*, B_DELETE, 0*/));
			// TODO: See if we can support the modifier-less B_DELETE
			// and draw it properly too. B_NO_MODIFIER?
	editMenu->AddItem(new BMenuItem(B_TRANSLATE("Move file to Trash"),
		new BMessage(M_PLAYLIST_MOVE_TO_TRASH), 'T'));
 
	menuBar->AddItem(editMenu);
 
	AddChild(menuBar);
	fileMenu->SetTargetForItems(this);
	editMenu->SetTargetForItems(this);
 
	menuBar->ResizeToPreferred();
	frame = Bounds();
	frame.top = menuBar->Frame().bottom + 1;
}
 
 
void
PlaylistWindow::_ObjectChanged(const Notifier* object)
{
	if (object == fCommandStack) {
		// relable Undo item and update enabled status
		BString label(B_TRANSLATE("Undo"));
		fUndoMI->SetEnabled(fCommandStack->GetUndoName(label));
		if (fUndoMI->IsEnabled())
			fUndoMI->SetLabel(label.String());
		else
			fUndoMI->SetLabel(B_TRANSLATE("<nothing to undo>"));
 
		// relable Redo item and update enabled status
		label.SetTo(B_TRANSLATE("Redo"));
		fRedoMI->SetEnabled(fCommandStack->GetRedoName(label));
		if (fRedoMI->IsEnabled())
			fRedoMI->SetLabel(label.String());
		else
			fRedoMI->SetLabel(B_TRANSLATE("<nothing to redo>"));
	}
}
 
 
void
PlaylistWindow::_SavePlaylist(const BMessage* message)
{
	entry_ref ref;
	const char* name;
	if (message->FindRef("directory", &ref) != B_OK
		|| message->FindString("name", &name) != B_OK) {
		display_save_alert(B_TRANSLATE("Internal error (malformed message). "
			"Saving the playlist failed."));
		return;
	}
 
	BString tempName(name);
	tempName << system_time();
 
	BPath origPath(&ref);
	BPath tempPath(&ref);
	if (origPath.InitCheck() != B_OK || tempPath.InitCheck() != B_OK
		|| origPath.Append(name) != B_OK
		|| tempPath.Append(tempName.String()) != B_OK) {
		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
			"Saving the playlist failed."));
		return;
	}
 
	BEntry origEntry(origPath.Path());
	BEntry tempEntry(tempPath.Path());
	if (origEntry.InitCheck() != B_OK || tempEntry.InitCheck() != B_OK) {
		display_save_alert(B_TRANSLATE("Internal error (out of memory). "
			"Saving the playlist failed."));
		return;
	}
 
	_SavePlaylist(origEntry, tempEntry, name);
}
 
 
void
PlaylistWindow::_SavePlaylist(const entry_ref& ref)
{
	BString tempName(ref.name);
	tempName << system_time();
	entry_ref tempRef(ref);
	tempRef.set_name(tempName.String());
 
	BEntry origEntry(&ref);
	BEntry tempEntry(&tempRef);
 
	_SavePlaylist(origEntry, tempEntry, ref.name);
}
 
 
void
PlaylistWindow::_SavePlaylist(BEntry& origEntry, BEntry& tempEntry,
	const char* finalName)
{
	class TempEntryRemover {
	public:
		TempEntryRemover(BEntry* entry)
			: fEntry(entry)
		{
		}
		~TempEntryRemover()
		{
			if (fEntry)
				fEntry->Remove();
		}
		void Detach()
		{
			fEntry = NULL;
		}
	private:
		BEntry* fEntry;
	} remover(&tempEntry);
 
	BFile file(&tempEntry, B_CREATE_FILE | B_ERASE_FILE | B_WRITE_ONLY);
	if (file.InitCheck() != B_OK) {
		BString errorMessage(B_TRANSLATE(
			"Saving the playlist failed:\n\nError: "));
		errorMessage << strerror(file.InitCheck());
		display_save_alert(errorMessage.String());
		return;
	}
 
	AutoLocker<Playlist> lock(fPlaylist);
	if (!lock.IsLocked()) {
		display_save_alert(B_TRANSLATE("Internal error (locking failed). "
			"Saving the playlist failed."));
		return;
	}
 
	status_t ret = fPlaylist->Flatten(&file);
	if (ret != B_OK) {
		display_save_alert(ret);
		return;
	}
	lock.Unlock();
 
	if (origEntry.Exists()) {
		// TODO: copy attributes
	}
 
	// clobber original entry, if it exists
	tempEntry.Rename(finalName, true);
	remover.Detach();
 
	BNodeInfo info(&file);
	info.SetType("application/x-vnd.haiku-playlist");
}
 
 
void
PlaylistWindow::_QueryInitialDurations()
{
	BAutolock lock(fPlaylist);
 
	BMessage addMessage(MSG_PLAYLIST_ITEM_ADDED);
	for (int32 i = 0; i < fPlaylist->CountItems(); i++) {
		addMessage.AddPointer("item", fPlaylist->ItemAt(i));
		addMessage.AddInt32("index", i);
	}
 
	BMessenger(fDurationListener).SendMessage(&addMessage);
}
 
 
void
PlaylistWindow::_UpdateTotalDuration(bigtime_t duration)
{
	BAutolock lock(this);
 
	char buffer[64];
	duration /= 1000000;
	duration_to_string(duration, buffer, sizeof(buffer));
 
	BString text;
	text.SetToFormat(B_TRANSLATE("Total duration: %s"), buffer);
 
	fTotalDuration->SetText(text.String());
}
 
 
// #pragma mark -
 
 
PlaylistWindow::DurationListener::DurationListener(PlaylistWindow& parent)
	:
	PlaylistObserver(this),
	fKnown(20, true),
	fTotalDuration(0),
	fParent(parent)
{
	Run();
}
 
 
PlaylistWindow::DurationListener::~DurationListener()
{
}
 
 
void
PlaylistWindow::DurationListener::MessageReceived(BMessage* message)
{
	switch (message->what) {
		case MSG_PLAYLIST_ITEM_ADDED:
		{
			void* item;
			int32 index;
 
			int32 currentItem = 0;
			while (message->FindPointer("item", currentItem, &item) == B_OK
				&& message->FindInt32("index", currentItem, &index) == B_OK) {
				_HandleItemAdded(static_cast<PlaylistItem*>(item), index);
				++currentItem;
			}
 
			break;
		}
 
		case MSG_PLAYLIST_ITEM_REMOVED:
		{
			int32 index;
 
			if (message->FindInt32("index", &index) == B_OK) {
				_HandleItemRemoved(index);
			}
 
			break;
		}
 
		default:
			BLooper::MessageReceived(message);
			break;
	}
}
 
 
bigtime_t
PlaylistWindow::DurationListener::TotalDuration()
{
	return fTotalDuration;
}
 
 
void
PlaylistWindow::DurationListener::_HandleItemAdded(PlaylistItem* item,
	int32 index)
{
	bigtime_t duration = item->Duration();
	fTotalDuration += duration;
	fParent._UpdateTotalDuration(fTotalDuration);
	fKnown.AddItem(new bigtime_t(duration), index);
}
 
 
void
PlaylistWindow::DurationListener::_HandleItemRemoved(int32 index)
{
	bigtime_t* deleted = fKnown.RemoveItemAt(index);
	if (deleted == NULL)
		return;
 
	fTotalDuration -= *deleted;
	fParent._UpdateTotalDuration(fTotalDuration);
 
	delete deleted;
}
 

V763 Parameter 'frame' is always rewritten in function body before being used.

V773 Visibility scope of the 'alert' pointer was exited without releasing the memory. A memory leak is possible.