// Copyright (C) 2002 Neil Stevens <neil@qualityassistant.com>
//
// 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 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 MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// THE AUTHOR(S) 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(s) of the author(s) shall not be
// used in advertising or otherwise to promote the sale, use or other dealings
// in this Software without prior written authorization from the author(s).

#include <kapplication.h>
#include <kdebug.h>
#include <kfilemetainfo.h>
#include <klocale.h>
#include <noatun/app.h>
#include <noatun/player.h>
#include <qheader.h>
#include <qvaluestack.h>
#include <qtimer.h>

#include "branch.h"
#include "playlist.h"

#include <cassert>

namespace
{
QString relativeString(const KURL& parent, const KURL& child)
{
	QString childString = child.url();
	QString parentString = parent.url(1);
	childString.remove(0, parentString.length());
	return KURL::decode_string(childString);
}
}

//////////////////////
// PlaylistItemData //
//////////////////////

Hayes::PlaylistItemData::PlaylistItemData(const KFileItem &i)
	: url(i.url())
	, item(new KFileItem(i))
	, len(-1)
{
}

Hayes::PlaylistItemData::~PlaylistItemData(void)
{
	delete item;
	item = 0;
}

QString Hayes::PlaylistItemData::property(const QString &key, const QString &) const
{
	// Ideally url would be the only property not handled by KFileMetaInfo.
	// index is there for one reason alone: the plugin called Flood.
	if(key == "url" || key == "index")
	{
		return url.prettyURL();
	}
	// Length is treated specially becuase the KFileMetaInfo is often
	// inaccurate, at least in my experience.  And noatun's value is accurate
	// by definition.
	else if(key == "length")
	{
		if(len != -1 || !item || !item->metaInfo().isValid())
			return QString::number(len);

		int length = item->metaInfo().value(key).toInt();
		return QString::number(length * 1000);
	}
	else if(item && item->metaInfo().contains(key))
	{
		return item->metaInfo().value(key).toString();
	}
	else return QString::null;
}

void Hayes::PlaylistItemData::setProperty(const QString &key, const QString &value)
{
	if(key == "length")
	{
		len = value.toInt();
	}
}

void Hayes::PlaylistItemData::clearProperty(const QString &)
{
}

QStringList Hayes::PlaylistItemData::properties(void) const
{
	QStringList list;
	list.append("url");
	list.append("index");
	if(item && item->metaInfo().isValid())
		list += item->metaInfo().supportedKeys();
	if(!list.contains("length")) list.append("length");
	return list;
}

bool Hayes::PlaylistItemData::isProperty(const QString &key) const
{
	return key == "url" ||
	       key == "index" ||
	       key == "length" ||
	       (item && item->metaInfo().isValid() && item->metaInfo().contains(key))
	       ;
}

bool Hayes::PlaylistItemData::operator==(const PlaylistItemData &b) const
{
	return url == b.url;
}

const KFileItem &Hayes::PlaylistItemData::fileItem(void) const
{
	return *item;
}

//////////////
// Playlist //
//////////////

Hayes::Playlist::Playlist(QWidget *viewParent, QWidget *parent, const char *viewName, const char *name)
	: ::Playlist(parent, name)
	, DCOPObject("Hayes")
	, treeView(new FileTreeView(viewParent, viewName))
	, myBranch(0)
	, currentItem(0)
	, itemToOpen(0)
	, shuffle(false)
	, historyPosition(history.end())
{
	napp->setAutoPlay(false);
	connect(treeView, SIGNAL(executed(QListViewItem *)), this, SLOT(executed(QListViewItem *)));
	connect(treeView, SIGNAL(itemTaken(FileTreeViewItem *)), this, SLOT(itemDeleted(FileTreeViewItem *)));
}

void Hayes::Playlist::open(const KURL &newRoot)
{
	if(newRoot == root) return;
	clear();
	myBranch = new Branch(treeView, newRoot, newRoot.prettyURL());
	connect(myBranch, SIGNAL(clear(void)), this, SLOT(cleared(void)));
	treeView->addBranch(myBranch);
	root = newRoot;
	myBranch->root()->setOpen(true);
}

void Hayes::Playlist::clear(void)
{
	kdDebug(66666) << "clear" << endl;
	setCurrentItem(0);
	if(myBranch) treeView->removeBranch(myBranch);
	history.clear();
	historyPosition = history.end();
}

void Hayes::Playlist::setShuffle(bool b)
{
	if(shuffle != b)
	{
		shuffle = b;
		emit shuffleChanged(b);
	}
}

void Hayes::Playlist::toggleShuffle(void)
{
	setShuffle(!shuffle);
}

bool Hayes::Playlist::isShuffling(void)
{
	return shuffle;
}

void Hayes::Playlist::setSaveVolume(bool b)
{
	if(saveVolume != b)
	{
		saveVolume = b;
		emit saveVolumeChanged(b);
	}
}

void Hayes::Playlist::toggleSaveVolume(void)
{
	setSaveVolume(!saveVolume);
}

bool Hayes::Playlist::isSavingVolume(void)
{
	return saveVolume;
}

void Hayes::Playlist::reset(void)
{
	if(!myBranch) return;
	setCurrentItem(getFirstItem(true, false));
}

void Hayes::Playlist::setCurrent(const PlaylistItem &givenItem)
{
	if(!myBranch) return;
	FileTreeViewItem *item = findItem(givenItem);
	if(!item) return;

	setCurrentItem(item);
	playCurrent();
}

void Hayes::Playlist::setCurrent(const KURL &url)
{
	setCurrent(makePlaylistItem(findItem(url)));
}

void Hayes::Playlist::setCurrent(const QString &url)
{
	setCurrent(KURL(url));
}

PlaylistItem Hayes::Playlist::next(void)
{
	kdDebug(66666) << "next" << endl;
	if(!myBranch) return 0;
	setCurrentItem(getNextItem(currentItem, true, true));
	playCurrent();
	return current();
}

PlaylistItem Hayes::Playlist::nextSection(void)
{
	if(!myBranch) return 0;
	if(!currentItem) return next();
	QListViewItem *currentSection = currentItem->parent();
	FileTreeViewItem *item = currentItem;
	while(item && item->parent() == currentSection)
		item = getNextItem(item, true, true);
	setCurrentItem(item);
	playCurrent();
	return current();
}

PlaylistItem Hayes::Playlist::current(void)
{
	if(!myBranch) return 0;
	if(!currentItem)
	{
		FileTreeViewItem *item = getFirstItem(true, true);
		if(item) setCurrentItem(item);
	}
	return makePlaylistItem(currentItem);
}

PlaylistItem Hayes::Playlist::previous(void)
{
	if(!myBranch) return 0;
	setCurrentItem(getPreviousItem(currentItem, true, true));
	if(!currentItem) reset();
	playCurrent();
	return current();
}

PlaylistItem Hayes::Playlist::previousSection(void)
{
	if(!myBranch) return 0;
	if(!currentItem) return previous();
	QListViewItem *currentSection = currentItem->parent();
	FileTreeViewItem *item = currentItem;
	while(item && item->parent() == currentSection)
		item = getPreviousItem(item, true, true);
	if(item) item = getNextItem(static_cast<FileTreeViewItem *>(item->parent()), true, false);
	setCurrentItem(item);
	playCurrent();
	return current();
}

PlaylistItem Hayes::Playlist::getFirst(void) const
{
	if(!myBranch) return 0;
	return makePlaylistItem(getFirstItem(false, false));
}

PlaylistItem Hayes::Playlist::getLast(void) const
{
	if(!myBranch) return 0;
	return makePlaylistItem(getLastItem(false));
}

PlaylistItem Hayes::Playlist::getAfter(const PlaylistItem &prevItem) const
{
	if(!myBranch) return 0;
	FileTreeViewItem *item = findItem(prevItem);

	if(!item) return 0;
	return makePlaylistItem(getNextItem(item, false, false));
}

PlaylistItem Hayes::Playlist::getBefore(const PlaylistItem &nextItem) const
{
	if(!myBranch) return 0;
	FileTreeViewItem *item = findItem(nextItem);

	if(!item) return 0;
	return makePlaylistItem(getPreviousItem(item, false, false));
}

Hayes::FileTreeViewItem *Hayes::Playlist::viewItem(const PlaylistItem &pItem) const
{
	if(!myBranch) return 0;
	return findItem(pItem);
}

Hayes::FileTreeViewItem *Hayes::Playlist::getFirstItem(bool honorCheckBox, bool honorShuffle) const
{
	if(!myBranch || !myBranch->root()) return 0;

	FileTreeViewItem *item = static_cast<FileTreeViewItem *>(myBranch->root());
	return getNextItem(item, honorCheckBox, honorShuffle);
}

Hayes::FileTreeViewItem *Hayes::Playlist::getLastItem(bool honorCheckBox) const
{
	if(!myBranch || !myBranch->root()) return 0;

	FileTreeViewItem *item = static_cast<FileTreeViewItem *>(myBranch->root());
	if(!item || !item->firstChild()) return 0;

	// Get the very very last item in the treeview
	// The, starting at that bottom, work up to the first valid item
	while(item->firstChild())
	{
		item = static_cast<FileTreeViewItem *>(item->firstChild());
		while(item->nextSibling()) item = static_cast<FileTreeViewItem *>(item->nextSibling());
	}

	if(!item || (!item->isDir() && (item->isOn() || !honorCheckBox)))
		return item;
	else
		return getPreviousItem(item, honorCheckBox, false);
}

Hayes::FileTreeViewItem *Hayes::Playlist::getNextItem(FileTreeViewItem *item,
                                                      bool honorCheckBox,
                                                      bool honorShuffle) const
{
	if(shuffle && honorShuffle)
	{
		if(historyPosition == history.end() || ++historyPosition == history.end())
		{
			// find a new random item:

			// 1: start at root item
			// 2: pick a random checked child file/non-empty child dir of item
			// 3: if item is a directory, goto 2
			// 4: return item

			// Note: an efficient way of getting a uniform distribution across
			// file items is welcome.

			item = static_cast<FileTreeViewItem *>(treeView->firstChild());
			if(!item) return 0;
			do
			{
				kdDebug(66666) << "trying item " << item->text(0) << endl;
				FileTreeViewItem *newItem;
				do
				{
					newItem =  static_cast<FileTreeViewItem *>(item->firstChild());
					if (!newItem) return 0;

					int whichChild = kapp->random() % item->childCount();
					for(int i = 0; i < whichChild; ++i)
						newItem = static_cast<FileTreeViewItem *>(newItem->nextSibling());
				
					if(newItem->isDir() && newItem->isOn()) openItem(newItem);

				} while((honorCheckBox && !newItem->isOn()) ||
				        (newItem->isDir() && !newItem->childCount()));

				item = newItem;

			} while(item->fileItem()->isDir());
			history.append(item->fileItem()->url());
			--historyPosition;
			return item;
		}
		else
		{
			return findItem(*historyPosition);
		}
	}
	else
	// Here is the normal, non-shuffle next
	{
		if(!item) return getFirstItem(honorCheckBox, false);
		if(!item)
		{
			napp->player()->stop();
			return 0;
		}
		do
		{
			if(item->isDir())
			{
				if((item->isOn() || !honorCheckBox))
					openItem(item);
				else
					item->setOpen(false);
			}
			item = static_cast<FileTreeViewItem *>(item->itemBelow());
		}
		while(item && (item->isDir() || !(item->isOn() || !honorCheckBox)));

		return item;
	}
}

Hayes::FileTreeViewItem *Hayes::Playlist::getPreviousItem(FileTreeViewItem *fitem,
                                                          bool honorCheckBox,
                                                          bool honorShuffle) const
{
	if(shuffle && honorShuffle)
	{
		if(historyPosition == history.begin())
			historyPosition = history.end();
		return findItem(*(--historyPosition));
	}
	else
	// Here is the normal, non-shuffle previous
	{
		if(!fitem) return 0;
		FileTreeViewItem *item = static_cast<FileTreeViewItem *>(fitem);
		do
		{
			item = static_cast<FileTreeViewItem *>(item->itemAbove());
			if(item && item->isDir() && !item->isOpen() && (item->isOn() || !honorCheckBox))
			{
				openItem(item);
				// now go to the last child
				QListViewItem *i;
				for(i = item->firstChild(); i->nextSibling(); i = i->nextSibling());
				item = static_cast<FileTreeViewItem *>(i);
			}
		}
		while(item && (item->isDir() || !(item->isOn() || !honorCheckBox)));

		return item;
	}
}

// KFileTreeView may be asynchronous, but this isn't
void Hayes::Playlist::openItem(FileTreeViewItem *item) const
{
	if(!item->isDir() || item->isOpen()) return;
	itemToOpen = item;
	QTimer::singleShot(1, const_cast<Playlist *>(this), SLOT(populateBegin(void)));
	kapp->enter_loop();
}

void Hayes::Playlist::populateBegin(void)
{
	connect(myBranch, SIGNAL(populateFinished(KFileTreeViewItem *)), this, SLOT(populateFinished(KFileTreeViewItem *)));
	emit busy(i18n("Opening %1").arg(itemToOpen->url().prettyURL()));
	itemToOpen->setOpen(true);
}

void Hayes::Playlist::populateFinished(KFileTreeViewItem *item)
{
	disconnect(myBranch, SIGNAL(populateFinished(KFileTreeViewItem *)), this, SLOT(populateFinished(KFileTreeViewItem *)));
	emit finished(i18n("Finished opening %1").arg(item->url().prettyURL()), 2000);
	item->sort();
	kapp->exit_loop();
}

void Hayes::Playlist::executed(QListViewItem *qitem)
{
	FileTreeViewItem *item = dynamic_cast<FileTreeViewItem *>(qitem);
	if(!item || item->isDir()) return;

	setCurrentItem(item);
	playCurrent();
}

void Hayes::Playlist::itemDeleted(FileTreeViewItem *item)
{
	kdDebug(66666) << "itemDeleted " << item->url().prettyURL() << endl;
	KURL url = item->url();
	for(QValueList<KURL>::Iterator i = history.begin(); i != history.end(); ++i)
	{
		if(*i == url || url.isParentOf(*i))
		{
			history.remove(i);
			--i;
		}
	}
	if(item == currentItem)
	{
		kdDebug(66666) << "itemDeleted was current item" << endl;
		currentWasDeletedRudely();
	}
}

void Hayes::Playlist::cleared(void)
{
	myBranch = 0;
	currentWasDeletedRudely();
	clear();
}

void Hayes::Playlist::currentWasDeletedRudely(void)
{
	currentItem = 0;
	playCurrent();
	emit newSong(0);
}

void Hayes::Playlist::setCurrentItem(FileTreeViewItem *item)
{
	if(currentItem == item) return;

	kdDebug(66666) << "setCurrentItem " << (void *)item << endl;

	if(saveVolume && currentItem)
		currentItem->setVolume(napp->player()->volume());

	currentItem = item;
	treeView->setSpecialItem(item);
	if(item)
	{
		treeView->ensureItemVisible(item);
		if(shuffle && (item->fileItem()->url() != *historyPosition))
		{
			history.append(item->fileItem()->url());
			historyPosition = history.end();
			--historyPosition;
		}

		if(saveVolume && item->hasVolume())
			napp->player()->setVolume(item->volume());
	}

	emit newSong(current());
}

PlaylistItem Hayes::Playlist::makePlaylistItem(FileTreeViewItem *item)
{
	if(!item) return 0;
	PlaylistItemData *data = new PlaylistItemData(*item->fileItem());
	return PlaylistItem(data);
}

Hayes::FileTreeViewItem *Hayes::Playlist::findItem(KURL url) const
{
	if(url == root) return static_cast<FileTreeViewItem *>(myBranch->root());
	if(!root.isParentOf(url)) return 0;

	// TODO: this *fails* when charsets are mismatched
	FileTreeViewItem *item = static_cast<FileTreeViewItem *>(treeView->findItem(myBranch,
	                                                         relativeString(root, url)));

	if(!item)
	{
		// Open parent of URL, so that url can actually be found
		KURL parentURL = url;
		parentURL.cd("..");
		FileTreeViewItem *parent = findItem(parentURL);
		if(!parent || !parent->isDir()) return 0;
		openItem(parent);

		// TODO: this *fails* when charsets are mismatched
		item = static_cast<FileTreeViewItem *>(treeView->findItem(myBranch,
		                                       relativeString(root, url)));
	}

	return item;
}

Hayes::FileTreeViewItem *Hayes::Playlist::findItem(PlaylistItem plItem) const
{
	// I don't trust Noatun as far as I can throw it
	const PlaylistItemData *d = dynamic_cast<const PlaylistItemData *>(plItem.data());
	if(!d) return 0; // Dork at the helm?

	// OK, now that the sanity check is passed
	return findItem(d->fileItem().url());
}

#include "playlist.moc"
