/*
 * Backend engine for game installer UI
 *
 * Copyright (C) 2003--2013  Enrico Zini <enrico@debian.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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "Engine.h"

#include <wibble/string.h>
#include <wibble/regexp.h>
#include <iostream>
#include <fstream>

using namespace std;
using namespace wibble;
using namespace ept::apt;
using namespace ept::debtags;

Engine::Engine()
	: m_db("/var/lib/apt-xapian-index/index"), m_stem("en"), m_filter_state(ANY), m_dirty(true), m_max(0), m_popcon_validx(-1)
{
	m_qp.set_default_op(Xapian::Query::OP_AND);
        m_qp.set_database(m_db);
        m_qp.set_stemmer(m_stem);
        m_qp.set_stemming_strategy(Xapian::QueryParser::STEM_SOME);
        m_qp.add_prefix("pkg", "XP");
        m_qp.add_boolean_prefix("tag", "XT");
        m_qp.add_boolean_prefix("sec", "XS");

        // Read the Xapian value index used for popcon data
        std::ifstream in;
        in.open("/var/lib/apt-xapian-index/values", ios::in);
        if (!in.is_open() || in.fail())
            m_popcon_validx = -1;
        ERegexp match_line("^app-popcon[ \t]+([0-9]+)", 2);
        string line;
        while (true)
        {
            getline(in, line);
            if (in.fail()) break;
            if (in.eof()) break;
            if (match_line.match(line))
            {
                m_popcon_validx = strtoul(match_line[1].c_str(), 0, 10);
                break;
            }
        }
}

struct EngineMatchDecider : public Xapian::MatchDecider
{
	Engine& e;
	EngineMatchDecider(Engine& e) : e(e) {}

	virtual bool operator()(const Xapian::Document &doc) const
	{
		// Filter out results that apt doesn't know
		if (!e.apt().isValid(doc.get_data()))
			return false;

		// Finally filter by installed state if requested
		Engine::State s = e.getInstalledFilter();
		if (s != Engine::ANY)
		{
			PackageState state = e.apt().state(doc.get_data());
			if (s == Engine::INSTALLED && !state.isInstalled())
				return false;
			if (s == Engine::NOTINSTALLED && state.isInstalled())
				return false;
		}
		return true;
	}
};

static Xapian::Query allGames(Vocabulary& voc, const std::string& facet="game")
{
	set<std::string> games = voc.tags(facet);
	vector<string> terms;
	for (set<std::string>::const_iterator i = games.begin();
			i != games.end(); ++i)
		terms.push_back("XT" + *i);
	return Xapian::Query(Xapian::Query::OP_OR, terms.begin(), terms.end());
}

Xapian::Query Engine::makeQuery()
{
	Xapian::Query query;
	Xapian::Query kwquery;
	Xapian::Query typequery;
	Xapian::Query ifacequery;

	if (!m_filter_keywords.empty())
	{
		// Add prefixes to tag names
		Splitter splitter("[ \t]*,[ \t]*", REG_EXTENDED);
		vector<string> kw;
		for (Splitter::const_iterator i = splitter.begin(m_filter_keywords);
				i != splitter.end(); ++i)
		{
			if (m_vocabulary.hasTag(*i))
				kw.push_back("tag:" + *i);
			else
				kw.push_back(*i);
		}
		bool do_partial = not (kw.size() == 1 and kw[0].size() < 3);

		kwquery = m_qp.parse_query(str::join(kw.begin(), kw.end(), " "),
				Xapian::QueryParser::FLAG_BOOLEAN |
				Xapian::QueryParser::FLAG_LOVEHATE |
				Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
				Xapian::QueryParser::FLAG_WILDCARD |
				(do_partial ? Xapian::QueryParser::FLAG_PARTIAL : 0) |
				Xapian::QueryParser::FLAG_PURE_NOT |
				Xapian::QueryParser::FLAG_SPELLING_CORRECTION);
	}
	if (!m_filter_type.empty())
		typequery = Xapian::Query("XT"+m_filter_type);
	if (!m_filter_iface.empty())
		ifacequery = Xapian::Query("XT"+m_filter_iface);
		
	if (kwquery.empty())
		if (typequery.empty())
			if (ifacequery.empty())
			{
				// If there is no query, default to querying all games
				query = allGames(voc(), mainFacet);
			}
			else
			{
				// Otherwise, all games with given interface
				query = Xapian::Query(Xapian::Query::OP_AND, ifacequery, allGames(voc(), mainFacet));
			}
		else
			if (ifacequery.empty())
				query = typequery;
			else
				query = Xapian::Query(Xapian::Query::OP_AND, ifacequery, typequery);
	else
		if (typequery.empty())
			if (ifacequery.empty())
				query = Xapian::Query(Xapian::Query::OP_AND, kwquery, allGames(voc(), mainFacet));
			else
				query = Xapian::Query(Xapian::Query::OP_AND, kwquery,
							Xapian::Query(Xapian::Query::OP_AND, ifacequery, allGames(voc(), mainFacet)));
		else
			if (ifacequery.empty())
				query = Xapian::Query(Xapian::Query::OP_AND, kwquery, typequery);
			else
				query = Xapian::Query(Xapian::Query::OP_AND, kwquery,
							Xapian::Query(Xapian::Query::OP_AND, typequery, ifacequery));

	// We always want programs, so always AND it here
	return Xapian::Query(Xapian::Query::OP_AND, globalFilter, query);
}

float Engine::read_popcon(const Xapian::Document& doc) const
{
    if (m_popcon_validx == -1) return 0;
    string val = doc.get_value(m_popcon_validx);
    if (val.empty()) return 0;
    return Xapian::sortable_unserialise(val);
}

void Engine::recompute()
{
	EngineMatchDecider md(*this);

	// Clear existing results
	m_results.clear();
	m_types.clear();
	m_interfaces.clear();
	m_res_max = 0;

	//cerr << "Engine recompute:" << endl;

	// Compute the types
	if (!m_filter_type.empty())
	{
		//cerr << "  filter type: " << m_filter_type.fullname() << endl;
		std::string tmp = m_filter_type;
		m_filter_type = std::string();
		Xapian::Enquire enquire(m_db);
		enquire.set_query(makeQuery());

		// Get all the results out of Xapian
		bool done = false;
		for (size_t pos = 0; !done; pos += 50)
		{
			Xapian::MSet matches = enquire.get_mset(pos, 50, 0, 0, &md);
			if (matches.size() < 50)
				done = true;
			for (Xapian::MSetIterator i = matches.begin(); i != matches.end(); ++i)
			{
				// Get all the game and interface tags in the result set
				set<std::string> tags = m_debtags.getTagsOfItem(i.get_document().get_data());
				for (set<std::string>::const_iterator j = tags.begin();
						j != tags.end(); ++j)
					if (voc::getfacet(*j) == mainFacet)
						m_types.insert(*j);
			}
		}
		m_filter_type = tmp;
	} else {
		//cerr << "  no filter type" << endl;
	}

	// Compute the interfaces
	if (!m_filter_iface.empty())
	{
		//cerr << "  filter iface: " << m_filter_iface.fullname() << endl;
		std::string tmp = m_filter_iface;
		m_filter_iface = std::string();
		Xapian::Enquire enquire(m_db);
		enquire.set_query(makeQuery());

		// Get all the results out of Xapian
		bool done = false;
		for (size_t pos = 0; !done; pos += 50)
		{
			Xapian::MSet matches = enquire.get_mset(pos, 50, 0, 0, &md);
			if (matches.size() < 50)
				done = true;
			for (Xapian::MSetIterator i = matches.begin(); i != matches.end(); ++i)
			{
				// Get all the game and interface tags in the result set
				set<std::string> tags = m_debtags.getTagsOfItem(i.get_document().get_data());
				for (set<std::string>::const_iterator j = tags.begin();
						j != tags.end(); ++j)
					if (voc::getfacet(*j) == secondaryFacet)
						m_interfaces.insert(*j);
			}
		}
		m_filter_iface = tmp;
	} else {
		//cerr << "  no filter iface" << endl;
	}

	Xapian::Enquire enquire(m_db);
	enquire.set_query(makeQuery());

	//cerr << "  filter query: " << enquire.get_query().get_description() << endl;

	// Get all the results out of Xapian
	bool done = false;
	for (size_t pos = 0; !done; pos += 50)
	{
		Xapian::MSet matches = enquire.get_mset(pos, 50, 0, 0, &md);
		if (matches.size() < 50)
			done = true;
		for (Xapian::MSetIterator i = matches.begin(); i != matches.end(); ++i)
		{
			// Stop producing if the quality goes below a cutoff point
			// FIXME: hardcoded value, but I can't see a reason to make it
			// configurable yet
			// FIXME: can be done adaptive, and can be done using set_cutoff in the
			// enquire
			//if (i.get_percent() < 40)
				//break;

			Result res;
			Xapian::Document doc = i.get_document();
			res.name = doc.get_data();
			res.popcon = read_popcon(doc);
			res.relevance = i.get_percent();

			if (res.popcon > m_res_max)
				m_res_max = res.popcon;

			m_results.push_back(res);

			// Get all the game and interface tags in the result set
			// only for type or filter when they are not set
			if (m_filter_type.empty() || m_filter_iface.empty())
			{
				set<std::string> tags = m_debtags.getTagsOfItem(res.name);
				for (set<std::string>::const_iterator j = tags.begin();
						j != tags.end(); ++j)
					if (m_filter_type.empty() && voc::getfacet(*j) == mainFacet)
						m_types.insert(*j);
					else if (m_filter_iface.empty() && voc::getfacet(*j) == secondaryFacet)
						m_interfaces.insert(*j);
			}
		}
	}
	// Always keep the currently selected items in the lists
	if (!m_filter_type.empty())
		m_types.insert(m_filter_type);
	if (!m_filter_iface.empty())
		m_interfaces.insert(m_filter_iface);


	if (m_res_max > m_max)
		m_max = m_res_max;

    if (m_filter_keywords.empty())
		sort(m_results.begin(), m_results.end());

	m_dirty = false;
}

std::vector<Result> Engine::related(const std::string& name, int count) const
{
	Xapian::Enquire enquire(m_db);
	
	// Retrieve the document for the given package
	enquire.set_query(Xapian::Query("XP"+name));
	Xapian::MSet matches = enquire.get_mset(0, 1);
	Xapian::MSetIterator mi = matches.begin();
	if (mi == matches.end()) return std::vector<Result>();
	Xapian::Document doc = mi.get_document();

	// Retrieve the list of similar documents
	enquire.set_query(Xapian::Query(Xapian::Query::OP_OR, doc.termlist_begin(), doc.termlist_end()));
	matches = enquire.get_mset(0, count);
	mi = matches.begin();
	if (mi == matches.end()) return std::vector<Result>();
	// Skip the first element, which is the package itself
	vector<Result> results;
	for (++mi; mi != matches.end(); ++mi)
	{
		Result res;
		Xapian::Document doc = mi.get_document();
		res.name = doc.get_data();
		res.popcon = read_popcon(doc);
		res.relevance = mi.get_percent();
		results.push_back(res);
	}
	return results;
}

void Engine::setKeywordFilter(const std::string& keywords)
{
	m_filter_keywords = keywords;
	m_dirty = true;
}

void Engine::setTypeFilter(const std::string& tag)
{
	m_filter_type = tag;
	m_dirty = true;
}

void Engine::setInterfaceFilter(const std::string& tag)
{
	m_filter_iface = tag;
	m_dirty = true;
}

void Engine::setInstalledFilter(State state)
{
	m_filter_state = state;
	m_dirty = true;
}

// vim:set ts=4 sw=4:
