// vim: set tabstop=4 shiftwidth=4 noexpandtab:
/*
Gwenview: an image viewer
Copyright 2008 Aurélien Gâteau <agateau@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.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.

*/
// Self
#include "filtercontroller.moc"

#include <config-gwenview.h>

// Qt
#include <QAction>
#include <QCompleter>
#include <QDate>
#include <QLineEdit>
#include <QPainter>
#include <QPainterPath>
#include <QTimer>
#include <QToolButton>

// KDE
#include <kcombobox.h>
#include <kdebug.h>
#include <kfileitem.h>
#include <kicon.h>
#include <kiconloader.h>
#include <klineedit.h>
#include <klocale.h>

// Local
#include <lib/datewidget.h>
#include <lib/flowlayout.h>
#include <lib/paintutils.h>
#include <lib/semanticinfo/sorteddirmodel.h>
#include <lib/timeutils.h>

#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
// KDE
#include <nepomuk/kratingwidget.h>
#ifdef GWENVIEW_SEMANTICINFO_BACKEND_NEPOMUK
#include <nepomuk/resourcemanager.h>
#endif

// Local
#include <lib/semanticinfo/abstractsemanticinfobackend.h>
#include <lib/semanticinfo/tagmodel.h>
#endif

namespace Gwenview {

/**
 * An AbstractSortedDirModelFilter which filters on the file names
 */
class NameFilter : public AbstractSortedDirModelFilter {
public:
	enum Mode {
		Contains,
		DoesNotContain
	};
	NameFilter(SortedDirModel* model)
	: AbstractSortedDirModelFilter(model)
	, mText()
	, mMode(Contains)
	{}

	virtual bool needsSemanticInfo() const {
		return false;
	}

	virtual bool acceptsIndex(const QModelIndex& index) const {
		if (mText.isEmpty()) {
			return true;
		}
		switch (mMode) {
		case Contains:
			return index.data().toString().contains(mText, Qt::CaseInsensitive);
		default: /*DoesNotContain:*/
			return !index.data().toString().contains(mText, Qt::CaseInsensitive);
		}
	}

	void setText(const QString& text) {
		mText = text;
		model()->applyFilters();
	}

	void setMode(Mode mode) {
		mMode = mode;
		model()->applyFilters();
	}

private:
	QString mText;
	Mode mMode;
};

struct NameFilterWidgetPrivate {
	QPointer<NameFilter> mFilter;
	KComboBox* mModeComboBox;
	KLineEdit* mLineEdit;
};

NameFilterWidget::NameFilterWidget(SortedDirModel* model)
: d(new NameFilterWidgetPrivate)
{
	d->mFilter = new NameFilter(model);

	d->mModeComboBox = new KComboBox;
	d->mModeComboBox->addItem(i18n("Name contains"), QVariant(NameFilter::Contains));
	d->mModeComboBox->addItem(i18n("Name does not contain"), QVariant(NameFilter::DoesNotContain));

	d->mLineEdit = new KLineEdit;

	QHBoxLayout* layout = new QHBoxLayout(this);
	layout->setMargin(0);
	layout->setSpacing(2);
	layout->addWidget(d->mModeComboBox);
	layout->addWidget(d->mLineEdit);

	QTimer* timer = new QTimer(this);
	timer->setInterval(350);
	timer->setSingleShot(true);
	connect(timer, SIGNAL(timeout()), SLOT(applyNameFilter()));

	connect(d->mLineEdit, SIGNAL(textChanged(QString)),
		timer, SLOT(start()));

	connect(d->mModeComboBox, SIGNAL(currentIndexChanged(int)),
		SLOT(applyNameFilter()));

	QTimer::singleShot(0, d->mLineEdit, SLOT(setFocus()));
}

NameFilterWidget::~NameFilterWidget()
{
	delete d->mFilter;
	delete d;
}

void NameFilterWidget::applyNameFilter() {
	QVariant data = d->mModeComboBox->itemData(d->mModeComboBox->currentIndex());
	d->mFilter->setMode(NameFilter::Mode(data.toInt()));
	d->mFilter->setText(d->mLineEdit->text());
}

/**
 * An AbstractSortedDirModelFilter which filters on the file dates
 */
class DateFilter : public AbstractSortedDirModelFilter {
public:
	enum Mode {
		GreaterOrEqual,
		Equal,
		LessOrEqual
	};
	DateFilter(SortedDirModel* model)
	: AbstractSortedDirModelFilter(model)
	, mMode(GreaterOrEqual)
	{}

	virtual bool needsSemanticInfo() const {
		return false;
	}

	virtual bool acceptsIndex(const QModelIndex& index) const {
		if (!mDate.isValid()) {
			return true;
		}
		KFileItem fileItem = model()->itemForSourceIndex(index);
		QDate date = TimeUtils::dateTimeForFileItem(fileItem).date();
		switch (mMode) {
		case GreaterOrEqual:
			return date >= mDate;
		case Equal:
			return date == mDate;
		default: /* LessOrEqual */
			return date <= mDate;
		}
	}

	void setDate(const QDate& date) {
		mDate = date;
		model()->applyFilters();
	}

	void setMode(Mode mode) {
		mMode = mode;
		model()->applyFilters();
	}

private:
	QDate mDate;
	Mode mMode;
};

struct DateFilterWidgetPrivate {
	QPointer<DateFilter> mFilter;
	KComboBox* mModeComboBox;
	DateWidget* mDateWidget;
};

DateFilterWidget::DateFilterWidget(SortedDirModel* model)
: d(new DateFilterWidgetPrivate)
{
	d->mFilter = new DateFilter(model);

	d->mModeComboBox = new KComboBox;
	d->mModeComboBox->addItem(i18n("Date >="), DateFilter::GreaterOrEqual);
	d->mModeComboBox->addItem(i18n("Date ="),  DateFilter::Equal);
	d->mModeComboBox->addItem(i18n("Date <="), DateFilter::LessOrEqual);

	d->mDateWidget = new DateWidget;

	QHBoxLayout* layout = new QHBoxLayout(this);
	layout->setMargin(0);
	layout->addWidget(d->mModeComboBox);
	layout->addWidget(d->mDateWidget);

	connect(d->mDateWidget, SIGNAL(dateChanged(QDate)),
		SLOT(applyDateFilter()));
	connect(d->mModeComboBox, SIGNAL(currentIndexChanged(int)),
		SLOT(applyDateFilter()));

	applyDateFilter();
}

DateFilterWidget::~DateFilterWidget()
{
	delete d->mFilter;
	delete d;
}

void DateFilterWidget::applyDateFilter() {
	QVariant data = d->mModeComboBox->itemData(d->mModeComboBox->currentIndex());
	d->mFilter->setMode(DateFilter::Mode(data.toInt()));
	d->mFilter->setDate(d->mDateWidget->date());
}

#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
/**
 * An AbstractSortedDirModelFilter which filters on file ratings
 */
class RatingFilter : public AbstractSortedDirModelFilter {
public:
	enum Mode {
		GreaterOrEqual,
		Equal,
		LessOrEqual
	};

	RatingFilter(SortedDirModel* model)
	: AbstractSortedDirModelFilter(model)
	, mRating(0)
	, mMode(GreaterOrEqual) {}

	virtual bool needsSemanticInfo() const {
		return true;
	}

	virtual bool acceptsIndex(const QModelIndex& index) const {
		SemanticInfo info = model()->semanticInfoForSourceIndex(index);
		switch (mMode) {
		case GreaterOrEqual:
			return info.mRating >= mRating;
		case Equal:
			return info.mRating == mRating;
		default: /* LessOrEqual */
			return info.mRating <= mRating;
		}
	}

	void setRating(int value) {
		mRating = value;
		model()->applyFilters();
	}

	void setMode(Mode mode) {
		mMode = mode;
		model()->applyFilters();
	}

private:
	int mRating;
	Mode mMode;
};

struct RatingWidgetPrivate {
	KComboBox* mModeComboBox;
	KRatingWidget* mRatingWidget;
	QPointer<RatingFilter> mFilter;
};

RatingFilterWidget::RatingFilterWidget(SortedDirModel* model)
: d(new RatingWidgetPrivate) {
	d->mModeComboBox = new KComboBox;
	d->mModeComboBox->addItem(i18n("Rating >="), RatingFilter::GreaterOrEqual);
	d->mModeComboBox->addItem(i18n("Rating =") , RatingFilter::Equal);
	d->mModeComboBox->addItem(i18n("Rating <="), RatingFilter::LessOrEqual);

	d->mRatingWidget = new KRatingWidget;
	d->mRatingWidget->setHalfStepsEnabled(true);
	d->mRatingWidget->setMaxRating(10);

	QHBoxLayout* layout = new QHBoxLayout(this);
	layout->setMargin(0);
	layout->addWidget(d->mModeComboBox);
	layout->addWidget(d->mRatingWidget);

	d->mFilter = new RatingFilter(model);

	QObject::connect(d->mModeComboBox, SIGNAL(currentIndexChanged(int)),
		SLOT(updateFilterMode()));

	QObject::connect(d->mRatingWidget, SIGNAL(ratingChanged(int)),
		SLOT(slotRatingChanged(int)));

	updateFilterMode();
}


RatingFilterWidget::~RatingFilterWidget() {
	delete d->mFilter;
	delete d;
}


void RatingFilterWidget::slotRatingChanged(int value) {
	d->mFilter->setRating(value);
}


void RatingFilterWidget::updateFilterMode() {
	QVariant data = d->mModeComboBox->itemData(d->mModeComboBox->currentIndex());
	d->mFilter->setMode(RatingFilter::Mode(data.toInt()));
}


/**
 * An AbstractSortedDirModelFilter which filters on associated tags
 */
class TagFilter : public AbstractSortedDirModelFilter {
public:
	TagFilter(SortedDirModel* model)
	: AbstractSortedDirModelFilter(model)
	, mWantMatchingTag(true)
	{}

	virtual bool needsSemanticInfo() const {
		return true;
	}

	virtual bool acceptsIndex(const QModelIndex& index) const {
		if (mTag.isEmpty()) {
			return true;
		}
		SemanticInfo info = model()->semanticInfoForSourceIndex(index);
		if (mWantMatchingTag) {
			return info.mTags.contains(mTag);
		} else {
			return !info.mTags.contains(mTag);
		}
	}

	void setTag(const SemanticInfoTag& tag) {
		mTag = tag;
		model()->applyFilters();
	}

	void setWantMatchingTag(bool value) {
		mWantMatchingTag = value;
		model()->applyFilters();
	}

private:
	SemanticInfoTag mTag;
	bool mWantMatchingTag;
};

struct TagFilterWidgetPrivate {
	KComboBox* mModeComboBox;
	QComboBox* mTagComboBox;
	QPointer<TagFilter> mFilter;
};

TagFilterWidget::TagFilterWidget(SortedDirModel* model)
: d(new TagFilterWidgetPrivate) {
	d->mFilter = new TagFilter(model);

	d->mModeComboBox = new KComboBox;
	d->mModeComboBox->addItem(i18n("Tagged"), QVariant(true));
	d->mModeComboBox->addItem(i18n("Not Tagged"), QVariant(false));

	d->mTagComboBox = new QComboBox;

	QHBoxLayout* layout = new QHBoxLayout(this);
	layout->setMargin(0);
	layout->addWidget(d->mModeComboBox);
	layout->addWidget(d->mTagComboBox);

	AbstractSemanticInfoBackEnd* backEnd = model->semanticInfoBackEnd();
	backEnd->refreshAllTags();
	TagModel* tagModel = TagModel::createAllTagsModel(this, backEnd);

	QCompleter* completer = new QCompleter(d->mTagComboBox);
	completer->setCaseSensitivity(Qt::CaseInsensitive);
	completer->setModel(tagModel);
	d->mTagComboBox->setCompleter(completer);
	d->mTagComboBox->setInsertPolicy(QComboBox::NoInsert);
	d->mTagComboBox->setEditable(true);
	d->mTagComboBox->setModel(tagModel);
	d->mTagComboBox->setCurrentIndex(-1);

	connect(d->mTagComboBox, SIGNAL(currentIndexChanged(int)), SLOT(updateTagSetFilter()) );

	connect(d->mModeComboBox, SIGNAL(currentIndexChanged(int)), SLOT(updateTagSetFilter()) );

	QTimer::singleShot(0, d->mTagComboBox, SLOT(setFocus()));
}

TagFilterWidget::~TagFilterWidget() {
	delete d->mFilter;
	delete d;
}


void TagFilterWidget::updateTagSetFilter() {
	QModelIndex index = d->mTagComboBox->model()->index(d->mTagComboBox->currentIndex(), 0);
	if (!index.isValid()) {
		kWarning() << "Invalid index";
		return;
	}
	SemanticInfoTag tag = index.data(TagModel::TagRole).toString();
	d->mFilter->setTag(tag);

	bool wantMatchingTag = d->mModeComboBox->itemData(d->mModeComboBox->currentIndex()).toBool();
	d->mFilter->setWantMatchingTag(wantMatchingTag);
}
#endif


/**
 * A container for all filter widgets. It features a close button on the right.
 */
class FilterWidgetContainer : public QFrame {
public:
	FilterWidgetContainer() {
		QPalette pal = palette();
		pal.setColor(QPalette::Window, pal.color(QPalette::Highlight));
		setPalette(pal);
	}

	void setFilterWidget(QWidget* widget) {
		QToolButton* closeButton = new QToolButton;
		closeButton->setIcon(KIcon("window-close"));
		closeButton->setAutoRaise(true);
		closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum);
		int size = IconSize(KIconLoader::Small);
		closeButton->setIconSize(QSize(size, size));
		connect(closeButton, SIGNAL(clicked()), SLOT(deleteLater()));
		QHBoxLayout* layout = new QHBoxLayout(this);
		layout->setMargin(2);
		layout->setSpacing(2);
		layout->addWidget(widget);
		layout->addWidget(closeButton);
	}

protected:
	virtual void paintEvent(QPaintEvent*) {
		QPainter painter(this);
		painter.setRenderHint(QPainter::Antialiasing);
		QPainterPath path = PaintUtils::roundedRectangle(QRectF(rect()).adjusted(0.5, 0.5, -0.5, -0.5), 6);

		QColor color = palette().color(QPalette::Highlight);
		painter.fillPath(path, PaintUtils::alphaAdjustedF(color, 0.5));
		painter.setPen(color);
		painter.drawPath(path);
	}
};


struct FilterControllerPrivate {
	FilterController* that;
	QFrame* mFrame;
	SortedDirModel* mDirModel;
	QList<QAction*> mActionList;

	int mFilterWidgetCount; /**< How many filter widgets are in mFrame */

	void addAction(const QString& text, const char* slot) {
		QAction* action = new QAction(text, that);
		QObject::connect(action, SIGNAL(triggered()), that, slot);
		mActionList << action;
	}

	void addFilter(QWidget* widget) {
		if (mFrame->isHidden()) {
			mFrame->show();
		}
		FilterWidgetContainer* container = new FilterWidgetContainer;
		container->setFilterWidget(widget);
		mFrame->layout()->addWidget(container);

		mFilterWidgetCount++;
		QObject::connect(container, SIGNAL(destroyed()), that, SLOT(slotFilterWidgetClosed()));
	}
};


FilterController::FilterController(QFrame* frame, SortedDirModel* dirModel)
: QObject(frame)
, d(new FilterControllerPrivate) {
	d->that = this;
	d->mFrame = frame;
	d->mDirModel = dirModel;
	d->mFilterWidgetCount = 0;

	d->mFrame->hide();
	FlowLayout* layout = new FlowLayout(d->mFrame);
	layout->setSpacing(2);

	d->addAction(i18nc("@action:inmenu", "Filter by Name"), SLOT(addFilterByName()));
	d->addAction(i18nc("@action:inmenu", "Filter by Date"), SLOT(addFilterByDate()));
#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
#ifdef GWENVIEW_SEMANTICINFO_BACKEND_NEPOMUK
	// Only add filters if Nepomuk is running
	if (Nepomuk::ResourceManager::instance()->init() == 0) {
#endif
		d->addAction(i18nc("@action:inmenu", "Filter by Rating"), SLOT(addFilterByRating()));
		d->addAction(i18nc("@action:inmenu", "Filter by Tag"), SLOT(addFilterByTag()));
#ifdef GWENVIEW_SEMANTICINFO_BACKEND_NEPOMUK
	}
#endif
#endif
}


FilterController::~FilterController() {
	delete d;
}


QList<QAction*> FilterController::actionList() const {
	return d->mActionList;
}


void FilterController::addFilterByName() {
	d->addFilter(new NameFilterWidget(d->mDirModel));
}


void FilterController::addFilterByDate() {
	d->addFilter(new DateFilterWidget(d->mDirModel));
}


#ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
void FilterController::addFilterByRating() {
	d->addFilter(new RatingFilterWidget(d->mDirModel));
}


void FilterController::addFilterByTag() {
	d->addFilter(new TagFilterWidget(d->mDirModel));
}
#endif


void FilterController::slotFilterWidgetClosed() {
	d->mFilterWidgetCount--;
	if (d->mFilterWidgetCount == 0) {
		d->mFrame->hide();
	}
}


} // namespace
