// 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 <qtimer.h>

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

#include <cassert>

namespace
{
QString relativeString(KURL parent, 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 &def) 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 def;
	}
	else
	{
		return item->metaInfo().value(key).toString();
	}
}

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)
	, treeView(new FileTreeView(viewParent, viewName))
	, myBranch(0)
	, currentItem(0)
	, shuffle(false)
	, historyPosition(history.end())
{
	connect(treeView, SIGNAL(executed(QListViewItem *)), this, SLOT(executed(QListViewItem *)));
}

void Hayes::Playlist::open(const KURL &newRoot)
{
	if(newRoot == root) return;
	if(myBranch) treeView->removeBranch(myBranch);
	myBranch = new Branch(treeView, newRoot, newRoot.prettyURL());
	treeView->addBranch(myBranch);
	root = newRoot;

	// Setting the currentItem to the first item is delayed until the first
	// next() or current(). This is the price of the async loading that
	// KFileTreeView gives us.
	firstTime = true;

	myBranch->root()->setOpen(true);
}

void Hayes::Playlist::setShuffle(bool b)
{
	shuffle = b;
}

void Hayes::Playlist::setSaveVolume(bool b)
{
	saveVolume = b;
}

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();
}

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

PlaylistItem Hayes::Playlist::current(void)
{
	if(!myBranch) return 0;
	if(firstTime)
	{
		kdDebug(66666) << "first time current" << endl;
		setCurrentItem(getFirstItem(true, true));
		return current();
	}
	else
	{
		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::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;

	if(shuffle && honorShuffle)
	{
		return getNextItem(0, honorCheckBox, honorShuffle);
	}
	else
	// this is the normal, non-shuffle first
	{
		FileTreeViewItem *item = static_cast<FileTreeViewItem *>(myBranch->root()->firstChild());
	
		if(!item || (!item->isDir() && (item->isOn() || !honorCheckBox)))
			return item;
		else
			return getNextItem(item, honorCheckBox, false);
	}
}

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 *fitem,
                                                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.

			FileTreeViewItem *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());
					int whichChild = kapp->random() % item->childCount();
					for(int i = 0; i < whichChild; ++i)
						newItem = static_cast<FileTreeViewItem *>(newItem->nextSibling());
				
					if(newItem->isDir()) const_cast<Playlist *>(this)->openItem(item);

				} 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
	{
		FileTreeViewItem *item = static_cast<FileTreeViewItem *>(fitem);
		if(!item) item = getFirstItem(honorCheckBox, false);
		if(!item) return 0;
		do
		{
			if(item->isDir())
			{
				if((item->isOn() || !honorCheckBox))
					const_cast<Playlist *>(this)->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->isDir() && !item->isOpen() && (item->isOn() || !honorCheckBox))
			{
				const_cast<Playlist *>(this)->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)
{
	if(!item->isDir() || item->isOpen()) return;
	itemToOpen = item;
	QTimer::singleShot(1, 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::setCurrentItem(FileTreeViewItem *item)
{
	firstTime = false;

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

	currentItem = item;
	treeView->setSpecialItem(static_cast<FileTreeViewItem *>(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
{
	// TODO: this *fails* when charsets are mismatched
	FileTreeViewItem *item = static_cast<FileTreeViewItem *>(treeView->findItem(const_cast<Branch *>(myBranch),
	                          relativeString(myBranch->rootUrl(), 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"
