/*
 * debtags - Implement package tags support for Debian
 *
 * Copyright (C) 2003--2012  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
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#define APPNAME PACKAGE_NAME
#else
#warning No config.h found: using fallback values
#define APPNAME __FILE__
#define PACKAGE_VERSION "unknown"
#endif

#include <tagcoll/input/stdio.h>
#include <tagcoll/stream/filters.h>
#include <tagcoll/expression.h>
#include <tagcoll/TextFormat.h>
#include <tagcoll/SmartHierarchy.h>
#include <tagcoll/coll/simple.h>
#include <tagcoll/utils/set.h>
#include <tagcoll/patch.h>

#include "nag.h"
#include "loader.h"
#include "cmdline.h"
#include "nullstream.h"

#include <ept/apt/packagerecord.h>
#include <ept/debtags/debtags.h>
#include <ept/debtags/vocabulary.h>
#include <ept/debtags/maint/path.h>

#include <wibble/sys/fs.h>
#include <wibble/sys/exec.h>
#include <wibble/sys/process.h>

#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <sys/types.h>	// umask
#include <sys/stat.h>	// umask
#include <unistd.h>     // unlink

#include <cstring>

namespace std {

template<typename TAG, typename _Traits>
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>& out, const std::set<TAG>& tags)
{
	for (typename std::set<TAG>::const_iterator i = tags.begin();
			i != tags.end(); i++)
		if (i == tags.begin())
			out << i->fullname();
		else
			out << ", " << i->fullname();
	return out;
}

template<typename TAG, typename _Traits>
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>& out, const wibble::Singleton<TAG>& tags)
{
	out << *tags.begin();
	return out;
}

template<typename TAG, typename _Traits>
basic_ostream<char, _Traits>& operator<<(basic_ostream<char, _Traits>& out, const wibble::Empty<TAG>&)
{
	return out;
}

}

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


template<typename OUT>
class ExpressionFilter : public wibble::mixin::OutputIterator< ExpressionFilter<OUT> >
{
	tagcoll::Expression expr;
	bool invert;
	OUT out;
public:
	ExpressionFilter(const tagcoll::Expression& expr, bool invert, const OUT& out)
		: expr(expr), invert(invert), out(out) {}

	template<typename ITEMS, typename TAGS>
	ExpressionFilter<OUT>& operator=(const std::pair<ITEMS, TAGS>& data)
	{
		bool matched = expr(data.second);
		if (invert)
			matched = !matched;
		if (matched)
		{
			*out = data;
			++out;
		}
		return *this;
	}
};
template<typename OUT>
ExpressionFilter<OUT> expressionFilter(const tagcoll::Expression& expr, bool invert, const OUT& out)
{
	return ExpressionFilter<OUT>(expr, invert, out);
}

template<typename OUT>
void readCollection(const string& file, const OUT& out)
{
	if (file == "-")
	{
		input::Stdio input(stdin, "<stdin>");
		textformat::parse(input, out);
	}
	else
	{
		input::Stdio input(file);
		textformat::parse(input, out);
	}
}

class SubstringTagMatcher
{
protected:
	vector<string> patterns;

public:
	void add(const std::string& pattern) { patterns.push_back(pattern); }
	
	bool operator()(const voc::Data& item)
	{
		for (vector<string>::const_iterator i = patterns.begin();
				i != patterns.end(); i++)
		{
			if (strcasestr(item.name.c_str(), i->c_str()))
				return true;
			if (strcasestr(item.shortDescription().c_str(), i->c_str()))
				return true;
			if (strcasestr(item.longDescription().c_str(), i->c_str()))
				return true;
		}
		return false;
	}
};

struct VocabularyCheck
{
	int missing_count;
	map<string, int> missing;
	unsigned int mtag_width;
	int max_missing;

	VocabularyCheck() : missing_count(0), mtag_width(0), max_missing(0) {}

	void report(ostream& out)
	{
		if (missing_count > 0)
		{
			out << missing.size() << " tags were found in packages but not in the vocabulary" << endl;
			out << "This happened " << missing_count << " times" << endl;
			out << "The tags found in the collection but not in the vocabulary are:" << endl;

			int mtag_maxdigits = 0;
			for (int i = max_missing; i > 0; i = i / 10)
				mtag_maxdigits++;

			for (map<string, int>::const_iterator i = missing.begin();
					i != missing.end(); i++)
			{
				char buf[1000];
				snprintf(buf, 1000, "\t%s %*s (in %*d package%s)",
					i->first.c_str(), 
					(int)(mtag_width - i->first.size()),
					"", 
					mtag_maxdigits,
					i->second,
					i->second > 1 ? "s" : "");
				out << buf << endl;
			}
		}
	}
};

// Check a collection agaisnt a tag vocabulary
class VocabularyChecker : public wibble::mixin::OutputIterator< VocabularyChecker >
{
protected:
	const Vocabulary& voc;
	VocabularyCheck& res;

public:
	VocabularyChecker(const Vocabulary& voc, VocabularyCheck& res) : voc(voc), res(res) {}

	template<typename ITEMS, typename TAGS>
	VocabularyChecker& operator=(const std::pair<ITEMS, TAGS>& data)
	{
		for (typename TAGS::const_iterator i = data.second.begin();
				i != data.second.end(); ++i)
			if (!voc.hasTag(*i))
			{
				const string& tag = *i;
				res.missing_count++;
				res.missing[tag]++;
				if (tag.size() > res.mtag_width)
					res.mtag_width = tag.size();
				if (res.missing[tag] > res.max_missing)
					res.max_missing = res.missing[tag];
			}
		return *this;
	}
};

template<typename VOC, typename OUT>
class VocabularyFilter : public wibble::mixin::OutputIterator< VocabularyFilter<VOC, OUT> >
{
protected:
	const VOC& voc;
	OUT out;

public:
	VocabularyFilter(const VOC& voc, const OUT& out) : voc(voc), out(out) {}

	template<typename ITEMS, typename TAGS>
	VocabularyFilter& operator=(const std::pair<ITEMS, TAGS>& data)
	{
		std::set< typename TAGS::value_type > tags;
		for (typename TAGS::const_iterator i = data.second.begin();
				i != data.second.end(); ++i)
			if (voc.hasTag(*i))
				tags.insert(*i);
		*out = make_pair(data.first, tags);
		++out;
		return *this;
	}
};
template<typename VOC, typename OUT>
VocabularyFilter<VOC, OUT> vocabularyFilter(const VOC& voc, const OUT& out)
{
	return VocabularyFilter<VOC, OUT>(voc, out);
}

static void printShortVocabularyItem(const voc::FacetData& facet)
{
	cout << facet.name << " (facet) - " << facet.shortDescription() << endl;
}

static void printShortVocabularyItem(const voc::TagData& tag)
{
	cout << tag.name << " - " << tag.shortDescription() << endl;
}

static void printVocabularyItem(const voc::FacetData& tag)
{
	string ld = tag.longDescription();
	cout << "Facet: " << tag.name << endl;
	cout << "Description: " << tag.shortDescription() << endl;
	cout << " " << ld << endl;
	if (ld[ld.size() - 1] != '\n')
		cout << endl;
}
static void printVocabularyItem(const voc::TagData& tag)
{
	string ld = tag.longDescription();
	cout << "Tag: " << tag.name << endl;
	cout << "Description: " << tag.shortDescription() << endl;
	cout << " " << ld << endl;
	if (ld[ld.size() - 1] != '\n')
		cout << endl;
}

struct AtomicStdioWriter
{
	std::string fname;
	char* tmpfname;
	int fd;
	FILE* out;

	AtomicStdioWriter(const std::string& fname)
		: fname(fname), tmpfname(0), out(0), fd(-1)
	{
		// Build the temp file template
		tmpfname = new char[fname.size() + 8];
		strncpy(tmpfname, fname.c_str(), fname.size());
		strncpy(tmpfname + fname.size(), ".XXXXXX", 8);
		// Create and open the temporary file
		fd = mkstemp(tmpfname);
		if (fd < 0)
			throw wibble::exception::File(tmpfname, "opening file");

		// Pass the file descriptor to stdio
		out = fdopen(fd, "wt");
		if (!out)
			throw wibble::exception::File(tmpfname, "fdopening file");
	}

	~AtomicStdioWriter()
	{
		if (out)
		{
			if (unlink(tmpfname) == -1)
				throw wibble::exception::File(tmpfname, "cannot delete temporary file");
			fclose(out);
		}
		if (tmpfname) delete[] tmpfname;
	}

	void flush()
	{
		// Read the current umask
		mode_t cur_umask = umask(0);
		umask(cur_umask);

		// Give the file the right permissions
		if (fchmod(fd, 0666 & ~cur_umask) < 0)
			throw wibble::exception::File(tmpfname, "setting file permissions");

		// Flush stdio's buffers
		fflush(out);

		// Flush OS buffers
		fdatasync(fd);

		// Close the file
		fclose(out);
		out = NULL;

		// Rename the successfully written file to its final name
		if (rename(tmpfname, fname.c_str()) == -1)
			throw wibble::exception::System(string("renaming ") + tmpfname + " to " + fname);
	}
};

int main(int argc, const char* argv[])
{
    DebtagsOptions opts;
    Loader loader;

	try {
		// Install the handler for unexpected exceptions
		wibble::exception::InstallUnexpected installUnexpected;

		if (opts.parse(argc, argv))
			return 0;

        nag::init(opts.out_verbose->boolValue(), opts.out_debug->boolValue());

		// Output the full package tag database
		if (opts.foundCommand() == opts.cat)
		{
			Debtags& debtags = loader.debtags();
            auto_ptr<CollPrinter> printer = loader.make_coll_printer(opts);

			if (opts.hasNext())
			{
				debtags.output(expressionFilter(opts.next(), opts.match_invert->boolValue(), *printer));
			} else
				debtags.output(*printer);

            return printer->count > 0 ? 0 : 1;
		}
		// Output the full package database
		else if (opts.foundCommand() == opts.dumpavail)
		{
            auto_ptr<PackagePrinter> printer = loader.make_package_printer(opts, PackagePrinter::FULL);

			if (opts.hasNext())
			{
				loader.debtags().output(expressionFilter(opts.next(), opts.match_invert->boolValue(), *printer));
			} else {
				// If there is no expression filter, dump from the Apt database
				Apt& apt = loader.apt();
				PackageRecord record;
				for (Apt::record_iterator i = apt.recordBegin();
						i != apt.recordEnd(); ++i)
				{
					record.scan(*i);
					*printer = record;
				}
			}

            return printer->count > 0 ? 0 : 1;
		}
		// search [-v] <tag expression>\n"
		// Output the names and description of the packages that match\n"
		// the given tag expression\n"
		else if (opts.foundCommand() == opts.search)
		{
			// TODO: complain if no expression found

            auto_ptr<PackagePrinter> printer = loader.make_package_printer(opts, PackagePrinter::SHORT);

			if (opts.hasNext())
			{
				loader.debtags().output(expressionFilter(opts.next(), opts.match_invert->boolValue(), *printer));
			} else
				loader.debtags().output(*printer);

            return printer->count > 0 ? 0 : 1;
		}
		// grep [-v] [-q] <tag expression>
		// Output the lines of the full package tag database that match the
		// given tag expression
		else if (opts.foundCommand() == opts.grep)
		{
			// TODO: complain if no expression found
			Debtags& debtags = loader.debtags();
            auto_ptr<CollPrinter> printer = loader.make_coll_printer(opts);

			if (opts.hasNext())
			{
				debtags.output(expressionFilter(opts.next(), opts.match_invert->boolValue(), *printer));
			} else
				debtags.output(*printer);

            return printer->count > 0 ? 0 : 1;
		}
		// tagcat
		// Output the entire tag vocabulary
		else if (opts.foundCommand() == opts.tagcat)
		{
			Vocabulary& voc = loader.voc();

            if (opts.hasNext())
                throw wibble::exception::BadOption("the tagcat command does not require an argument");

			std::set<std::string> facets = voc.facets();
			for (std::set<std::string>::const_iterator i = facets.begin();
					i != facets.end(); i++)
			{
				const voc::FacetData* f = voc.facetData(*i);
				if (!f) continue;
				printVocabularyItem(*f);

				std::set<std::string> tags = f->tags();
				for (std::set<std::string>::const_iterator j = tags.begin();
						j != tags.end(); j++)
				{
					const voc::TagData* t = voc.tagData(*j);
					if (!t) continue;
					printVocabularyItem(*t);
				}
			}
			return 0;
		}
		// tagshow <tag>
		// Show the vocabulary informations about a tag
		else if (opts.foundCommand() == opts.tagshow)
		{
			string tag = opts.next();

			const voc::TagData* t = loader.voc().tagData(tag);
			if (!t)
			{
				verbose("Tag `%s' was not found in tag vocabulary\n", tag.c_str());
				return 1;
			}
			else
			{
				printVocabularyItem(*t);
				return 0;
			}
		}
		// tagsearch <pattern [pattern [pattern [...]]]>
		// Show a summary of all tags matching the given patterns
		else if (opts.foundCommand() == opts.tagsearch)
		{
			SubstringTagMatcher match;

			// Get the patterns to be matched
			bool empty;
			while (opts.hasNext())
			{
				string pattern = opts.next();
				match.add(pattern);
				empty = false;
			}

			if (empty)
			{
				error("No patterns given in commandline\n");
				return 1;
			}

			int matched = 0;

			std::set<std::string> facets = loader.voc().facets();
			for (std::set<std::string>::const_iterator i = facets.begin();
					i != facets.end(); i++)
			{
				const voc::FacetData* f = loader.voc().facetData(*i);
				if (!f) continue;
				if (match(*f))
				{
					matched++;
					printShortVocabularyItem(*f);
				}

				std::set<std::string> tags = f->tags();
				for (std::set<std::string>::const_iterator j = tags.begin();
						j != tags.end(); j++)
				{
					const voc::TagData* t = loader.voc().tagData(*j);
					if (!t) continue;
					if (match(*t))
					{
						matched++;
						printShortVocabularyItem(*t);
					}
				}
			}

			return matched > 0 ? 0 : 1;
		}
		// show <pkg>
		// Call apt-cache show <pkg>, but add tag informations to the output.\n"
		else if (opts.foundCommand() == opts.show)
		{
			while (opts.hasNext())
			{
				string name = opts.next();

				if (loader.apt().isValid(name))
				{
                    auto_ptr<PackagePrinter> printer = loader.make_package_printer(PackagePrinter::FULL);
                    *printer = name;
					return 0;
				} else {
					verbose("Package %s not found", name.c_str());
					return 1;
				}
			}
		}
		// tag
		//   tag [add <package> <tags...>\n"
		//   tag [rm  <package> <tags...>\n"
		//   tag [ls  <package>\n"
		//                View and edit the tags for a package\n");
		else if (opts.foundCommand() == opts.tag)
		{
			std::string cmd = opts.next();

			if (cmd == "add" || cmd == "rm")
			{
				loader.editable(true);

				string pkg = opts.next();
				if (!loader.apt().isValid(pkg))
				{
					error("Package %s not found\n", pkg.c_str());
					return 1;
				}

				std::set<std::string> tagset;
				while (opts.hasNext())
				{
					string tag = opts.next();
					if (loader.voc().hasTag(tag))
						tagset.insert(tag);
					else
						error("Tag '%s' not found: ignored\n", tag.c_str());
				}

				if (!tagset.empty())
				{
					PatchList<string, std::string> change;
					if (cmd == "add")
						change.addPatch(Patch<string, std::string>(pkg, tagset, std::set<std::string>()));
					else
						change.addPatch(Patch<string, std::string>(pkg, std::set<std::string>(), tagset));
					loader.debtags().applyChange(change);
					loader.debtags().savePatch();
				} else
					verbose("No tags to add\n");
			}
			else if (cmd == "ls")
			{
				string pkg = opts.next();
				if (loader.apt().isValid(pkg))
				{
					std::set<std::string> ts = loader.debtags().getTagsOfItem(pkg);
					for (std::set<std::string>::const_iterator i = ts.begin();
							i != ts.end(); i++)
						cout << *i << endl;
					return 0;
				} else {
					verbose("Package %s not found", pkg.c_str());
					return 1;
				}
			}
			else
				throw wibble::exception::Consistency("parsing the 'tag' subcommand", "command " + cmd + " is not valid working with tags");
		}
		// submit
		// Mail the local updates to the tag database to the central tag
		// repository
		else if (opts.foundCommand() == opts.submit)
		{
            if (opts.hasNext())
            {
                wibble::sys::Exec cmd(BINDIR "/debtags-submit-patch");
                cmd.envFromParent = true;
                cmd.searchInPath = false;
                cmd.args.push_back("/usr/bin/debtags-submit-patch");
                if (nag::is_verbose) cmd.args.push_back("--verbose");
                cmd.args.push_back(opts.next());
                cmd.exec();
            }
            else
            {
                tagcoll::PatchList<std::string, std::string> patch = loader.debtags().changes();
                if (patch.empty())
                {
                    verbose("Nothing to submit.\n");
                    return 0;
                }
                else
                {
                    string cmd = BINDIR "/debtags-submit-patch --stdin";
                    if (nag::is_verbose) cmd += " --verbose";
                    FILE* out = popen(cmd.c_str(), "w");
                    tagcoll::textformat::outputPatch(patch, out);
                    int res = pclose(out);
                    if (res < 0)
                        throw wibble::exception::System("running " + cmd);
                    else
                        return res;
                }
            }
		}
		// check <file>
		// Check that all the tags in the given tagged collection are
		// present in the tag vocabulary.  Checks the main database if no
		// file is specified
		else if (opts.foundCommand() == opts.check)
		{
			if (!opts.hasNext())
				throw wibble::exception::BadOption("you should specify the file with the collection to check");

			string file = opts.next();

			VocabularyCheck results;
			readCollection(file, VocabularyChecker(loader.voc(), results));

			if (results.missing_count > 0)
			{
				results.report(cout);
				return 1;
			}
			else
				return 0;
		}
		// mkpatch [filename]
		// Create a tag patch between the current tag database and the tag
		// collection [filename]
		else if (opts.foundCommand() == opts.diff)
		{
			string file = opts.next();

			coll::Simple<string, std::string> coll;
			loader.debtags().outputSystem(file, inserter(coll));

			PatchList<string, std::string> newpatches;
			newpatches.addPatch(loader.debtags(), coll);

			textformat::outputPatch(newpatches, stdout);
		}
		// update
		// Updates the package tag database (requires root)
		else if (opts.foundCommand() == opts.update)
		{
            using namespace wibble::sys;

            if (opts.hasNext())
                throw wibble::exception::BadOption("the update command does not require an argument");

            if (!fs::access(debtags::Path::debtagsIndexDir(), W_OK))
            {
                if (!fs::exists(debtags::Path::debtagsIndexDir()))
                    cerr << "Output directory " << debtags::Path::debtagsIndexDir() << " does not exist" << endl;
                else
                    cerr << "I do not have permission to write to " << debtags::Path::debtagsIndexDir() << endl;
                exit(1);
            }


            // Set a standard umask since we create system files
            mode_t orig_umask = process::umask(0022);

            // Load tags from apt
            tagcoll::coll::Simple<string, string> coll;
            Apt& apt = loader.apt();
            PackageRecord record;
            for (Apt::record_iterator i = apt.recordBegin();
                    i != apt.recordEnd(); ++i)
            {
                record.scan(*i);
                coll.insert(wibble::singleton(record.package()), record.tag());
            }

            // Load the vocabulary shipped with debtags
            Vocabulary voc(true);
            tagcoll::input::Stdio in("/usr/share/debtags/vocabulary");
            voc.read(in);

            // Write out the vocabulary
            voc.write();

            // Write out the tags
            AtomicStdioWriter writer(debtags::Path::tagdb());
            coll.output(textformat::StdioWriter(writer.out));
            writer.flush();
        }
		else if (opts.foundCommand() == opts.vocfilter)
		{
			if (!opts.hasNext())
				throw wibble::exception::BadOption("you should specify the file with the collection to check");

			string file = opts.next();

			if (opts.misc_vocfile->boolValue())
			{
				Vocabulary vm(true);
				input::Stdio input(opts.misc_vocfile->stringValue());
				vm.read(input);
				readCollection(file, vocabularyFilter(vm, textformat::OstreamWriter(cout)));
			}
			else
				readCollection(file, vocabularyFilter(loader.voc(), textformat::OstreamWriter(cout)));
		}
		else
			throw wibble::exception::BadOption(string("unhandled command ") +
						(opts.foundCommand() ? opts.foundCommand()->name() : "(null)"));

		return 0;
	} catch (wibble::exception::BadOption& e) {
		cerr << e.desc() << endl;
		opts.outputHelp(cerr);
		return 1;
	} catch (std::exception& e) {
		cerr << e.what() << endl;
		return 1;
	}
}

#include <ept/debtags/debtags.tcc>
#include <tagcoll/coll/simple.tcc>
#include <tagcoll/coll/fast.tcc>
#include <tagcoll/TextFormat.tcc>

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