//
// C++ Implementation: aptfrontpackagedb
//
// Description: 
//
//
// Author: Benjamin Mesing <bensmail@gmx.net>, (C) 2005
//
// Copyright: See COPYING file that comes with this distribution
//
//
#include "aptfrontpackagedb.h"

#include <ept/apt/apt.h>
#include <ept/apt/packagerecord.h>

#include <xapian.h>

#include <QStringList>

// NPlugin
#include <iprogressobserver.h>
#include <iprovider.h>
#include <packagenotfoundexception.h>

#include <helpers.h>


namespace NApt {

AptFrontPackageDB::AptFrontPackageDB(NPlugin::IProvider* pProvider) :
	_pProvider(pProvider),
	_currentPackage(_pProvider->apt(), std::string())
{
}


AptFrontPackageDB::~AptFrontPackageDB()
{
}


const AptFrontPackage& AptFrontPackageDB::getPackageRecord(const QString& pkg) const
{
	return getPackageRecord(toString(pkg));
}


const AptFrontPackage& AptFrontPackageDB::getPackageRecord(const string& package) const
{
	_currentPackage = AptFrontPackage
	(
		_pProvider->apt(), _pProvider->apt().validate(package)
	);
	if (!_currentPackage.isValid())
		throw NPlugin::PackageNotFoundException(package);
	return _currentPackage;
}

const QString AptFrontPackageDB::getShortDescription(const string& package) const
{
	return getPackageRecord(package).shortDescription();
}

IPackage::InstalledState AptFrontPackageDB::getState(const string& package) const
{
	return getPackageRecord(package).installedState();
}

void AptFrontPackageDB::reloadPackageInformation(NUtil::IProgressObserver* pObserver)
{
	_pProvider->reloadAptFrontCache();
	if (pObserver)
		pObserver->setProgress(100);
}


/////////////////////////////////////////////////////
// IAptSearch Interface
/////////////////////////////////////////////////////

bool AptFrontPackageDB::search(std::set<string>& result,
		const QString& pattern, bool searchDescr) const
{
	QStringList includePatterns;
	includePatterns.push_back(pattern);
	return search(result, includePatterns, QStringList(), searchDescr); 
}


bool AptFrontPackageDB::search(std::set<string>& result, const QStringList& includePatterns, 
	const QStringList& excludePatterns, bool searchDescr) const
{
	typedef ept::apt::Apt Apt;
	const Apt& packages = _pProvider->apt();
	if (searchDescr)	// perform a full text search within the descriptions
	{
		try {
			Xapian::Enquire enq(_pProvider->xapian());
			vector<string> incs;
			// Get the weighted OR include query
			for (QStringList::const_iterator jt = includePatterns.begin(); jt != includePatterns.end(); ++jt)
				incs.push_back(jt->toLower().toStdString());
			Xapian::Query inc(Xapian::Query::OP_AND, incs.begin(), incs.end());
			if (excludePatterns.empty())
			{
				// Build the normal OR query
				enq.set_query(inc);
			} else {
				vector<string> excs;
				for (QStringList::const_iterator jt = excludePatterns.begin(); jt != excludePatterns.end(); ++jt)
					// the Xapian index requires all search terms to be lower case
					excs.push_back(jt->toLower().toStdString());
				Xapian::Query exc(Xapian::Query::OP_OR, excs.begin(), excs.end());
				// Build the combined AND NOT query
				enq.set_query(Xapian::Query(Xapian::Query::OP_AND_NOT, inc, exc));
			}
	
			// Get the 25k best results out of Xapian
			Xapian::MSet matches = enq.get_mset(0, 25000);
			for (Xapian::MSetIterator i = matches.begin(); i != matches.end(); ++i)
			{
				// Filter out results that apt doesn't know
				if (!packages.isValid(i.get_document().get_data()))
					continue;
				result.insert(i.get_document().get_data());
				// For reference, this would be the Xapian relevance
				// i.get_percent();
			}
		// Shouldn't happen
		} catch (Xapian::Error& e) {
			qFatal( "%s: %s (%s)\n",
				e.get_type(),
				e.get_msg().c_str(),
				e.get_context().c_str());
		}
	}
	// search in the package names in both cases (since Xapian-search cannot search within the title,
	// it can only search for the title starting with the given word (by XPterm*), but that does not find
	// the search term occuring at the end of the title (e.g. search term "apt" in "python-apt")
	for ( Apt::iterator it = packages.begin(); it != packages.end(); ++it )
	{
		if (result.find(*it) != result.end())	// if the package is already in the result list
			continue;
		QString name = toQString(*it);
		// will be set to false when one of the include patterns was not found in the package description
		bool included = true;
		// check if each included pattern occurs in the package
		for (QStringList::const_iterator jt = includePatterns.begin(); jt != includePatterns.end(); ++jt)
		{
			if ( !searchString(name, *jt, false, false) )
			{
				included = false;
				break;
			}
		}
		if (included)
			result.insert(*it);
	}

	for (std::set<string>::iterator it = result.begin(); it != result.end();) 
	{
		bool erase = false;
		QString name = toQString(*it);
		for (QStringList::const_iterator jt = excludePatterns.begin(); jt != excludePatterns.end(); ++jt)
		{
			if ( searchString(name, *jt, false, false) )
			{
				erase = true;
				break;
			}
		}
		if (erase) // we cannot erase the current iterator without causing harm, so we do it a little 
			// more complicated
			result.erase(it++);
		else
			++it;
	}

	return result.empty();
}


bool AptFrontPackageDB::searchString(const QString& text, const QString& searchPattern, bool caseSensitive, bool wholeWords)
{
	Qt::CaseSensitivity qCaseSensitive = caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
	if (!wholeWords)	// the easy case
	{
		return text.contains(searchPattern, qCaseSensitive);
	}
	int startIndex = 0;
	while (startIndex <= text.length()) 
	{
		int findIndex = text.indexOf(searchPattern, startIndex, qCaseSensitive);
		// if the search pattern was not found in the remaining string
		if (findIndex == -1)
			return false;
		bool startsAtBoundary = false;	// holds if the match started at a word boundary
		if (findIndex == 0)
			startsAtBoundary = true;
		else
		{
			QChar characterBefore = text.at(findIndex - 1);
			startsAtBoundary = !characterBefore.isLetter();
		}
		// if the match starts at a boundary, check if it also ends at one
		if (startsAtBoundary)
		{
			int endIndex = findIndex + searchPattern.length();
			// if the match is the end of the text it is a match
			if (endIndex == text.length())
				return true;
			QChar characterAfter = text.at(endIndex);
			// if there is a non-letter-character following the match, the search matches
			if (!characterAfter.isLetter())
				return true;
		}
		startIndex = findIndex + searchPattern.length();
	}
	assert(false);
	return false;
}


}

