/***************************************************************************
	kjloader.cpp  -  The KJöfol-GUI itself
	--------------------------------------
	Maintainer: Stefan Gehn <sgehn@gmx.net>

 ***************************************************************************/

// local includes
#include "kjloader.h"
#include "kjloader.moc"
#include "kjwidget.h"
#include "kjbackground.h"
#include "kjbutton.h"
#include "kjfont.h"
#include "kjseeker.h"
#include "kjsliders.h"
#include "kjtextdisplay.h"
#include "kjvis.h"
#include "kjprefs.h"
#include "kjequalizer.h"

#include "helpers.cpp"

// arts-includes, needed for pitch
#include <artsmodules.h>
#include <reference.h>
#include <soundserver.h>
#include <kmedia2.h>

// noatun-specific includes
#include <noatun/engine.h>
#include <noatunarts/noatunarts.h>
#include <noatun/stdaction.h>
#include <noatun/app.h>
#include <noatun/player.h>
#include <noatun/vequalizer.h>

// system includes
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <unistd.h>

#include <qdragobject.h>
#include <qimage.h>
#include <qbitmap.h>
#include <qpixmap.h>
#include <qcursor.h>
#include <qpainter.h>
#include <qtooltip.h>
#include <qptrvector.h>
#include <qvbox.h>
#include <qlabel.h>

#include <kaction.h>
#include <kdebug.h>
#include <kfiledialog.h>
#include <khelpmenu.h>
#include <kstdaction.h>
#include <kpopupmenu.h>
#include <klocale.h>
#include <kglobalsettings.h>
#include <kglobal.h>
#include <kstandarddirs.h>
#include <knotifyclient.h>
#include <kpixmapeffect.h>
#include <kurldrag.h>

#include <kwin.h>
#include <kiconloader.h>

class KJToolTip : public QToolTip
{
public:
	KJToolTip(KJLoader *parent)
		: QToolTip(parent), mParent(parent)
	{}

protected:
	virtual void maybeTip(const QPoint &p)
	{
		if ( !mParent->prefs()->displayTooltips() )
			return;

		QPtrList<KJWidget> things=mParent->widgetsAt(p);
		for (KJWidget *i=things.first(); i!=0; i=things.next())
		{
			QString string=i->tip();
			if (string.length())
			{
				tip(i->rect(), string);
				return;
			}
		}
	}

private:
	KJLoader *mParent;
};


KJLoader *KJLoader::kjofol=0;


KJLoader::KJLoader()
	: QWidget(0,"KJLoader", WType_TopLevel | WStyle_NoBorder | WRepaintNoErase ),
	  UserInterface(), moving(false), mClickedIn(0), mText(0), mNumbers(0), mVolumeFont(0), mPitchFont(0), splashScreen(0)
{
	kjofol = this;

	mTooltips = new KJToolTip(this);

	// Windowname and Icon
	setCaption ( i18n("Noatun") );
	setIcon ( SmallIcon("noatun") );
	setAcceptDrops ( true );

	// We're going to draw over everything, there is no point in drawing the grey background first
	setBackgroundMode(NoBackground);

	// used for dockmode
	mWin = new KWinModule();

	subwidgets.setAutoDelete(true);

	mPrefs = new KJPrefs(this);
	connect ( mPrefs, SIGNAL(configChanged()), this, SLOT(readConfig()) );

	QString skin = mPrefs->skin();
	if ( QFile(skin).exists() )
	{
		loadSkin(skin);
	}
	else
	{
		KNotifyClient::event("warning",
			i18n("There was trouble loading skin %1. Please select another skin file.").arg(skin));
		napp->preferences();
	}

	mHelpMenu = new KHelpMenu(this, kapp->aboutData());
	connect(napp->player(), SIGNAL(timeout()), SLOT(timeUpdate()));
	connect(napp->player(), SIGNAL(stopped()), SLOT(timeUpdate()));
	connect(napp->player(), SIGNAL(newSong()), SLOT(newSong()));

	connect(napp, SIGNAL(hideYourself()), SLOT(hide()));
	connect(napp, SIGNAL(showYourself()), SLOT(show()));
//	KStdAction::quit(napp, SLOT(quit()), actionCollection());

	QApplication::restoreOverrideCursor();
//	newSong();
}

QPtrList<KJWidget> KJLoader::widgetsAt(const QPoint &pt) const
{
	QPtrList<KJWidget> things;
	for ( QPtrListIterator<KJWidget> i(subwidgets); i.current(); ++i )
		if ( (*i)->rect().contains(pt) )
			things.append((*i));

	return things;
}

void KJLoader::removeChild(KJWidget *c)
{
	if ( mClickedIn == c )
		mClickedIn = 0;
	if (subwidgets.findRef(c) != -1)
		subwidgets.take();
}

void KJLoader::addChild(KJWidget *c)
{
	subwidgets.append(c);
}

// The BIG one ;)
// this methode does all the hard work on loading these weird skins
void KJLoader::loadSkin(const QString &file)
{
//	kdDebug(66666) << "<KJLoader::loadSkin(const QString &file)>" << endl;
//	kdDebug(66666) << " file = " << file.latin1() << endl;

	if ( file == mCurrentSkin ) // we don't load the same skin again
		return;

	mCurrentSkin = file;

	// don't overwrite path to *.rc when we are loading dock- or winshade-mode
	if ( (file != mCurrentWinshadeModeSkin) && (file != mCurrentDockModeSkin) )
	{
		mCurrentDefaultSkin = file;
//		kdDebug(66666) << " setting mCurrentDefaultSkin: '" << file.latin1() << "'" << endl;
	}
	unloadSkin();

	Parser::open( filenameNoCase(file) );

	KJPitchText *pitchText=0;
	KJVolumeText *volumeText=0;
	mText = 0;
	mNumbers = 0;
	mVolumeFont = 0;
	mPitchFont = 0;

	if ( exist("splashscreen") && mPrefs->displaySplash() )
		showSplash();

	if ( (file != mCurrentWinshadeModeSkin) && (file != mCurrentDockModeSkin) )
	{
		if ( exist("dockmodercfile") )
		{
			// set path to dockmode rc-file (its not always skinname.dck)
			mCurrentDockModeSkin = file.left(file.findRev("/")+1) + (item("dockmodercfile")[1]);
			mDockPosition  = item("dockmodeposition")[1].toInt();
			mDockPositionX = item("dockmodepositionxy")[1].toInt();
			mDockPositionY = item("dockmodepositionxy")[2].toInt();
		}
		else	// NO DockMode
			mCurrentDockModeSkin="";

		if ( exist("winshademodercfile") )
			mCurrentWinshadeModeSkin = file.left(file.findRev("/")+1) + (item("winshademodercfile")[1]);
		else	// no WinshadeMode
			mCurrentWinshadeModeSkin="";
	}

	// Font-loading
	if ( exist("fontimage") )
		mText = new KJFont("font", this);

	if ( exist("timefontimage") )
		mNumbers = new KJFont("timefont", this);

	if (exist("volumefontimage"))
		mVolumeFont = new KJFont("volumefont", this);

	// our skin-background, There has to be one so no check with exist()
	subwidgets.append( new KJBackground(this) );

	if ( exist("pitchtext") )
	{
		if (exist("pitchfontimage"))
		{
			mPitchFont = new KJFont("pitchfont", this);
		}
		else
		{
			mPitchFont = mNumbers;
			kdDebug(66666) << "no pitchfont but pitchtext!" << endl;
			kdDebug(66666) << "replacing pitchfont with timefont" << endl;
		}
		subwidgets.append( pitchText=new KJPitchText(item("pitchtext"), this) );
	}

	if (exist("volumetext"))
		subwidgets.append(volumeText=new KJVolumeText(item("volumetext"), this));

	if ( exist("volumecontroltype") )
	{
		if ( item("volumecontroltype")[1] == "bmp" )
		{
			KJVolumeBMP *b;
			subwidgets.append(b=new KJVolumeBMP(item("volumecontrolbutton"), this));
			b->setText(volumeText);
		}
		else if ( item("volumecontroltype")[1] == "bar" )
		{
			KJVolumeBar *b;
			subwidgets.append(b=new KJVolumeBar(item("volumecontrolbutton"), this));
			b->setText(volumeText);
		}
/*		else
		{
			kdDebug(66666) << "unknown volumecontrol: " << item("volumecontroltype")[1].latin1() << endl;
		} */
	}
	else
	{
		kdDebug(66666) << "guessing type of volumecontrol" << endl;
		if (exist("volumecontrolbutton") &&
			exist("volumecontrolimage") &&
			exist("volumecontrolimagexsize") &&
			exist("volumecontrolimageposition") &&
			exist("volumecontrolimagenb") )
		{
			KJVolumeBMP *b;
			subwidgets.append(b=new KJVolumeBMP(item("volumecontrolbutton"), this));
			b->setText(volumeText);
		}
		else if (exist("volumecontrolimage") &&
				exist("volumecontrolbutton") )
		{
			KJVolumeBar *b;
			subwidgets.append(b=new KJVolumeBar(item("volumecontrolbutton"), this));
			b->setText(volumeText);
		}
/*		else
		{
			kdDebug(66666) << " no volumecontrol" << endl;
		} */
	}

	if (exist("pitchcontrolbutton") &&
		exist("pitchcontrolimage") &&
		exist("pitchcontrolimagexsize") &&
		exist("pitchcontrolimageposition") &&
		exist("pitchcontrolimagenb") )
	{
//		kdDebug(66666) << "added KJPitchBMP" << endl;
		KJPitchBMP *b;
		subwidgets.append(b=new KJPitchBMP(item("pitchcontrolbutton"), this));
		b->setText(pitchText);
	}
	else
	{
		// make sure we reset speed to 100% as the user won't be able
		// to reset it without a pitchcontrol
		Arts::PlayObject playobject = napp->player()->engine()->playObject();
		Arts::PitchablePlayObject pitchable = Arts::DynamicCast(playobject);

		if ( !pitchable.isNull() )
		{
			if ( pitchable.speed() > 1.0f )
				pitchable.speed(1.0f);
		}
	}

	if (exist("filenamewindow"))
		subwidgets.append(new KJFilename(item("filenamewindow"), this));

	if (exist("mp3timewindow"))
		subwidgets.append(new KJTime(item("mp3timewindow"), this));

	if (exist("mp3kbpswindow"))
		subwidgets.append(new KJFileInfo(item("mp3kbpswindow"), this));

	if (exist("mp3khzwindow"))
		subwidgets.append(new KJFileInfo(item("mp3khzwindow"), this));

	if (exist("analyzerwindow"))
	{
		int vistype = mPrefs->visType();
		switch ( vistype )
		{
			case KJVisScope::Null:
				subwidgets.append(new KJNullScope(item("analyzerwindow"), this));
				break;
			case KJVisScope::FFT:
				subwidgets.append(new KJVis(item("analyzerwindow"), this));
				break;
			case KJVisScope::Mono:
				subwidgets.append(new KJScope(item("analyzerwindow"), this));
				break;
		}
	}

	if (exist("EqualizerWindow"))
		subwidgets.append(new KJEqualizer(item("EqualizerWindow"), this));

	// I cant believe it, there are skins without a seeker, now THATS stupid :)
	if (exist("seekregion"))
		QTimer::singleShot(0, this, SLOT(loadSeeker()));

	// all the regular buttons
	for (QDictIterator<QStringList> i(*this); i.current(); ++i)
	{
		QString d=i.currentKey();
		if(d.contains("button") &&
			!d.startsWith("playlistwindow") && // don't add buttons that belong to the playlistwindow
			d != "pitchcontrolbutton" &&  // both already handled above as they aren't buttons but sliders
			d != "volumecontrolbutton" &&
			d != "spectrumanalyzerbutton" && // FIXME: unsupported button
			d != "oscilloscopebutton" && // FIXME: unsupported button
			i.count() >= 7 )
		{
			subwidgets.append(new KJButton(*(*i), this));
		}
	}

	show();
	conserveMemory();

	repaint();

	// update displays if we are already playing
	// This happens while changing skins
	if (napp->player()->isPlaying())
		newSong();

//	kdDebug(66666) << "</KJLoader::loadSkin(const QString &file)>" << endl;
}

void KJLoader::loadSeeker()
{
	subwidgets.append(new KJSeeker(item("seekregion"), this));
}

void KJLoader::unloadSkin()
{
//	kdDebug(66666) << "<KJLoader::unloadSkin()>" << endl;

	KWin::clearState(winId(), NET::SkipTaskbar);

//	kdDebug(66666) << " freeing subwidgets" << endl;
	subwidgets.clear();

	// This is special because mPitchfont can also point to mNumbers
	// as some skins use the NumberFont for pitchtext
	if ( mPitchFont && mPitchFont != mNumbers )
	{
//		kdDebug(66666) << " freeing mPitchFont" << endl;
		delete mPitchFont;
	}

	if ( mText )
	{
//		kdDebug(66666) << " freeing mText" << endl;
		delete mText;
	}

	if ( mNumbers )
	{
//		kdDebug(66666) << " freeing mNumbers" << endl;
		delete mNumbers;
	}

	if ( mVolumeFont )
	{
//		kdDebug(66666) << " freeing mVolumeFont" << endl;
		delete mVolumeFont;
	}

//	kdDebug(66666) << "</KJLoader::unloadSkin()>" << endl;
}

void KJLoader::minimize()
{
//	kdDebug(66666) << "KJLoader::minimize()" << endl;
	showMinimized();
}

void KJLoader::closeEvent(QCloseEvent*)
{
//	kdDebug(66666) << "KJLoader::closeEvent(QCloseEvent*)" << endl;
	unload();
}

void KJLoader::dragEnterEvent(QDragEnterEvent *event)
{
	// accept uri drops only
	event->accept(KURLDrag::canDecode(event));
}

void KJLoader::dropEvent(QDropEvent *event)
{
	KURL::List urls;
	if ( KURLDrag::decode(event,urls) )
	{
		for ( KURL::List::iterator it = urls.begin(); it != urls.end(); ++it )
			napp->player()->openFile((*it), false);
	}
}

void KJLoader::wheelEvent(QWheelEvent *e)
{	// from QT-Docu: delta() is 120 for one step
	if (e->state() & ControlButton)
		napp->player()->setVolume ( napp->player()->volume() + (e->delta()/8) ); // 15% volumechange
	else
		napp->player()->setVolume ( napp->player()->volume() + (e->delta()/24) ); // 5% volumechange
}

// now for some dockmode stuff
void KJLoader::switchToDockmode()
{
//	kdDebug(66666) << "KJLoader::switchToDockmode()" << endl;

	loadSkin( mCurrentDockModeSkin );

	KWin::setState(winId(), NET::SkipTaskbar);

	connect(mWin, SIGNAL(activeWindowChanged(WId)), this, SLOT(slotWindowActivate(WId)));
	connect(mWin, SIGNAL(windowRemoved(WId)), this, SLOT(slotWindowRemove(WId)));
	connect(mWin, SIGNAL(stackingOrderChanged()), this, SLOT(slotStackingChanged()));
	connect(mWin, SIGNAL(windowChanged(WId)), this, SLOT(slotWindowChange(WId)));
	connect(mWin, SIGNAL(currentDesktopChanged(int)), this, SLOT(slotDesktopChange(int)));

	if (mWin->activeWindow() && (mWin->activeWindow() != winId()) )
	{
		mDockToWin = mWin->activeWindow();
		mDockWindowRect = KWin::info(mDockToWin).frameGeometry;
		slotWindowActivate(mDockToWin);
		hide();
		restack();
	}

}

void KJLoader::returnFromDockmode()
{
//	kdDebug(66666) << "KJLoader::returnFromDockmode()" << endl;

	mWin->disconnect();

	loadSkin( mCurrentDefaultSkin );
}

void KJLoader::slotWindowActivate(WId win)
{
//	kdDebug(66666) << "START slotWindowActivate()" << endl;

	if ( mCurrentSkin != mCurrentDockModeSkin )
		return;

	// ensure we dock to the active window _except_ our own
	// and stick to the last window if the NEW current one is a desktop
	if ( (win != winId()) && (KWin::info(win).windowType != NET::Desktop) )
		mDockToWin = win;

	if ( mDockToWin )
	{
		mDockWindowRect = KWin::info(mDockToWin).frameGeometry;
		switch ( mDockPosition )
		{
			case 0:
				move( mDockWindowRect.x() + mDockPositionX, mDockWindowRect.y() + mDockPositionY );
				break;
			case 2:
				move( mDockWindowRect.x() + mDockPositionX, mDockWindowRect.y() + mDockWindowRect.height() + mDockPositionY );
				break;
		}

		if ( !isVisible() )
			show();
		restack();
	}
	else
	{
		// We don't want to do anything until a window comes into
		// focus.
//		kdDebug(66666) << "no window having focus" << endl;
		hide();
	}

//	kdDebug(66666) << "END slotWindowActivate()" << endl;
}

void KJLoader::slotWindowRemove(WId win)
{
//	kdDebug(66666) << "START slotWindowRemove()" << endl;
	if ( mCurrentSkin != mCurrentDockModeSkin )
		return;

	if (win == mDockToWin)
	{
//		kdDebug(66666) << "our window removed: " << win << endl;
		hide();
		mDockToWin = 0L;
	}
//	kdDebug(66666) << "END slotWindowRemove()" << endl;
}

void KJLoader::slotWindowChange(WId win)
{
//	kdDebug(66666) << "START slotWindowChange()" << endl;
	if ( mCurrentSkin != mCurrentDockModeSkin )
		return;

	if ( win == mDockToWin )
	{
//		kdDebug(66666) << "changed our window:" << win << endl;
		KWin::Info info = KWin::info( mDockToWin );
		if (info.isIconified() || info.mappingState == NET::Withdrawn || info.windowType == NET::Desktop)
		{
//			kdDebug(66666) << "[KJLoader] changed our window:" << win << ". either iconified or desktop" << endl;
			// target-window has been iconified or window is desktop
			hide();
			mDockToWin = 0L;
		}
		else
		{
			// Size or position of target-window changed.
			mDockWindowRect = info.frameGeometry;
			// make sure we are still on the window.
			switch ( mDockPosition )
			{
				case 0:
					move( mDockWindowRect.x() + mDockPositionX, mDockWindowRect.y() + mDockPositionY );
					break;
				case 2:
					move( mDockWindowRect.x() + mDockPositionX, mDockWindowRect.y() + mDockWindowRect.height() + mDockPositionY );
					break;
			}
			restack();
		}
	}
//	kdDebug(66666) << "END slotWindowChange()" << endl;
}

void KJLoader::slotDesktopChange(int)
{
//	kdDebug(66666) << "START slotDesktopChange()" << endl;
	if ( mCurrentSkin != mCurrentDockModeSkin )
		return;

	hide();
	mDockToWin = 0L;
//	kdDebug(66666) << "END slotDesktopChange()" << endl;
}

void KJLoader::slotStackingChanged()
{
//	kdDebug(66666) << "START slotStackingChanged()" << endl;
	if ( mCurrentSkin != mCurrentDockModeSkin )
		return;

	// We seem to get this signal before the window has been restacked,
	// so we just schedule a restack.
	QTimer::singleShot ( 20, this, SLOT(restack()) );

//	kdDebug(66666) << "END slotStackingChanged()" << endl;
}

// Set the animation's stacking order to be just above the target window's
// window decoration, or on top.
void KJLoader::restack()
{
//	kdDebug(66666) << "START restack()" << endl;

	if ( !mDockToWin )
	{
//		kdDebug(66666) << "No window to dock to, no restacking" << endl;
		hide();
		return;
	}

	// simply raise ourselves to the top
	raise();
	// and then ensure our target-window gets focus
//	NET::setActiveWindow (mDockToWin);

//	kdDebug(66666) << "END restack()" << endl;
}

KJLoader::~KJLoader()
{
//	kdDebug(66666) << "KJLoader::~KJLoader()" << endl;
//	kdDebug(66666) << "freeing mHelpMenu" << endl;
//	delete mHelpMenu;
//	kdDebug(66666) << "freeing mTooltips" << endl;
//	delete mTooltips;
//	kdDebug(66666) << "freeing mWin" << endl;
	delete mWin;
}

void KJLoader::paintEvent(QPaintEvent *e)
{
	QPainter p(this);
	for (KJWidget* i=subwidgets.first(); i!=0; i=subwidgets.next())
		if (i->rect().intersects(e->rect()))
			i->paint(&p, e->rect().intersect(i->rect()));
//	QWidget::paintEvent(e);
}

void KJLoader::mouseMoveEvent(QMouseEvent *e)
{
	if (moving)
	{
		move ( QCursor::pos()-mMousePoint );
		return;
	}

//	QWidget::mouseMoveEvent(e);
	// not on background but on a widget: pass event to subwidget
	if ( !moving && mClickedIn && subwidgets.findRef(mClickedIn) != -1 )
	{
		mClickedIn->mouseMove (
			e->pos()-mClickedIn->rect().topLeft(),
			mClickedIn->rect().contains(mapFromGlobal(QCursor::pos())) );
	}
}

void KJLoader::mousePressEvent(QMouseEvent *e)
{
//	kdDebug(66666) << "KJLoader::mousePressEvent(QMouseEvent *e)" << endl;

//	QWidget::mousePressEvent(e);

	if ( e->button()==RightButton )
		NoatunStdAction::ContextMenu::showContextMenu();
	else /* if ( e->button()==LeftButton ) */
	{
		mMousePoint = mapFromGlobal(QCursor::pos());
		// try to find a KJWidget that is here
		for (KJWidget* i=subwidgets.first(); i!=0; i=subwidgets.next())
			if (i->rect().contains(mMousePoint))
			{
				if (i->mousePress(mMousePoint-i->rect().topLeft()))
				{
					mClickedIn=i;
					return;
				}
			}
		// can't find a widget, so move the window
		if ( mCurrentSkin != mCurrentDockModeSkin)
			moving = true;
	}
}

void KJLoader::mouseReleaseEvent(QMouseEvent */*e*/)
{
//	kdDebug(66666) << "KJLoader::mouseReleaseEvent(QMouseEvent *e)" << endl;

//	QWidget::mouseReleaseEvent(e);

	if (!moving && mClickedIn && subwidgets.findRef(mClickedIn)!=-1)
	{
		mClickedIn->mouseRelease(mapFromGlobal(QCursor::pos())-
		                                       mClickedIn->rect().topLeft(),
		                         mClickedIn->rect().contains(
			                         mapFromGlobal(QCursor::pos())));
		mClickedIn=0;
	}

	moving = false;
}

void KJLoader::timeUpdate()
{
	for (KJWidget* widget=subwidgets.first(); widget; widget=subwidgets.next())
		widget->timeUpdate(napp->player()->getTime()/1000); // pass seconds to all Widgets
}

void KJLoader::newSong()
{
	if (!napp->player()->current())
		return;
	for ( KJWidget* i=subwidgets.first(); i!=0; i=subwidgets.next() )
		i->newFile();
}

void KJLoader::readConfig()
{
//	kdDebug(66666) << "KJLoader::readConfig()" << endl;
	for (KJWidget* i=subwidgets.first(); i!=0; i=subwidgets.next())
		i->readConfig();
}

void KJLoader::showSplash()
{
	splashScreen = new QLabel( 0L, "SplashScreen",
		WType_TopLevel | WStyle_NoBorder | WRepaintNoErase | WX11BypassWM );

	QPixmap splashPix = pixmap(item("splashscreen")[1]);
	splashScreen->setPixmap( splashPix );
	splashScreen->setBackgroundMode ( NoBackground );
	splashScreen->setMask( KJWidget::getMask(image(item("splashscreen")[1])) );

	QSize sh = splashScreen->sizeHint();

	QRect desk = KGlobalSettings::splashScreenDesktopGeometry();
	splashScreen->move (desk.x() + (desk.width() - sh.width())/2,
		desk.y() + (desk.height() - sh.height())/2 );

	splashScreen->setFixedSize(sh);
	splashScreen->show();
	napp->processEvents(); // we want this one time to get the splash actually displayed ASAP

	QTimer::singleShot(3000, this, SLOT(hideSplash()) );
}

void KJLoader::hideSplash()
{
	splashScreen->hide();
	delete splashScreen;
}
