/*
 * Copyright (C) 2012, Gaetan Bisson <bisson@archlinux.org>.
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/*
 * Squaw. The simplistic Qt-based user agent for the Web.
 *
 * Squaw is a Web browser based on the Qt port of WebKit which strives to be
 * minimalistic and flexible; it achieves both by merely consisting of this
 * short, easy-to-hack C++ file.
 *
 * Compile with:
 *
 *   moc -o squaw.moc squaw.cpp
 *
 *   c++ -O2 -lQtWebKit -lQtGui -lQtNetwork -lQtCore -o squaw squaw.cpp
 */

/*
 * OPTIONS AND ARGUMENTS
 *
 * Squaw expects an argument array consisting of zero or more options followed
 * by a description of the resource to be accessed; these are processed in the
 * arg() and res() methods, respectively, which you will most likely want to
 * customize.
 *
 * By default, two options are supported:
 * - "-h foo bar", which sets a "foo: bar" HTTP header;
 * - "-u foo", which sets the User-Agent string to "foo".
 *
 * By default, several types of resource descriptions are supported, such as:
 * - "wp foo bar", to search Wikipedia for "foo bar";
 * - "foo.bar", to access the http://foo.bar/ site;
 * - "bbc", to access the BBC News site.
 */

/*
 * STORAGE CONSIDERATIONS
 *
 * Squaw uses three directories, created in main():
 * - CacheLocation, typically "~/.cache/Squaw", as network cache directory.
 * - DesktopLocation, typically "~/Desktop", as download target directory.
 * - "~/.squaw" to store three specific files:
 * - "~/.squaw/block.txt", a read-only list of hosts to block.
 * - "~/.squaw/cookies.txt", a read-write persistent list of cookies.
 * - "~/.squaw/credentials.txt", a read-only list of HTTP authentication
 *   values.
 *
 * Lines of the latter must contain three components: the URL prefix where the
 * given authentication values should be used, the user name, and the password,
 * separated by tabs. See a_auth().
 *
 * To quickly generate an ad-blocking list of hosts, use for instance:
 *
 *   curl http://someonewhocares.org/hosts/hosts |
 *   awk '/hijack/{a=1}a&&/^127/{print $2}' > block.txt
 */


/* ****************************************************************************
 *
 * HEADERS
 *
 */


#include <sys/stat.h>
#include <QtGui/QtGui>
#include <QtNetwork/QtNetwork>
#include <QtWebKit/QtWebKit>


/* ****************************************************************************
 *
 * FEATURES ON TOP OF QTWEBKIT
 *
 */


QString S, T, D, U;
QList< QPair<QByteArray, QByteArray> > H;

/* Customize user-agent string, use single window */
class Page: public QWebPage {
	public:
	QString userAgentForUrl (const QUrl &u) const { return U.isEmpty() ? QWebPage::userAgentForUrl(u) : U; }
	QWebPage *createWindow (WebWindowType t) { return this; }
};

/* Block selected domains, customize HTTP request headers */
class Network: public QNetworkAccessManager {
	QHash<QString, bool> l;
	public:
	Network() {
		/* Load block list */
		QFile f (S+"block.txt");
		if (f.open(QIODevice::ReadOnly)) {
			while (f.bytesAvailable()) l.insert(QString(f.readLine().trimmed()), true);
			f.close();
		}
	}
	QNetworkReply *createRequest (Operation o, const QNetworkRequest &r, QIODevice *d=0) {
		if (l.value(r.url().host(), false)) return QNetworkAccessManager::head(QNetworkRequest());
		QNetworkRequest q (r);
		for (int i=0;i<H.length();i++) q.setRawHeader(H.at(i).first, H.at(i).second);
		return QNetworkAccessManager::createRequest(o, q, d);
	}
};

/* Implement persistent, plain text cookie storage */
class Cookies: public QNetworkCookieJar {
	QString F;
	public:
	Cookies () {
		F = S+"cookies.txt";
		QFile f (F);
		if (f.open(QIODevice::ReadOnly)) {
			QList<QNetworkCookie> C;
			while (f.bytesAvailable()) C.append(QNetworkCookie::parseCookies(f.readLine()));
			setAllCookies(C);
			f.close();
	  	}
	}
	bool setCookiesFromUrl (const QList<QNetworkCookie> &c, const QUrl &u) {
		bool r = this->QNetworkCookieJar::setCookiesFromUrl(c, u);
		QFile f (F);
		if (f.open(QIODevice::WriteOnly)) {
			QList<QNetworkCookie> C = allCookies();
			for (int i=0;i<C.size();i++) f.write(C.at(i).toRawForm()+"\n");
			f.close();
		}
		return r;
	}
};


/* ****************************************************************************
 *
 * INTERFACE LAYOUT
 *
 */


class Squaw: public QMainWindow {

	Q_OBJECT
	Page *p;
	Network *n;
	QWebView *v;
	QLineEdit *a;
	QLineEdit *s;
	QStatusBar *b;
	QPrintDialog *l;
	QList<QString> c;
	QList<QNetworkReply*> d;
	QList<QNetworkReply*> e;

	public:
	Squaw (QUrl u) {

		/* Load credentials */
		QFile f (S+"credentials.txt");
		if (f.open(QIODevice::ReadOnly)) {
			while (f.bytesAvailable()) c.append(QString(f.readLine().trimmed()).split(QChar('	')));
			f.close();
		}

		/* Global settings for QtWebKit */
		QWebSettings *o = QWebSettings::globalSettings();
		//o->setAttribute(QWebSettings::PluginsEnabled, true);
		o->setAttribute(QWebSettings::DnsPrefetchEnabled, true);
		o->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
		o->setAttribute(QWebSettings::PrintElementBackgrounds, false);
		o->setMaximumPagesInCache(5);
		o->setUserStyleSheetUrl(QUrl(
			"data:text/css;charset=utf-8;base64,"+QByteArray(
"body{text-rendering:optimizeLegibility;font-family:serif!important;background-color:#ddeeee;}"
"code,pre,samp,textarea,tt{font-family:monospace!important;}"
"*{font-family:inherit!important;}"
			).toBase64()));

		/* Create core objects */
		n = new Network;
		n->setCookieJar(new Cookies);
		n->setCache(new QNetworkDiskCache);
		qobject_cast<QNetworkDiskCache*>(n->cache())->setCacheDirectory(T);
		p = new Page;
		p->setNetworkAccessManager(n);
		p->setForwardUnsupportedContent(true);
		v = new QWebView;
		v->setPage(p);
		v->setFocus();
		v->load(u);
		setCentralWidget(v);

		/* Create widgets */
		s = new QLineEdit;
		a = new QLineEdit;
		a->setReadOnly(true);
		a->setFont(QFont("monospace"));
		b = statusBar();
		b->setSizeGripEnabled(false);
		b->addPermanentWidget(a, 4);
		b->addPermanentWidget(s, 1);
		l = new QPrintDialog;

		/* Event listeners */
		connect(new QShortcut(QKeySequence::Forward,  v), SIGNAL(activated()), SLOT(a_forward()));
		connect(new QShortcut(QKeySequence::Back,     v), SIGNAL(activated()), SLOT(a_back()));
		connect(new QShortcut(QKeySequence::Refresh,  v), SIGNAL(activated()), SLOT(a_reload()));
		connect(new QShortcut(QKeySequence::Print,    v), SIGNAL(activated()), SLOT(a_print()));
		connect(new QShortcut(QKeySequence::ZoomIn,   v), SIGNAL(activated()), SLOT(a_zoom_in()));
		connect(new QShortcut(QKeySequence::ZoomOut,  v), SIGNAL(activated()), SLOT(a_zoom_out()));
		connect(new QShortcut(QKeySequence("Ctrl+="), v), SIGNAL(activated()), SLOT(a_zoom_none()));
		connect(new QShortcut(QKeySequence::Find,     v), SIGNAL(activated()), SLOT(a_search_focus()));
		connect(new QShortcut(QKeySequence("Escape"), v), SIGNAL(activated()), SLOT(a_search_unfocus()));
		connect(new QShortcut(QKeySequence("Return"), v), SIGNAL(activated()), SLOT(a_search_next()));
		connect(new QShortcut(QKeySequence::FindNext, v), SIGNAL(activated()), SLOT(a_search_next()));
		connect(s, SIGNAL(textChanged(QString)),                               SLOT(a_search_change(QString)));
		connect(v, SIGNAL(urlChanged(QUrl)),                                   SLOT(a_url(QUrl)));
		connect(v, SIGNAL(titleChanged(QString)),                              SLOT(a_title(QString)));
		connect(p, SIGNAL(downloadRequested(QNetworkRequest)),                 SLOT(a_download(QNetworkRequest)));
		connect(p, SIGNAL(unsupportedContent(QNetworkReply*)),                 SLOT(a_unsupported(QNetworkReply*)));
		connect(n, SIGNAL(finished(QNetworkReply*)),                           SLOT(a_finished(QNetworkReply*)));
		connect(n, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)),        SLOT(a_ssl(QNetworkReply*, QList<QSslError>)));
		connect(n, SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), SLOT(a_auth(QNetworkReply*, QAuthenticator*)));
	}

	/* Save downloaded data permanently */
	QString save (QNetworkReply *r) {
		QString s = D+r->url().toString().section(QChar('?'), 0, 0).section(QChar('/'), -1, -1);
		while (QFileInfo(s).exists()) s += "+";
		QFile f (s);
		f.open(QIODevice::WriteOnly);
		f.write(r->readAll());
		f.close();
		return s;
	}

	protected slots:

		void a_forward () { p->triggerAction(QWebPage::Forward); }
		void a_back () { p->triggerAction(QWebPage::Back); }
		void a_reload () { p->triggerAction(QWebPage::Reload); }
		void a_print () { if (l->exec() == QDialog::Accepted) p->currentFrame()->print(l->printer()); }
		void a_zoom_in () { v->setZoomFactor(v->zoomFactor()*1.1); }
		void a_zoom_out () { v->setZoomFactor(v->zoomFactor()/1.1); }
		void a_zoom_none () { v->setZoomFactor(1); }
		void a_search_focus () { s->setFocus(); s->selectAll(); }
		void a_search_unfocus () { s->setText(""); v->setFocus(); }
		void a_search_change (QString s) { p->findText("", QWebPage::HighlightAllOccurrences); p->findText(s, QWebPage::HighlightAllOccurrences); }
		void a_search_next () { p->findText(s->text(), QWebPage::FindWrapsAroundDocument); }
		void a_url (QUrl u) { a->setText(u.toString()); }
		void a_title (QString t) { setWindowTitle(t); }

		// FIXME bind middle click to:
		// QProcess().startDetached(QString("squaw"), QStringList(request.url().toString()));

		/* Handle downloads and external/unsupported resources */
		void a_download (QNetworkRequest r) {
			d.append(n->get(r));
			a->setCursor(Qt::WaitCursor);
		}
		void a_unsupported (QNetworkReply *r) {
			e.append(r);
			a->setCursor(Qt::WaitCursor);
		}
		void a_finished (QNetworkReply *r) {
			if (d.removeAll(r)) save(r);
			if (e.removeAll(r)) {
				QString s = save(r);
				QProcess().startDetached(QString("xdg-open"), QStringList(s));
			}
			if (!(d.length()+e.length())) a->unsetCursor();
		}

		/* Mark broken SSL red, let it through */
		void a_ssl (QNetworkReply *r, QList<QSslError> e) {
			QPalette t = a->palette();
			t.setColor(QPalette::Base, Qt::red);
			a->setPalette(t);
			r->ignoreSslErrors();
		}

		/* Use credentials when possible */
		void a_auth (QNetworkReply *r, QAuthenticator *a) {
			for (int i=0;i<c.size();i+=3)
				if (r->url().toString().startsWith(c.at(i))) {
					a->setUser(c.at(i+1));
					a->setPassword(c.at(i+2));
				}
		}

};


/* ****************************************************************************
 *
 * STARTUP
 *
 */


/* Process optional arguments */
void arg (int *c, char **v[]) {
	while (*c>0)
		if (!strcmp((*v)[0], "-h")) {
			H.append(QPair<QByteArray, QByteArray>((*v)[1], (*v)[2]));
			*c -= 3;
			*v += 3;
		} else if (!strcmp((*v)[0], "-u")) {
			U = QString::fromUtf8((*v)[1]);
			*c -= 2;
			*v += 2;
		} else break;
}

/* Parse resource description */
QUrl res (int c, char *v[]) {
	QString s = QString::fromUtf8(v[0]);
	QString t = QString::fromUtf8(v[1]);
	if (s.contains("://")) return s;
	if (s.contains(".")) return "http://"+s;
	for (int i=2;i<c;i++) t += "+"+QString::fromUtf8(v[i]);
	if (s=="dd")   return "https://duckduckgo.com/?q="+t;
	if (s=="gg")   return "https://encrypted.google.com/search?q="+t;
	if (s=="im")   return "https://encrypted.google.com/images?q="+t;
	if (s=="map")  return "https://maps.google.com/?q="+t;
	if (s=="wp")   return "http://en.wikipedia.org/w/index.php?title=Special:Search&search="+t;
	if (s=="wpfr") return "http://fr.wikipedia.org/w/index.php?title=Spécial:Recherche&search="+t;
	if (s=="abc")  return "http://www.abc.net.au/news/world/"+t;
	if (s=="bbc")  return "http://www.bbc.co.uk/news/"+t;
	if (s=="fs")   return "https://bugs.archlinux.org/task/"+t;
	if (s=="bug")  return "https://bugs.archlinux.org/?dev=4787"+t;
	if (s=="pkg")  return "https://www.archlinux.org/packages/?q="+t;
	if (s=="aur")  return "https://aur.archlinux.org/packages.php?O=0&do_Search=Go&K="+t;
	if (s=="arch") return "https://www.archlinux.org/devel/"+t;
	if (s=="gm")   return "https://mail.google.com/mail/"+t;
	               return "https://duckduckgo.com/?q="+s+"+"+t;
}

/* Initialize and run */
int main (int argc, char *argv[]) {
	QApplication x (argc, argv);
	x.setApplicationName("Squaw");
	x.setApplicationVersion("0.7");

	umask(S_IRWXG|S_IRWXO);
	S = QDesktopServices::storageLocation(QDesktopServices::HomeLocation)+"/.squaw/";
	T = QDesktopServices::storageLocation(QDesktopServices::CacheLocation)+"/";
	D = QDesktopServices::storageLocation(QDesktopServices::DesktopLocation)+"/";
	QDir().mkpath(S);
	QDir().mkpath(T);
	QDir().mkpath(D);

	int c = argc-1;
	char **v = argv+1;
	arg(&c, &v);
	
	if (c<1) {
		printf("\
Squaw. The simplistic Qt-based user agent for the Web.\n\
Copyright (C) 2012 Gaetan Bisson. All rights reserved.\n\
Version 0.7; compiled on "__DATE__".\n\
\n\
Squaw does not support wandering the Web: a description of\n\
the resource to be accessed must be provided as argument.\n\
See the source code for details.\n\
");
		return 1;
	}

	(new Squaw(res(c, v)))->show();
	return x.exec();
}

#include "squaw.moc"
