/***************************************************************************
    begin                : Sat Feb 16 2002
    copyright            : (C) 2002 - 2004 by Scott Wheeler
    email                : wheeler@kde.org
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include <tdeconfig.h>
#include <tdemessagebox.h>
#include <kurldrag.h>
#include <kiconloader.h>
#include <klineedit.h>
#include <tdeaction.h>
#include <tdepopupmenu.h>
#include <tdelocale.h>
#include <kdebug.h>
#include <kinputdialog.h>
#include <tdefiledialog.h>
#include <tdeglobalsettings.h>
#include <kurl.h>
#include <tdeio/netaccess.h>
#include <tdeio/job.h>
#include <dcopclient.h>

#include <tqheader.h>
#include <tqcursor.h>
#include <tqdir.h>
#include <tqeventloop.h>
#include <tqtooltip.h>
#include <tqwidgetstack.h>
#include <tqfile.h>
#include <tqhbox.h>

#include <id3v1genres.h>

#include <time.h>
#include <math.h>
#include <dirent.h>

#include "playlist.h"
#include "playlistitem.h"
#include "playlistcollection.h"
#include "playlistsearch.h"
#include "mediafiles.h"
#include "collectionlist.h"
#include "filerenamer.h"
#include "actioncollection.h"
#include "tracksequencemanager.h"
#include "juk.h"
#include "tag.h"
#include "k3bexporter.h"
#include "upcomingplaylist.h"
#include "deletedialog.h"
#include "webimagefetcher.h"
#include "coverinfo.h"
#include "coverdialog.h"
#include "tagtransactionmanager.h"
#include "cache.h"

#define TStringToTQString(s) TQString::fromUtf8((s).toCString(true))

using namespace ActionCollection;

/**
 * Just a shortcut of sorts.
 */

static bool manualResize()
{
    return action<TDEToggleAction>("resizeColumnsManually")->isChecked();
}

/**
 * A tooltip specialized to show full filenames over the file name column.
 */

class PlaylistToolTip : public TQToolTip
{
public:
    PlaylistToolTip(TQWidget *parent, Playlist *playlist) :
	TQToolTip(parent), m_playlist(playlist) {}

    virtual void maybeTip(const TQPoint &p)
    {
	PlaylistItem *item = static_cast<PlaylistItem *>(m_playlist->itemAt(p));

	if(!item)
	    return;

	TQPoint contentsPosition = m_playlist->viewportToContents(p);

	int column = m_playlist->header()->sectionAt(contentsPosition.x());

	if(column == m_playlist->columnOffset() + PlaylistItem::FileNameColumn ||
	   item->cachedWidths()[column] > m_playlist->columnWidth(column) ||
	   (column == m_playlist->columnOffset() + PlaylistItem::CoverColumn &&
	    item->file().coverInfo()->hasCover()))
	{
	    TQRect r = m_playlist->itemRect(item);
	    int headerPosition = m_playlist->header()->sectionPos(column);
	    r.setLeft(headerPosition);
	    r.setRight(headerPosition + m_playlist->header()->sectionSize(column));

	    if(column == m_playlist->columnOffset() + PlaylistItem::FileNameColumn)
		tip(r, item->file().absFilePath());
	    else if(column == m_playlist->columnOffset() + PlaylistItem::CoverColumn) {
		TQMimeSourceFactory *f = TQMimeSourceFactory::defaultFactory();
	        f->setImage("coverThumb",
			    TQImage(item->file().coverInfo()->pixmap(CoverInfo::Thumbnail).convertToImage()));
	        tip(r, "<center><img source=\"coverThumb\"/></center>");
	    }
	    else
		tip(r, item->text(column));
	}
    }

private:
    Playlist *m_playlist;
};

////////////////////////////////////////////////////////////////////////////////
// Playlist::SharedSettings definition
////////////////////////////////////////////////////////////////////////////////

bool Playlist::m_visibleChanged = false;
bool Playlist::m_shuttingDown = false;

/**
 * Shared settings between the playlists.
 */

class Playlist::SharedSettings
{
public:
    static SharedSettings *instance();
    /**
     * Sets the default column order to that of Playlist @param p.
     */
    void setColumnOrder(const Playlist *l);
    void toggleColumnVisible(int column);
    void setInlineCompletionMode(TDEGlobalSettings::Completion mode);

    /**
     * Apply the settings.
     */
    void apply(Playlist *l) const;
    void sync() { writeConfig(); }

protected:
    SharedSettings();
    ~SharedSettings() {}

private:
    void writeConfig();

    static SharedSettings *m_instance;
    TQValueList<int> m_columnOrder;
    TQValueVector<bool> m_columnsVisible;
    TDEGlobalSettings::Completion m_inlineCompletion;
};

Playlist::SharedSettings *Playlist::SharedSettings::m_instance = 0;

////////////////////////////////////////////////////////////////////////////////
// Playlist::SharedSettings public members
////////////////////////////////////////////////////////////////////////////////

Playlist::SharedSettings *Playlist::SharedSettings::instance()
{
    static SharedSettings settings;
    return &settings;
}

void Playlist::SharedSettings::setColumnOrder(const Playlist *l)
{
    if(!l)
	return;

    m_columnOrder.clear();

    for(int i = l->columnOffset(); i < l->columns(); ++i)
	m_columnOrder.append(l->header()->mapToIndex(i));

    writeConfig();
}

void Playlist::SharedSettings::toggleColumnVisible(int column)
{
    if(column >= int(m_columnsVisible.size()))
	m_columnsVisible.resize(column + 1, true);

    m_columnsVisible[column] = !m_columnsVisible[column];

    writeConfig();
}

void Playlist::SharedSettings::setInlineCompletionMode(TDEGlobalSettings::Completion mode)
{
    m_inlineCompletion = mode;
    writeConfig();
}


void Playlist::SharedSettings::apply(Playlist *l) const
{
    if(!l)
	return;

    int offset = l->columnOffset();
    int i = 0;
    for(TQValueListConstIterator<int> it = m_columnOrder.begin(); it != m_columnOrder.end(); ++it)
	l->header()->moveSection(i++ + offset, *it + offset);

    for(uint i = 0; i < m_columnsVisible.size(); i++) {
	if(m_columnsVisible[i] && !l->isColumnVisible(i + offset))
	    l->showColumn(i + offset, false);
	else if(!m_columnsVisible[i] && l->isColumnVisible(i + offset))
	    l->hideColumn(i + offset, false);
    }

    l->updateLeftColumn();
    l->renameLineEdit()->setCompletionMode(m_inlineCompletion);
    l->slotColumnResizeModeChanged();
}

////////////////////////////////////////////////////////////////////////////////
// Playlist::ShareSettings protected members
////////////////////////////////////////////////////////////////////////////////

Playlist::SharedSettings::SharedSettings()
{
    TDEConfigGroup config(TDEGlobal::config(), "PlaylistShared");

    bool resizeColumnsManually = config.readBoolEntry("ResizeColumnsManually", false);
    action<TDEToggleAction>("resizeColumnsManually")->setChecked(resizeColumnsManually);

    // save column order
    m_columnOrder = config.readIntListEntry("ColumnOrder");

    TQValueList<int> l = config.readIntListEntry("VisibleColumns");

    if(l.isEmpty()) {

	// Provide some default values for column visibility if none were
	// read from the configuration file.

	for(int i = 0; i <= PlaylistItem::lastColumn(); i++) {
	    switch(i) {
	    case PlaylistItem::BitrateColumn:
	    case PlaylistItem::CommentColumn:
	    case PlaylistItem::FileNameColumn:
	    case PlaylistItem::FullPathColumn:
		m_columnsVisible.append(false);
		break;
	    default:
		m_columnsVisible.append(true);
	    }
	}
    }
    else {
	// Convert the int list into a bool list.

	m_columnsVisible.resize(l.size(), true);
	uint i = 0;
	for(TQValueList<int>::Iterator it = l.begin(); it != l.end(); ++it) {
	    if(! bool(*it))
		m_columnsVisible[i] = bool(*it);
	    i++;
	}
    }

    m_inlineCompletion = TDEGlobalSettings::Completion(
	config.readNumEntry("InlineCompletionMode", TDEGlobalSettings::CompletionAuto));
}

////////////////////////////////////////////////////////////////////////////////
// Playlist::SharedSettings private members
////////////////////////////////////////////////////////////////////////////////

void Playlist::SharedSettings::writeConfig()
{
    TDEConfigGroup config(TDEGlobal::config(), "PlaylistShared");
    config.writeEntry("ColumnOrder", m_columnOrder);

    TQValueList<int> l;
    for(uint i = 0; i < m_columnsVisible.size(); i++)
	l.append(int(m_columnsVisible[i]));

    config.writeEntry("VisibleColumns", l);
    config.writeEntry("InlineCompletionMode", m_inlineCompletion);

    config.writeEntry("ResizeColumnsManually", manualResize());

    TDEGlobal::config()->sync();
}

////////////////////////////////////////////////////////////////////////////////
// public members
////////////////////////////////////////////////////////////////////////////////

PlaylistItemList Playlist::m_history;
TQMap<int, PlaylistItem *> Playlist::m_backMenuItems;
int Playlist::m_leftColumn = 0;

Playlist::Playlist(PlaylistCollection *collection, const TQString &name,
		   const TQString &iconName) :
    TDEListView(collection->playlistStack(), name.latin1()),
    m_collection(collection),
    m_fetcher(new WebImageFetcher(this)),
    m_selectedCount(0),
    m_allowDuplicates(false),
    m_polished(false),
    m_applySharedSettings(true),
    m_columnWidthModeChanged(false),
    m_disableColumnWidthUpdates(true),
    m_time(0),
    m_widthsDirty(true),
    m_searchEnabled(true),
    m_lastSelected(0),
    m_playlistName(name),
    m_rmbMenu(0),
    m_toolTip(0),
    m_blockDataChanged(false)
{
    setup();
    collection->setupPlaylist(this, iconName);
}

Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items,
		   const TQString &name, const TQString &iconName) :
    TDEListView(collection->playlistStack(), name.latin1()),
    m_collection(collection),
    m_fetcher(new WebImageFetcher(this)),
    m_selectedCount(0),
    m_allowDuplicates(false),
    m_polished(false),
    m_applySharedSettings(true),
    m_columnWidthModeChanged(false),
    m_disableColumnWidthUpdates(true),
    m_time(0),
    m_widthsDirty(true),
    m_searchEnabled(true),
    m_lastSelected(0),
    m_playlistName(name),
    m_rmbMenu(0),
    m_toolTip(0),
    m_blockDataChanged(false)
{
    setup();
    collection->setupPlaylist(this, iconName);
    createItems(items);
}

Playlist::Playlist(PlaylistCollection *collection, const TQFileInfo &playlistFile,
		   const TQString &iconName) :
    TDEListView(collection->playlistStack()),
    m_collection(collection),
    m_fetcher(new WebImageFetcher(this)),
    m_selectedCount(0),
    m_allowDuplicates(false),
    m_polished(false),
    m_applySharedSettings(true),
    m_columnWidthModeChanged(false),
    m_disableColumnWidthUpdates(true),
    m_time(0),
    m_widthsDirty(true),
    m_searchEnabled(true),
    m_lastSelected(0),
    m_fileName(playlistFile.absFilePath()),
    m_rmbMenu(0),
    m_toolTip(0),
    m_blockDataChanged(false)
{
    setup();
    loadFile(m_fileName, playlistFile);
    collection->setupPlaylist(this, iconName);
}

Playlist::Playlist(PlaylistCollection *collection, bool delaySetup) :
    TDEListView(collection->playlistStack()),
    m_collection(collection),
    m_fetcher(new WebImageFetcher(this)),
    m_selectedCount(0),
    m_allowDuplicates(false),
    m_polished(false),
    m_applySharedSettings(true),
    m_columnWidthModeChanged(false),
    m_disableColumnWidthUpdates(true),
    m_time(0),
    m_widthsDirty(true),
    m_searchEnabled(true),
    m_lastSelected(0),
    m_rmbMenu(0),
    m_toolTip(0),
    m_blockDataChanged(false)
{
    setup();

    if(!delaySetup)
	collection->setupPlaylist(this, "audio-midi");
}

Playlist::~Playlist()
{
    // clearItem() will take care of removing the items from the history,
    // so call clearItems() to make sure it happens.

    clearItems(items());

    delete m_toolTip;

    // Select a different playlist if we're the selected one

    if(isVisible() && this != CollectionList::instance())
	m_collection->raise(CollectionList::instance());

    if(!m_shuttingDown)
	m_collection->removePlaylist(this);
}

TQString Playlist::name() const
{
    if(m_playlistName.isNull())
	return m_fileName.section(TQDir::separator(), -1).section('.', 0, -2);
    else
	return m_playlistName;
}

FileHandle Playlist::currentFile() const
{
    return playingItem() ? playingItem()->file() : FileHandle::null();
}

int Playlist::time() const
{
    // Since this method gets a lot of traffic, let's optimize for such.

    if(!m_addTime.isEmpty()) {
	for(PlaylistItemList::ConstIterator it = m_addTime.begin();
	    it != m_addTime.end(); ++it)
	{
	    if(*it)
		m_time += (*it)->file().tag()->seconds();
	}

	m_addTime.clear();
    }

    if(!m_subtractTime.isEmpty()) {
	for(PlaylistItemList::ConstIterator it = m_subtractTime.begin();
	    it != m_subtractTime.end(); ++it)
	{
	    if(*it)
		m_time -= (*it)->file().tag()->seconds();
	}

	m_subtractTime.clear();
    }

    return m_time;
}

void Playlist::playFirst()
{
    TrackSequenceManager::instance()->setNextItem(static_cast<PlaylistItem *>(
	TQListViewItemIterator(const_cast<Playlist *>(this), TQListViewItemIterator::Visible).current()));
    action("forward")->activate();
}

void Playlist::playNextAlbum()
{
    PlaylistItem *current = TrackSequenceManager::instance()->currentItem();
    if(!current)
	return; // No next album if we're not already playing.

    TQString currentAlbum = current->file().tag()->album();
    current = TrackSequenceManager::instance()->nextItem();

    while(current && current->file().tag()->album() == currentAlbum)
	current = TrackSequenceManager::instance()->nextItem();

    TrackSequenceManager::instance()->setNextItem(current);
    action("forward")->activate();
}

void Playlist::playNext()
{
    TrackSequenceManager::instance()->setCurrentPlaylist(this);
    setPlaying(TrackSequenceManager::instance()->nextItem());
}

void Playlist::stop()
{
    m_history.clear();
    setPlaying(0);
}

void Playlist::playPrevious()
{
    if(!playingItem())
	return;

    bool random = action("randomPlay") && action<TDEToggleAction>("randomPlay")->isChecked();

    PlaylistItem *previous = 0;

    if(random && !m_history.isEmpty()) {
        PlaylistItemList::Iterator last = m_history.fromLast();
        previous = *last;
        m_history.remove(last);
    }
    else {
	m_history.clear();
	previous = TrackSequenceManager::instance()->previousItem();
    }

    if(!previous)
	previous = static_cast<PlaylistItem *>(playingItem()->itemAbove());

    setPlaying(previous, false);
}

void Playlist::setName(const TQString &n)
{
    m_collection->addNameToDict(n);
    m_collection->removeNameFromDict(m_playlistName);

    m_playlistName = n;
    emit signalNameChanged(m_playlistName);
}

void Playlist::save()
{
    if(m_fileName.isEmpty())
	return saveAs();

    TQFile file(m_fileName);

    if(!file.open(IO_WriteOnly))
	return KMessageBox::error(this, i18n("Could not save to file %1.").arg(m_fileName));

    TQTextStream stream(&file);

    TQStringList fileList = files();

    for(TQStringList::Iterator it = fileList.begin(); it != fileList.end(); ++it)
	stream << *it << endl;

    file.close();
}

void Playlist::saveAs()
{
    m_collection->removeFileFromDict(m_fileName);

    m_fileName = MediaFiles::savePlaylistDialog(name(), this);

    if(!m_fileName.isEmpty()) {
	m_collection->addFileToDict(m_fileName);

	// If there's no playlist name set, use the file name.
	if(m_playlistName.isEmpty())
	    emit signalNameChanged(name());
	save();
    }
}

void Playlist::clearItem(PlaylistItem *item, bool emitChanged)
{
    emit signalAboutToRemove(item);
    m_members.remove(item->file().absFilePath());
    m_search.clearItem(item);

    m_history.remove(item);
    m_addTime.remove(item);
    m_subtractTime.remove(item);

    delete item;
    if(emitChanged)
	dataChanged();
}

void Playlist::clearItems(const PlaylistItemList &items)
{
    m_blockDataChanged = true;

    for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it)
	clearItem(*it, false);

    m_blockDataChanged = false;

    dataChanged();
}

PlaylistItem *Playlist::playingItem() // static
{
    return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front();
}

TQStringList Playlist::files() const
{
    TQStringList list;

    for(TQListViewItemIterator it(const_cast<Playlist *>(this)); it.current(); ++it)
	list.append(static_cast<PlaylistItem *>(*it)->file().absFilePath());

    return list;
}

PlaylistItemList Playlist::items()
{
    return items(TQListViewItemIterator::IteratorFlag(0));
}

PlaylistItemList Playlist::visibleItems()
{
    return items(TQListViewItemIterator::Visible);
}

PlaylistItemList Playlist::selectedItems()
{
    PlaylistItemList list;

    switch(m_selectedCount) {
    case 0:
	break;
	// case 1:
	// list.append(m_lastSelected);
	// break;
    default:
	list = items(TQListViewItemIterator::IteratorFlag(TQListViewItemIterator::Selected |
							 TQListViewItemIterator::Visible));
	break;
    }

    return list;
}

PlaylistItem *Playlist::firstChild() const
{
    return static_cast<PlaylistItem *>(TDEListView::firstChild());
}

void Playlist::updateLeftColumn()
{
    int newLeftColumn = leftMostVisibleColumn();

    if(m_leftColumn != newLeftColumn) {
	updatePlaying();
	m_leftColumn = newLeftColumn;
    }
}

void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static
{
    m_visibleChanged = true;
    for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it)
	(*it)->setVisible(visible);
}

void Playlist::setSearch(const PlaylistSearch &s)
{
    m_search = s;

    if(!m_searchEnabled)
	return;

    setItemsVisible(s.matchedItems(), true);
    setItemsVisible(s.unmatchedItems(), false);

    TrackSequenceManager::instance()->iterator()->playlistChanged();
}

void Playlist::setSearchEnabled(bool enabled)
{
    if(m_searchEnabled == enabled)
	return;

    m_searchEnabled = enabled;

    if(enabled) {
	setItemsVisible(m_search.matchedItems(), true);
	setItemsVisible(m_search.unmatchedItems(), false);
    }
    else
	setItemsVisible(items(), true);
}

void Playlist::markItemSelected(PlaylistItem *item, bool selected)
{
    if(selected && !item->isSelected()) {
	m_selectedCount++;
	m_lastSelected = item;
    }
    else if(!selected && item->isSelected())
	m_selectedCount--;
}

void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster)
{
    for(PlaylistList::ConstIterator it = sources.begin(); it != sources.end(); ++it) {
        if((*it)->playing()) {
            CollectionListItem *base = playingItem()->collectionItem();
            for(TQListViewItemIterator itemIt(this); itemIt.current(); ++itemIt) {
                PlaylistItem *item = static_cast<PlaylistItem *>(itemIt.current());
                if(base == item->collectionItem()) {
                    item->setPlaying(true, setMaster);
		    PlaylistItemList playing = PlaylistItem::playingItems();
		    TrackSequenceManager::instance()->setCurrent(item);
                    return;
                }
            }
            return;
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

void Playlist::copy()
{
    tdeApp->clipboard()->setData(dragObject(0), TQClipboard::Clipboard);
}

void Playlist::paste()
{
    decode(tdeApp->clipboard()->data(), static_cast<PlaylistItem *>(currentItem()));
}

void Playlist::clear()
{
    PlaylistItemList l = selectedItems();
    if(l.isEmpty())
	l = items();

    clearItems(l);
}

void Playlist::slotRefresh()
{
    PlaylistItemList l = selectedItems();
    if(l.isEmpty())
	l = visibleItems();

    TDEApplication::setOverrideCursor(TQt::waitCursor);
    for(PlaylistItemList::Iterator it = l.begin(); it != l.end(); ++it) {
	(*it)->refreshFromDisk();

	if(!(*it)->file().tag() || !(*it)->file().fileInfo().exists()) {
	    kdDebug(65432) << "Error while trying to refresh the tag.  "
			   << "This file has probably been removed."
			   << endl;
	    clearItem((*it)->collectionItem());
	}

	processEvents();
    }
    TDEApplication::restoreOverrideCursor();
}

void Playlist::slotRenameFile()
{
    FileRenamer renamer;
    PlaylistItemList items = selectedItems();

    if(items.isEmpty())
        return;

    emit signalEnableDirWatch(false);

    m_blockDataChanged = true;
    renamer.rename(items);
    m_blockDataChanged = false;
    dataChanged();

    emit signalEnableDirWatch(true);
}

void Playlist::slotViewCover()
{
    PlaylistItemList items = selectedItems();
    if (items.isEmpty())
        return;
    for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it)
        (*it)->file().coverInfo()->popup();
}

void Playlist::slotRemoveCover()
{
    PlaylistItemList items = selectedItems();
    if(items.isEmpty())
        return;
    int button = KMessageBox::warningContinueCancel(this,
						    i18n("Are you sure you want to delete these covers?"),
						    TQString(),
						    i18n("&Delete Covers"));
    if(button == KMessageBox::Continue)
	refreshAlbums(items);
}

void Playlist::slotShowCoverManager()
{
    static CoverDialog *managerDialog = 0;

    if(!managerDialog)
	managerDialog = new CoverDialog(this);

    managerDialog->show();
}

unsigned int Playlist::eligibleCoverItems(const PlaylistItemList &items)
{
    // This used to count the number of tracks with an artist and album, that
    // is not strictly required anymore.  This may prove useful in the future
    // so I'm leaving it in for now, right now we just mark every item as
    // eligible.

    return items.count();
}

void Playlist::slotAddCover(bool retrieveLocal)
{
    PlaylistItemList items = selectedItems();

    if(items.isEmpty())
        return;

    if(eligibleCoverItems(items) == 0) {
	// No items in the list can be assigned a cover, inform the user and
	// bail.

	// KDE 4.0 Fix this string.
	KMessageBox::sorry(this, i18n("None of the items you have selected can "
		    "be assigned a cover.  A track must have both the Artist "
		    "and Album tags set to be assigned a cover."));

	return;
    }

    TQPixmap newCover;

    if(retrieveLocal) {
        KURL file = KFileDialog::getImageOpenURL(
	    ":homedir", this, i18n("Select Cover Image File"));
        newCover = TQPixmap(file.directory() + "/" + file.fileName());
    }
    else {
        m_fetcher->setFile((*items.begin())->file());
        m_fetcher->chooseCover();
        return;
    }

    if(newCover.isNull())
        return;

    TQString artist = items.front()->file().tag()->artist();
    TQString album = items.front()->file().tag()->album();

    coverKey newId = CoverManager::addCover(newCover, artist, album);
    refreshAlbums(items, newId);
}

// Called when image fetcher has added a new cover.
void Playlist::slotCoverChanged(int coverId)
{
    kdDebug(65432) << "Refreshing information for newly changed covers.\n";
    refreshAlbums(selectedItems(), coverId);
}

void Playlist::slotGuessTagInfo(TagGuesser::Type type)
{
    TDEApplication::setOverrideCursor(TQt::waitCursor);
    PlaylistItemList items = selectedItems();
    setDynamicListsFrozen(true);

    m_blockDataChanged = true;

    for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) {
        (*it)->guessTagInfo(type);
	processEvents();
    }

    // MusicBrainz queries automatically commit at this point.  What would
    // be nice is having a signal emitted when the last query is completed.

    if(type == TagGuesser::FileName)
	TagTransactionManager::instance()->commit();

    m_blockDataChanged = false;

    dataChanged();
    setDynamicListsFrozen(false);
    TDEApplication::restoreOverrideCursor();
}

void Playlist::slotReload()
{
    TQFileInfo fileInfo(m_fileName);
    if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable())
	return;

    clearItems(items());
    loadFile(m_fileName, fileInfo);
}

void Playlist::slotWeightDirty(int column)
{
    if(column < 0) {
	m_weightDirty.clear();
	for(int i = 0; i < columns(); i++) {
	    if(isColumnVisible(i))
		m_weightDirty.append(i);
	}
	return;
    }

    if(m_weightDirty.find(column) == m_weightDirty.end())
	m_weightDirty.append(column);
}

void Playlist::slotShowPlaying()
{
    if(!playingItem())
	return;

    Playlist *l = playingItem()->playlist();

    l->clearSelection();

    // Raise the playlist before selecting the items otherwise the tag editor
    // will not update when it gets the selectionChanged() notification
    // because it will think the user is choosing a different playlist but not
    // selecting a different item.

    m_collection->raise(l);

    l->setSelected(playingItem(), true);
    l->setCurrentItem(playingItem());
    l->ensureItemVisible(playingItem());
}

void Playlist::slotColumnResizeModeChanged()
{
    if(manualResize())
	setHScrollBarMode(Auto);
    else
	setHScrollBarMode(AlwaysOff);

    if(!manualResize())
	slotUpdateColumnWidths();

    SharedSettings::instance()->sync();
}

void Playlist::dataChanged()
{
    if(m_blockDataChanged)
	return;
    PlaylistInterface::dataChanged();
}

////////////////////////////////////////////////////////////////////////////////
// protected members
////////////////////////////////////////////////////////////////////////////////

void Playlist::removeFromDisk(const PlaylistItemList &items)
{
    if(isVisible() && !items.isEmpty()) {

	TQStringList files;
	for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it)
            files.append((*it)->file().absFilePath());

	DeleteDialog dialog(this);

	m_blockDataChanged = true;

	if(dialog.confirmDeleteList(files)) {
	    bool shouldDelete = dialog.shouldDelete();
	    TQStringList errorFiles;

	    for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) {
		if(playingItem() == *it)
		    action("forward")->activate();

		TQString removePath = (*it)->file().absFilePath();
		if((!shouldDelete && TDEIO::NetAccess::synchronousRun(TDEIO::trash(removePath), this)) ||
		   (shouldDelete && TQFile::remove(removePath)))
		{
		    CollectionList::instance()->clearItem((*it)->collectionItem());
		}
		else
		    errorFiles.append((*it)->file().absFilePath());
	    }

	    if(!errorFiles.isEmpty()) {
		TQString errorMsg = shouldDelete ? 
			i18n("Could not delete these files") :
			i18n("Could not move these files to the Trash");
		KMessageBox::errorList(this, errorMsg, errorFiles);
	    }
	}

	m_blockDataChanged = false;

	dataChanged();
    }
}

TQDragObject *Playlist::dragObject(TQWidget *parent)
{
    PlaylistItemList items = selectedItems();
    KURL::List urls;
    for(PlaylistItemList::Iterator it = items.begin(); it != items.end(); ++it) {
	KURL url;
	url.setPath((*it)->file().absFilePath());
	urls.append(url);
    }

    KURLDrag *drag = new KURLDrag(urls, parent, "Playlist Items");
    drag->setPixmap(BarIcon("audio-x-generic"));

    return drag;
}

void Playlist::contentsDragEnterEvent(TQDragEnterEvent *e)
{
    TDEListView::contentsDragEnterEvent(e);

    if(CoverDrag::canDecode(e)) {
	setDropHighlighter(true);
	setDropVisualizer(false);

	e->accept();
	return;
    }

    setDropHighlighter(false);
    setDropVisualizer(true);

    KURL::List urls;
    if(!KURLDrag::decode(e, urls) || urls.isEmpty()) {
	e->ignore();
	return;
    }

    e->accept();
    return;
}

bool Playlist::acceptDrag(TQDropEvent *e) const
{
    return CoverDrag::canDecode(e) || KURLDrag::canDecode(e);
}

bool Playlist::canDecode(TQMimeSource *s)
{
    KURL::List urls;

    if(CoverDrag::canDecode(s))
	return true;

    return KURLDrag::decode(s, urls) && !urls.isEmpty();
}

void Playlist::decode(TQMimeSource *s, PlaylistItem *item)
{
    KURL::List urls;

    if(!KURLDrag::decode(s, urls) || urls.isEmpty())
	return;

    // handle dropped images

    if(!MediaFiles::isMediaFile(urls.front().path())) {

	TQString file;

	if(urls.front().isLocalFile())
	    file = urls.front().path();
	else
	    TDEIO::NetAccess::download(urls.front(), file, 0);

	KMimeType::Ptr mimeType = KMimeType::findByPath(file);

	if(item && mimeType->name().startsWith("image/")) {
	    item->file().coverInfo()->setCover(TQImage(file));
	    refreshAlbum(item->file().tag()->artist(),
			 item->file().tag()->album());
	}

        TDEIO::NetAccess::removeTempFile(file);
    }

    TQStringList fileList;

    for(KURL::List::Iterator it = urls.begin(); it != urls.end(); ++it)
	fileList += MediaFiles::convertURLsToLocal((*it).path(), this);

    addFiles(fileList, item);
}

bool Playlist::eventFilter(TQObject *watched, TQEvent *e)
{
    if(watched == header()) {
	switch(e->type()) {
	case TQEvent::MouseMove:
	{
	    if((static_cast<TQMouseEvent*>(e)->state() & TQt::LeftButton) == TQt::LeftButton &&
		!action<TDEToggleAction>("resizeColumnsManually")->isChecked())
	    {
		m_columnWidthModeChanged = true;

		action<TDEToggleAction>("resizeColumnsManually")->setChecked(true);
		slotColumnResizeModeChanged();
	    }

	    break;
	}
	case TQEvent::MouseButtonPress:
	{
	    if(static_cast<TQMouseEvent*>(e)->button() == TQt::RightButton)
		m_headerMenu->popup(TQCursor::pos());

	    break;
	}
	case TQEvent::MouseButtonRelease:
	{
	    if(m_columnWidthModeChanged) {
		m_columnWidthModeChanged = false;
		notifyUserColumnWidthModeChanged();
	    }

	    if(!manualResize() && m_widthsDirty)
		TQTimer::singleShot(0, this, TQ_SLOT(slotUpdateColumnWidths()));
	    break;
	}
	default:
	    break;
	}
    }

    return TDEListView::eventFilter(watched, e);
}

void Playlist::keyPressEvent(TQKeyEvent *event)
{
    if(event->key() == Key_Up) {
	TQListViewItemIterator selected(this, TQListViewItemIterator::IteratorFlag(
					   TQListViewItemIterator::Selected |
					   TQListViewItemIterator::Visible));
	if(selected.current()) {
	    TQListViewItemIterator visible(this, TQListViewItemIterator::IteratorFlag(
					      TQListViewItemIterator::Visible));
	    if(selected.current() == visible.current())
		TDEApplication::postEvent(parent(), new FocusUpEvent);
	}
	
    }

    TDEListView::keyPressEvent(event);
}

void Playlist::contentsDropEvent(TQDropEvent *e)
{
    TQPoint vp = contentsToViewport(e->pos());
    PlaylistItem *item = static_cast<PlaylistItem *>(itemAt(vp));

    // First see if we're dropping a cover, if so we can get it out of the
    // way early.
    if(item && CoverDrag::canDecode(e)) {
	coverKey id;
	CoverDrag::decode(e, id);

	// If the item we dropped on is selected, apply cover to all selected
	// items, otherwise just apply to the dropped item.

	if(item->isSelected()) {
	    PlaylistItemList selItems = selectedItems();
	    for(PlaylistItemList::Iterator it = selItems.begin(); it != selItems.end(); ++it) {
		(*it)->file().coverInfo()->setCoverId(id);
		(*it)->refresh();
	    }
	}
	else {
	    item->file().coverInfo()->setCoverId(id);
	    item->refresh();
	}
	
	return;
    }

    // When dropping on the upper half of an item, insert before this item.
    // This is what the user expects, and also allows the insertion at
    // top of the list

    if(!item)
	item = static_cast<PlaylistItem *>(lastItem());
    else if(vp.y() < item->itemPos() + item->height() / 2)
	item = static_cast<PlaylistItem *>(item->itemAbove());

    m_blockDataChanged = true;

    if(e->source() == this) {

	// Since we're trying to arrange things manually, turn off sorting.

	setSorting(columns() + 1);

	TQPtrList<TQListViewItem> items = TDEListView::selectedItems();

	for(TQPtrListIterator<TQListViewItem> it(items); it.current(); ++it) {
	    if(!item) {

		// Insert the item at the top of the list.  This is a bit ugly,
		// but I don't see another way.

		takeItem(it.current());
		insertItem(it.current());
	    }
	    else
		it.current()->moveItem(item);

	    item = static_cast<PlaylistItem *>(it.current());
	}
    }
    else
	decode(e, item);

    m_blockDataChanged = false;

    dataChanged();
    emit signalPlaylistItemsDropped(this);
    TDEListView::contentsDropEvent(e);
}

void Playlist::contentsMouseDoubleClickEvent(TQMouseEvent *e)
{
    // Filter out non left button double clicks, that way users don't have the
    // weird experience of switching songs from a double right-click.

    if(e->button() == TQt::LeftButton)
	TDEListView::contentsMouseDoubleClickEvent(e);
}

void Playlist::showEvent(TQShowEvent *e)
{
    if(m_applySharedSettings) {
	SharedSettings::instance()->apply(this);
	m_applySharedSettings = false;
    }
    TDEListView::showEvent(e);
}

void Playlist::applySharedSettings()
{
    m_applySharedSettings = true;
}

void Playlist::read(TQDataStream &s)
{
    TQString buffer;

    s >> m_playlistName
      >> m_fileName;

    TQStringList files;
    s >> files;

    TQListViewItem *after = 0;

    m_blockDataChanged = true;

    for(TQStringList::ConstIterator it = files.begin(); it != files.end(); ++it)
        after = createItem(FileHandle(*it), after, false);

    m_blockDataChanged = false;

    dataChanged();
    m_collection->setupPlaylist(this, "audio-midi");
}

void Playlist::viewportPaintEvent(TQPaintEvent *pe)
{
    // If there are columns that need to be updated, well, update them.

    if(!m_weightDirty.isEmpty() && !manualResize())
    {
	calculateColumnWeights();
	slotUpdateColumnWidths();
    }

    TDEListView::viewportPaintEvent(pe);
}

void Playlist::viewportResizeEvent(TQResizeEvent *re)
{
    // If the width of the view has changed, manually update the column
    // widths.

    if(re->size().width() != re->oldSize().width() && !manualResize())
	slotUpdateColumnWidths();

    TDEListView::viewportResizeEvent(re);
}

void Playlist::insertItem(TQListViewItem *item)
{
    // Because we're called from the PlaylistItem ctor, item may not be a
    // PlaylistItem yet (it would be TQListViewItem when being inserted.  But,
    // it will be a PlaylistItem by the time it matters, but be careful if
    // you need to use the PlaylistItem from here.

    m_addTime.append(static_cast<PlaylistItem *>(item));
    TDEListView::insertItem(item);
}

void Playlist::takeItem(TQListViewItem *item)
{
    // See the warning in Playlist::insertItem.

    m_subtractTime.append(static_cast<PlaylistItem *>(item));
    TDEListView::takeItem(item);
}

void Playlist::addColumn(const TQString &label)
{
    slotWeightDirty(columns());
    TDEListView::addColumn(label, 30);
}

PlaylistItem *Playlist::createItem(const FileHandle &file,
				   TQListViewItem *after, bool emitChanged)
{
    return createItem<PlaylistItem, CollectionListItem, CollectionList>(file, after, emitChanged);
}

void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after)
{
    createItems<CollectionListItem, PlaylistItem, PlaylistItem>(siblings, after);
}

void Playlist::addFiles(const TQStringList &files, PlaylistItem *after)
{
    if(!after)
	after = static_cast<PlaylistItem *>(lastItem());

    TDEApplication::setOverrideCursor(TQt::waitCursor);

    m_blockDataChanged = true;

    FileHandleList queue;

    const TQStringList::ConstIterator filesEnd = files.end();
    for(TQStringList::ConstIterator it = files.begin(); it != filesEnd; ++it)
        addFile(*it, queue, true, &after);

    addFileHelper(queue, &after, true);

    m_blockDataChanged = false;

    slotWeightDirty();
    dataChanged();

    TDEApplication::restoreOverrideCursor();
}

void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id)
{
    TQValueList< TQPair<TQString, TQString> > albums;
    bool setAlbumCovers = items.count() == 1;

    for(PlaylistItemList::ConstIterator it = items.begin(); it != items.end(); ++it) {
	TQString artist = (*it)->file().tag()->artist();
	TQString album = (*it)->file().tag()->album();

	if(albums.find(qMakePair(artist, album)) == albums.end())
	    albums.append(qMakePair(artist, album));

	(*it)->file().coverInfo()->setCoverId(id);
	if(setAlbumCovers)
	    (*it)->file().coverInfo()->applyCoverToWholeAlbum(true);
    }

    for(TQValueList< TQPair<TQString, TQString> >::ConstIterator it = albums.begin();
	it != albums.end(); ++it)
    {
	refreshAlbum((*it).first, (*it).second);
    }
}

void Playlist::updatePlaying() const
{
    for(PlaylistItemList::ConstIterator it = PlaylistItem::playingItems().begin();
	it != PlaylistItem::playingItems().end(); ++it)
    {
	(*it)->listView()->triggerUpdate();
    }
}

void Playlist::refreshAlbum(const TQString &artist, const TQString &album)
{
    ColumnList columns;
    columns.append(PlaylistItem::ArtistColumn);
    PlaylistSearch::Component artistComponent(artist, false, columns,
					      PlaylistSearch::Component::Exact);

    columns.clear();
    columns.append(PlaylistItem::AlbumColumn);
    PlaylistSearch::Component albumComponent(album, false, columns,
					     PlaylistSearch::Component::Exact);

    PlaylistSearch::ComponentList components;
    components.append(artist);
    components.append(album);

    PlaylistList playlists;
    playlists.append(CollectionList::instance());

    PlaylistSearch search(playlists, components);
    PlaylistItemList matches = search.matchedItems();

    for(PlaylistItemList::Iterator it = matches.begin(); it != matches.end(); ++it)
	(*it)->refresh();
}

void Playlist::hideColumn(int c, bool updateSearch)
{
    m_headerMenu->setItemChecked(c, false);

    if(!isColumnVisible(c))
	return;

    setColumnWidthMode(c, Manual);
    setColumnWidth(c, 0);

    // Moving the column to the end seems to prevent it from randomly
    // popping up.

    header()->moveSection(c, header()->count());
    header()->setResizeEnabled(false, c);

    if(c == m_leftColumn) {
	updatePlaying();
	m_leftColumn = leftMostVisibleColumn();
    }

    if(!manualResize()) {
	slotUpdateColumnWidths();
	triggerUpdate();
    }

    if(this != CollectionList::instance())
	CollectionList::instance()->hideColumn(c, false);

    if(updateSearch)
	redisplaySearch();
}

void Playlist::showColumn(int c, bool updateSearch)
{
    m_headerMenu->setItemChecked(c, true);

    if(isColumnVisible(c))
	return;

    // Just set the width to one to mark the column as visible -- we'll update
    // the real size in the next call.

    if(manualResize())
	setColumnWidth(c, 35); // Make column at least slightly visible.
    else
	setColumnWidth(c, 1);

    header()->setResizeEnabled(true, c);
    header()->moveSection(c, c); // Approximate old position

    if(c == leftMostVisibleColumn()) {
	updatePlaying();
	m_leftColumn = leftMostVisibleColumn();
    }

    if(!manualResize()) {
	slotUpdateColumnWidths();
	triggerUpdate();
    }

    if(this != CollectionList::instance())
	CollectionList::instance()->showColumn(c, false);

    if(updateSearch)
	redisplaySearch();
}

bool Playlist::isColumnVisible(int c) const
{
    return columnWidth(c) != 0;
}

void Playlist::polish()
{
    TDEListView::polish();

    if(m_polished)
	return;

    m_polished = true;

    addColumn(i18n("Track Name"));
    addColumn(i18n("Artist"));
    addColumn(i18n("Album"));
    addColumn(i18n("Cover"));
    addColumn(i18n("Track"));
    addColumn(i18n("Genre"));
    addColumn(i18n("Year"));
    addColumn(i18n("Length"));
    addColumn(i18n("Bitrate"));
    addColumn(i18n("Comment"));
    addColumn(i18n("File Name"));
    addColumn(i18n("File Name (full path)"));

    setRenameable(PlaylistItem::TrackColumn, true);
    setRenameable(PlaylistItem::ArtistColumn, true);
    setRenameable(PlaylistItem::AlbumColumn, true);
    setRenameable(PlaylistItem::TrackNumberColumn, true);
    setRenameable(PlaylistItem::GenreColumn, true);
    setRenameable(PlaylistItem::YearColumn, true);

    setAllColumnsShowFocus(true);
    setSelectionMode(TQListView::Extended);
    setShowSortIndicator(true);
    setDropVisualizer(true);

    m_columnFixedWidths.resize(columns(), 0);

    //////////////////////////////////////////////////
    // setup header RMB menu
    //////////////////////////////////////////////////

    m_columnVisibleAction = new TDEActionMenu(i18n("&Show Columns"), this, "showColumns");

    m_headerMenu = m_columnVisibleAction->popupMenu();
    m_headerMenu->insertTitle(i18n("Show"));
    m_headerMenu->setCheckable(true);

    for(int i = 0; i < header()->count(); ++i) {
	if(i == PlaylistItem::FileNameColumn)
	    m_headerMenu->insertSeparator();
	m_headerMenu->insertItem(header()->label(i), i);
	m_headerMenu->setItemChecked(i, true);
	adjustColumn(i);
    }

    connect(m_headerMenu, TQ_SIGNAL(activated(int)), this, TQ_SLOT(slotToggleColumnVisible(int)));

    connect(this, TQ_SIGNAL(contextMenuRequested(TQListViewItem *, const TQPoint &, int)),
	    this, TQ_SLOT(slotShowRMBMenu(TQListViewItem *, const TQPoint &, int)));
    connect(this, TQ_SIGNAL(itemRenamed(TQListViewItem *, const TQString &, int)),
	    this, TQ_SLOT(slotInlineEditDone(TQListViewItem *, const TQString &, int)));
    connect(this, TQ_SIGNAL(doubleClicked(TQListViewItem *)),
	    this, TQ_SLOT(slotPlayCurrent()));
    connect(this, TQ_SIGNAL(returnPressed(TQListViewItem *)),
	    this, TQ_SLOT(slotPlayCurrent()));

    connect(header(), TQ_SIGNAL(sizeChange(int, int, int)),
	    this, TQ_SLOT(slotColumnSizeChanged(int, int, int)));

    connect(renameLineEdit(), TQ_SIGNAL(completionModeChanged(TDEGlobalSettings::Completion)),
	    this, TQ_SLOT(slotInlineCompletionModeChanged(TDEGlobalSettings::Completion)));

    connect(action("resizeColumnsManually"), TQ_SIGNAL(activated()),
	    this, TQ_SLOT(slotColumnResizeModeChanged()));

    if(action<TDEToggleAction>("resizeColumnsManually")->isChecked())
	setHScrollBarMode(Auto);
    else
	setHScrollBarMode(AlwaysOff);

    setAcceptDrops(true);
    setDropVisualizer(true);

    m_disableColumnWidthUpdates = false;

    setShowToolTips(false);
    m_toolTip = new PlaylistToolTip(viewport(), this);
}

void Playlist::setupItem(PlaylistItem *item)
{
    if(!m_search.isEmpty())
	item->setVisible(m_search.checkItem(item));

    if(childCount() <= 2 && !manualResize()) {
	slotWeightDirty();
	slotUpdateColumnWidths();
	triggerUpdate();
    }
}

void Playlist::setDynamicListsFrozen(bool frozen)
{
    m_collection->setDynamicListsFrozen(frozen);
}

////////////////////////////////////////////////////////////////////////////////
// protected slots
////////////////////////////////////////////////////////////////////////////////

void Playlist::slotPopulateBackMenu() const
{
    if(!playingItem())
	return;

    TDEPopupMenu *menu = action<TDEToolBarPopupAction>("back")->popupMenu();
    menu->clear();
    m_backMenuItems.clear();

    int count = 0;
    PlaylistItemList::ConstIterator it = m_history.end();

    while(it != m_history.begin() && count < 10) {
	++count;
	--it;
	int index = menu->insertItem((*it)->file().tag()->title());
	m_backMenuItems[index] = *it;
    }
}

void Playlist::slotPlayFromBackMenu(int number) const
{
    if(!m_backMenuItems.contains(number))
	return;

    TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]);
    action("forward")->activate();
}

////////////////////////////////////////////////////////////////////////////////
// private members
////////////////////////////////////////////////////////////////////////////////

void Playlist::setup()
{
    setItemMargin(3);

    connect(header(), TQ_SIGNAL(indexChange(int, int, int)), this, TQ_SLOT(slotColumnOrderChanged(int, int, int)));

    connect(m_fetcher, TQ_SIGNAL(signalCoverChanged(int)), this, TQ_SLOT(slotCoverChanged(int)));

    // Prevent list of selected items from changing while internet search is in
    // progress.
    connect(this, TQ_SIGNAL(selectionChanged()), m_fetcher, TQ_SLOT(abortSearch()));

    setSorting(1);
}

void Playlist::loadFile(const TQString &fileName, const TQFileInfo &fileInfo)
{
    TQFile file(fileName);
    if(!file.open(IO_ReadOnly))
	return;

    TQTextStream stream(&file);

    // Turn off non-explicit sorting.

    setSorting(PlaylistItem::lastColumn() + columnOffset() + 1);

    PlaylistItem *after = 0;

    m_disableColumnWidthUpdates = true;

    m_blockDataChanged = true;

    while(!stream.atEnd()) {
	TQString itemName = stream.readLine().stripWhiteSpace();

	TQFileInfo item(itemName);

	if(item.isRelative())
	    item.setFile(TQDir::cleanDirPath(fileInfo.dirPath(true) + "/" + itemName));

	if(item.exists() && item.isFile() && item.isReadable() &&
	   MediaFiles::isMediaFile(item.fileName()))
	{
	    if(after)
		after = createItem(FileHandle(item, item.absFilePath()), after, false);
	    else
		after = createItem(FileHandle(item, item.absFilePath()), 0, false);
	}
    }

    m_blockDataChanged = false;

    file.close();

    dataChanged();

    m_disableColumnWidthUpdates = false;
}

void Playlist::setPlaying(PlaylistItem *item, bool addToHistory)
{
    if(playingItem() == item)
	return;

    if(playingItem()) {
	if(addToHistory) {
	    if(playingItem()->playlist() ==
	       playingItem()->playlist()->m_collection->upcomingPlaylist())
		m_history.append(playingItem()->collectionItem());
	    else
		m_history.append(playingItem());
	}
	playingItem()->setPlaying(false);
    }

    TrackSequenceManager::instance()->setCurrent(item);
    TQByteArray data;
    tdeApp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data);

    if(!item)
	return;

    item->setPlaying(true);

    bool enableBack = !m_history.isEmpty();
    action<TDEToolBarPopupAction>("back")->popupMenu()->setEnabled(enableBack);
}

bool Playlist::playing() const
{
    return playingItem() && this == playingItem()->playlist();
}

int Playlist::leftMostVisibleColumn() const
{
    int i = 0;
    while(!isColumnVisible(header()->mapToSection(i)) && i < PlaylistItem::lastColumn())
	i++;

    return header()->mapToSection(i);
}

PlaylistItemList Playlist::items(TQListViewItemIterator::IteratorFlag flags)
{
    PlaylistItemList list;

    for(TQListViewItemIterator it(this, flags); it.current(); ++it)
	list.append(static_cast<PlaylistItem *>(it.current()));

    return list;
}

void Playlist::calculateColumnWeights()
{
    if(m_disableColumnWidthUpdates)
	return;

    PlaylistItemList l = items();
    TQValueListConstIterator<int> columnIt;

    TQValueVector<double> averageWidth(columns(), 0);
    double itemCount = l.size();

    TQValueVector<int> cachedWidth;

    // Here we're not using a real average, but averaging the squares of the
    // column widths and then using the square root of that value.  This gives
    // a nice weighting to the longer columns without doing something arbitrary
    // like adding a fixed amount of padding.

    for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it) {
	cachedWidth = (*it)->cachedWidths();
	for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt)
	    averageWidth[*columnIt] += pow(double(cachedWidth[*columnIt]), 2.0) / itemCount;
    }

    m_columnWeights.resize(columns(), -1);

    for(columnIt = m_weightDirty.begin(); columnIt != m_weightDirty.end(); ++columnIt) {
	m_columnWeights[*columnIt] = int(sqrt(averageWidth[*columnIt]) + 0.5);

	//  kdDebug(65432) << k_funcinfo << "m_columnWeights[" << *columnIt << "] == "
	//                 << m_columnWeights[*columnIt] << endl;
    }

    m_weightDirty.clear();
}

void Playlist::addFile(const TQString &file, FileHandleList &files, bool importPlaylists,
		       PlaylistItem **after)
{
    if(hasItem(file) && !m_allowDuplicates)
	return;

    processEvents();
    addFileHelper(files, after);

    // Our biggest thing that we're fighting during startup is too many stats
    // of files.  Make sure that we don't do one here if it's not needed.

    FileHandle cached = Cache::instance()->value(file);

    if(!cached.isNull()) {
	cached.tag();
	files.append(cached);
	return;
    }
    

    const TQFileInfo fileInfo = TQDir::cleanDirPath(file);
    if(!fileInfo.exists())
	return;

    if(fileInfo.isFile() && fileInfo.isReadable()) {
	if(MediaFiles::isMediaFile(file)) {
	    FileHandle f(fileInfo, fileInfo.absFilePath());
	    f.tag();
	    files.append(f);
	}
    }

    if(importPlaylists && MediaFiles::isPlaylistFile(file) &&
       !m_collection->containsPlaylistFile(fileInfo.absFilePath()))
    {
	new Playlist(m_collection, fileInfo);
	return;
    }

    if(fileInfo.isDir()) {

	// Resorting to the POSIX API because TQDir::listEntries() stats every
	// file and blocks while it's doing so.

	DIR *dir = ::opendir(TQFile::encodeName(fileInfo.filePath()));

	if(dir) {
	    struct dirent *dirEntry;

	    for(dirEntry = ::readdir(dir); dirEntry; dirEntry = ::readdir(dir)) {
		if(strcmp(dirEntry->d_name, ".") != 0 && strcmp(dirEntry->d_name, "..") != 0) {

		    // We set importPlaylists to the value from the add directories
		    // dialog as we want to load all of the ones that the user has
		    // explicitly asked for, but not those that we find in lower
		    // directories.

		    addFile(fileInfo.filePath() + TQDir::separator() + TQFile::decodeName(dirEntry->d_name),
			    files, m_collection->importPlaylists(), after);
		}
	    }
	    ::closedir(dir);
	}
	else {
	    kdWarning(65432) << "Unable to open directory "
	                     << fileInfo.filePath()
			     << ", make sure it is readable.\n";
	}
    }
}

void Playlist::addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer)
{
    static TQTime time = TQTime::currentTime();

    // Process new items every 10 seconds, when we've loaded 1000 items, or when
    // it's been requested in the API.

    if(ignoreTimer || time.elapsed() > 10000 ||
       (files.count() >= 1000 && time.elapsed() > 1000))
    {
	time.restart();

	const bool focus = hasFocus();
	const bool visible = isVisible() && files.count() > 20;

	if(visible)
	    m_collection->raiseDistraction();
	const FileHandleList::ConstIterator filesEnd = files.end();
	for(FileHandleList::ConstIterator it = files.begin(); it != filesEnd; ++it)
	    *after = createItem(*it, *after, false);
	files.clear();

	if(visible)
	    m_collection->lowerDistraction();

	if(focus)
	    setFocus();

	processEvents();
    }
}

////////////////////////////////////////////////////////////////////////////////
// private slots
////////////////////////////////////////////////////////////////////////////////

void Playlist::slotUpdateColumnWidths()
{
    if(m_disableColumnWidthUpdates || manualResize())
	return;

    // Make sure that the column weights have been initialized before trying to
    // update the columns.

    TQValueList<int> visibleColumns;
    for(int i = 0; i < columns(); i++) {
	if(isColumnVisible(i))
	    visibleColumns.append(i);
    }

    TQValueListConstIterator<int> it;

    if(count() == 0) {
	for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it)
	    setColumnWidth(*it, header()->fontMetrics().width(header()->label(*it)) + 10);

	return;
    }

    if(m_columnWeights.isEmpty())
	return;

    // First build a list of minimum widths based on the strings in the listview
    // header.  We won't let the width of the column go below this width.

    TQValueVector<int> minimumWidth(columns(), 0);
    int minimumWidthTotal = 0;

    // Also build a list of either the minimum *or* the fixed width -- whichever is
    // greater.

    TQValueVector<int> minimumFixedWidth(columns(), 0);
    int minimumFixedWidthTotal = 0;

    for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) {
	int column = *it;
	minimumWidth[column] = header()->fontMetrics().width(header()->label(column)) + 10;
	minimumWidthTotal += minimumWidth[column];

	minimumFixedWidth[column] = TQMAX(minimumWidth[column], m_columnFixedWidths[column]);
	minimumFixedWidthTotal += minimumFixedWidth[column];
    }

    // Make sure that the width won't get any smaller than this.  We have to
    // account for the scrollbar as well.  Since this method is called from the
    // resize event this will set a pretty hard lower bound on the size.

    setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width());

    // If we've got enough room for the fixed widths (larger than the minimum
    // widths) then instead use those for our "minimum widths".

    if(minimumFixedWidthTotal < visibleWidth()) {
	minimumWidth = minimumFixedWidth;
	minimumWidthTotal = minimumFixedWidthTotal;
    }

    // We've got a list of columns "weights" based on some statistics gathered
    // about the widths of the items in that column.  We need to find the total
    // useful weight to use as a divisor for each column's weight.

    double totalWeight = 0;
    for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it)
	totalWeight += m_columnWeights[*it];

    // Computed a "weighted width" for each visible column.  This would be the
    // width if we didn't have to handle the cases of minimum and maximum widths.

    TQValueVector<int> weightedWidth(columns(), 0);
    for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it)
	weightedWidth[*it] = int(double(m_columnWeights[*it]) / totalWeight * visibleWidth() + 0.5);

    // The "extra" width for each column.  This is the weighted width less the
    // minimum width or zero if the minimum width is greater than the weighted
    // width.

    TQValueVector<int> extraWidth(columns(), 0);

    // This is used as an indicator if we have any columns where the weighted
    // width is less than the minimum width.  If this is false then we can
    // just use the weighted width with no problems, otherwise we have to
    // "readjust" the widths.

    bool readjust = false;

    // If we have columns where the weighted width is less than the minimum width
    // we need to steal that space from somewhere.  The amount that we need to
    // steal is the "neededWidth".

    int neededWidth = 0;

    // While we're on the topic of stealing -- we have to have somewhere to steal
    // from.  availableWidth is the sum of the amount of space beyond the minimum
    // width that each column has been allocated -- the sum of the values of
    // extraWidth[].

    int availableWidth = 0;

    // Fill in the values discussed above.

    for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) {
	if(weightedWidth[*it] < minimumWidth[*it]) {
	    readjust = true;
	    extraWidth[*it] = 0;
	    neededWidth += minimumWidth[*it] - weightedWidth[*it];
	}
	else {
	    extraWidth[*it] = weightedWidth[*it] - minimumWidth[*it];
	    availableWidth += extraWidth[*it];
	}
    }

    // The adjustmentRatio is the amount of the "extraWidth[]" that columns will
    // actually be given.

    double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth);

    // This will be the sum of the total space that we actually use.  Because of
    // rounding error this won't be the exact available width.

    int usedWidth = 0;

    // Now set the actual column widths.  If the weighted widths are all greater
    // than the minimum widths, just use those, otherwise use the "reajusted
    // weighted width".

    for(it = visibleColumns.begin(); it != visibleColumns.end(); ++it) {
	int width;
	if(readjust) {
	    int adjustedExtraWidth = int(double(extraWidth[*it]) * adjustmentRatio + 0.5);
	    width = minimumWidth[*it] + adjustedExtraWidth;
	}
	else
	    width = weightedWidth[*it];

	setColumnWidth(*it, width);
	usedWidth += width;
    }

    // Fill the remaining gap for a clean fit into the available space.

    int remainingWidth = visibleWidth() - usedWidth;
    setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth);

    m_widthsDirty = false;
}

void Playlist::slotAddToUpcoming()
{
    m_collection->setUpcomingPlaylistEnabled(true);
    m_collection->upcomingPlaylist()->appendItems(selectedItems());
}

void Playlist::slotShowRMBMenu(TQListViewItem *item, const TQPoint &point, int column)
{
    if(!item)
	return;

    // Create the RMB menu on demand.

    if(!m_rmbMenu) {

	// A bit of a hack to get a pointer to the action collection.
	// Probably more of these actions should be ported over to using TDEActions.

	m_rmbMenu = new TDEPopupMenu(this);

	m_rmbUpcomingID = m_rmbMenu->insertItem(SmallIcon("today"),
	    i18n("Add to Play Queue"), this, TQ_SLOT(slotAddToUpcoming()));
	m_rmbMenu->insertSeparator();

	if(!readOnly()) {
	    action("edit_cut")->plug(m_rmbMenu);
	    action("edit_copy")->plug(m_rmbMenu);
	    action("edit_paste")->plug(m_rmbMenu);
	    m_rmbMenu->insertSeparator();
	    action("removeFromPlaylist")->plug(m_rmbMenu);
	}
	else
	    action("edit_copy")->plug(m_rmbMenu);

	m_rmbEditID = m_rmbMenu->insertItem(
	    i18n("Edit"), this, TQ_SLOT(slotRenameTag()));

	action("refresh")->plug(m_rmbMenu);
	action("removeItem")->plug(m_rmbMenu);

	m_rmbMenu->insertSeparator();

	action("guessTag")->plug(m_rmbMenu);
	action("renameFile")->plug(m_rmbMenu);

	action("coverManager")->plug(m_rmbMenu);

	m_rmbMenu->insertSeparator();

	m_rmbMenu->insertItem(
	    SmallIcon("folder-new"), i18n("Create Playlist From Selected Items..."), this, TQ_SLOT(slotCreateGroup()));

	K3bExporter *exporter = new K3bExporter(this);
	TDEAction *k3bAction = exporter->action();
	if(k3bAction)
	    k3bAction->plug(m_rmbMenu);
    }

    // Ignore any columns added by subclasses.

    column -= columnOffset();

    bool showEdit =
	(column == PlaylistItem::TrackColumn) ||
	(column == PlaylistItem::ArtistColumn) ||
	(column == PlaylistItem::AlbumColumn) ||
	(column == PlaylistItem::TrackNumberColumn) ||
	(column == PlaylistItem::GenreColumn) ||
	(column == PlaylistItem::YearColumn);

    if(showEdit)
	m_rmbMenu->changeItem(m_rmbEditID,
		i18n("Edit '%1'").arg(columnText(column + columnOffset())));

    m_rmbMenu->setItemVisible(m_rmbEditID, showEdit);

    // Disable edit menu if only one file is selected, and it's read-only

    FileHandle file = static_cast<PlaylistItem*>(item)->file();

    m_rmbMenu->setItemEnabled(m_rmbEditID, file.fileInfo().isWritable() ||
			      selectedItems().count() > 1);

    action("viewCover")->setEnabled(file.coverInfo()->hasCover());
    action("removeCover")->setEnabled(file.coverInfo()->hasCover());

    m_rmbMenu->popup(point);
    m_currentColumn = column + columnOffset();
}

void Playlist::slotRenameTag()
{
    // kdDebug(65432) << "Playlist::slotRenameTag()" << endl;

    // setup completions and validators

    CollectionList *list = CollectionList::instance();

    KLineEdit *edit = renameLineEdit();

    switch(m_currentColumn - columnOffset())
    {
    case PlaylistItem::ArtistColumn:
	edit->completionObject()->setItems(list->uniqueSet(CollectionList::Artists));
	break;
    case PlaylistItem::AlbumColumn:
	edit->completionObject()->setItems(list->uniqueSet(CollectionList::Albums));
	break;
    case PlaylistItem::GenreColumn:
    {
	TQStringList genreList;
	TagLib::StringList genres = TagLib::ID3v1::genreList();
	for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it)
	    genreList.append(TStringToTQString((*it)));
	edit->completionObject()->setItems(genreList);
	break;
    }
    default:
	edit->completionObject()->clear();
	break;
    }

    m_editText = currentItem()->text(m_currentColumn);

    rename(currentItem(), m_currentColumn);
}

bool Playlist::editTag(PlaylistItem *item, const TQString &text, int column)
{
    Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag());

    switch(column - columnOffset())
    {
    case PlaylistItem::TrackColumn:
	newTag->setTitle(text);
	break;
    case PlaylistItem::ArtistColumn:
	newTag->setArtist(text);
	break;
    case PlaylistItem::AlbumColumn:
	newTag->setAlbum(text);
	break;
    case PlaylistItem::TrackNumberColumn:
    {
	bool ok;
	int value = text.toInt(&ok);
	if(ok)
	    newTag->setTrack(value);
	break;
    }
    case PlaylistItem::GenreColumn:
	newTag->setGenre(text);
	break;
    case PlaylistItem::YearColumn:
    {
	bool ok;
	int value = text.toInt(&ok);
	if(ok)
	    newTag->setYear(value);
	break;
    }
    }

    TagTransactionManager::instance()->changeTagOnItem(item, newTag);
    return true;
}

void Playlist::slotInlineEditDone(TQListViewItem *, const TQString &, int column)
{
    TQString text = renameLineEdit()->text();
    bool changed = false;

    PlaylistItemList l = selectedItems();

    // See if any of the files have a tag different from the input.

    for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end() && !changed; ++it)
	if((*it)->text(column - columnOffset()) != text)
	    changed = true;

    if(!changed ||
       (l.count() > 1 && KMessageBox::warningContinueCancel(
	   0,
	   i18n("This will edit multiple files. Are you sure?"),
	   TQString(),
	   i18n("Edit"),
	   "DontWarnMultipleTags") == KMessageBox::Cancel))
    {
	return;
    }

    for(PlaylistItemList::ConstIterator it = l.begin(); it != l.end(); ++it)
	editTag(*it, text, column);

    TagTransactionManager::instance()->commit();

    CollectionList::instance()->dataChanged();
    dataChanged();
    update();
}

void Playlist::slotColumnOrderChanged(int, int from, int to)
{
    if(from == 0 || to == 0) {
	updatePlaying();
	m_leftColumn = header()->mapToSection(0);
    }

    SharedSettings::instance()->setColumnOrder(this);
}

void Playlist::slotToggleColumnVisible(int column)
{
    if(!isColumnVisible(column)) {
	int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset();
	int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset();

	if(column == fileNameColumn && isColumnVisible(fullPathColumn)) {
	    hideColumn(fullPathColumn, false);
	    SharedSettings::instance()->toggleColumnVisible(fullPathColumn);
	}
	if(column == fullPathColumn && isColumnVisible(fileNameColumn)) {
	    hideColumn(fileNameColumn, false);
	    SharedSettings::instance()->toggleColumnVisible(fileNameColumn);
	}
    }

    if(isColumnVisible(column))
	hideColumn(column);
    else
	showColumn(column);

    SharedSettings::instance()->toggleColumnVisible(column - columnOffset());
}

void Playlist::slotCreateGroup()
{
    TQString name = m_collection->playlistNameDialog(i18n("Create New Playlist"));

    if(!name.isEmpty())
	new Playlist(m_collection, selectedItems(), name);
}

void Playlist::notifyUserColumnWidthModeChanged()
{
    KMessageBox::information(this,
			     i18n("Manual column widths have been enabled.  You can "
				  "switch back to automatic column sizes in the view "
				  "menu."),
			     i18n("Manual Column Widths Enabled"),
			     "ShowManualColumnWidthInformation");
}

void Playlist::slotColumnSizeChanged(int column, int, int newSize)
{
    m_widthsDirty = true;
    m_columnFixedWidths[column] = newSize;
}

void Playlist::slotInlineCompletionModeChanged(TDEGlobalSettings::Completion mode)
{
    SharedSettings::instance()->setInlineCompletionMode(mode);
}

void Playlist::slotPlayCurrent()
{
    TQListViewItemIterator it(this, TQListViewItemIterator::Selected);
    PlaylistItem *next = static_cast<PlaylistItem *>(it.current());
    TrackSequenceManager::instance()->setNextItem(next);
    action("forward")->activate();
}

////////////////////////////////////////////////////////////////////////////////
// helper functions
////////////////////////////////////////////////////////////////////////////////

TQDataStream &operator<<(TQDataStream &s, const Playlist &p)
{
    s << p.name();
    s << p.fileName();
    s << p.files();

    return s;
}

TQDataStream &operator>>(TQDataStream &s, Playlist &p)
{
    p.read(s);
    return s;
}

bool processEvents()
{
    static TQTime time = TQTime::currentTime();

    if(time.elapsed() > 100) {
	time.restart();
	tdeApp->processEvents();
	return true;
    }
    return false;
}

#include "playlist.moc"
