/*
 * Copyright (C) 2012-2013, 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
 */

/*
 * ARGUMENTS
 *
 * Squaw expects an argument array consisting of zero or more options followed
 * by a resource description. The latter, processed in res(), can be a URL or
 * make use of keys (see below). The options currently supported by arg() are:
 * - "-h foo bar", which defines a "foo: bar" HTTP request header.
 * - "-u foo", which sets the User-Agent string to "foo".
 */

/*
 * CONFIGURATION
 *
 * Squaw uses three directories, all created in main():
 * - CacheLocation, typically "~/.cache/Squaw", as network cache directory.
 * - DesktopLocation, typically "~/Desktop", as download target directory.
 * - "~/.squaw" to store the following (all optional) files:
 *
 * - "block.txt", a read-only list of hosts to block.
 * - "cookies.txt", a read-write persistent list of cookies.
 * - "credentials.txt", a read-only list of authentication values.
 * - "keys.txt", a read-only list of keys pointing to resources.
 * - "user.css", a read-only user CSS style.
 *
 * Lines of the keys file of the type "key=url" have the effect that the
 * resource description "key foo bar" points to URL "urlfoo+bar"; see res().
 *
 * Lines of the credentials file of the type "attr=val" located between a "url"
 * line and the next blank line have the effect that the first input element
 * with attribute "attr" of URL "url" will be prefilled with the value "val".
 * Entries with attribute "user" or "password" are also used for HTTP
 * authentication. See get_creds(), a_prefill(), and a_auth().
 *
 * To 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 AND GLOBAL VARIABLES
 *
 */


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

QStringList A;
QString S, T, U, D, I;
QList<QByteArray> H;


/* ****************************************************************************
 *
 * NETWORK COOKIE JAR
 *
 * Make cookie jar persistent.
 */


class NetworkCookieJar: public QNetworkCookieJar {
	QList<QNetworkCookie> c;
	QFile f;

	public:
	NetworkCookieJar () {
		f.setFileName(S+"cookies.txt");
		f.open(QIODevice::ReadOnly);
		while (f.bytesAvailable()) c.append(QNetworkCookie::parseCookies(f.readLine()));
		setAllCookies(c);
		f.close();
	}
	bool setCookiesFromUrl (const QList<QNetworkCookie> &l, const QUrl &u) {
		bool r = QNetworkCookieJar::setCookiesFromUrl(l, u);
		c = allCookies();
		f.open(QIODevice::WriteOnly);
		for (int i=0;i<c.size();i++) f.write(c[i].toRawForm()+"\n");
		f.close();
		return r;
	}
};


/* ****************************************************************************
 *
 * NETWORK ACCESS MANAGER
 *
 * Block selected hosts. Customize HTTP request headers. Allow untrusted SSL.
 */


class NetworkAccessManager: public QNetworkAccessManager {
	QHash<QByteArray, bool> b;
	QNetworkDiskCache d;
	NetworkCookieJar c;
	Q_OBJECT

	public:
	NetworkAccessManager () {
		QFile f (S+"block.txt");
		f.open(QIODevice::ReadOnly);
		while (f.bytesAvailable()) b.insert(f.readLine().trimmed(), true);
		f.close();
		setCache(&d);
		setCookieJar(&c);
		d.setCacheDirectory(T);
		connect(this, SIGNAL(sslErrors(QNetworkReply*, QList<QSslError>)), SLOT(a_ssl(QNetworkReply*, QList<QSslError>)));
	}
	QNetworkReply *createRequest (Operation o, const QNetworkRequest &r, QIODevice *d=0) {
		QNetworkRequest q (r);
		if (b.value(r.url().host().toLocal8Bit(), false)) q.setUrl(QUrl());
		for (int i=0;i<H.size();i+=2) q.setRawHeader(H[i], H[i+1]);
		return QNetworkAccessManager::createRequest(o, q, d);
	}

	protected slots:
	void a_ssl (QNetworkReply *r, QList<QSslError> e) {
		for (int i=0;i<e.size();i++) switch (e[i].error()) {
			case QSslError::SelfSignedCertificateInChain:
			case QSslError::SelfSignedCertificate:
			case QSslError::CertificateUntrusted:
			case QSslError::NoError:
				continue;
			default:
				return;
		}
		r->ignoreSslErrors();
	}
};


/* ****************************************************************************
 *
 * WEB PAGE
 *
 * Prefill forms and reply to HTTP authentication requests. Track downloads.
 * Customize User-Agent string. Use single window.
 */


class WebPage: public QWebPage {
	QHash<QByteArray, QHash<QString, QString> > c;
	QList<QNetworkReply*> d;
	QList<QNetworkReply*> e;
	NetworkAccessManager n;
	Q_OBJECT

	public:
	WebPage () {
		get_creds();
		setNetworkAccessManager(&n);
		setForwardUnsupportedContent(true);
		connect(this, SIGNAL(loadFinished(bool)),                                      SLOT(a_prefill(bool)));
		connect(this, SIGNAL(downloadRequested(QNetworkRequest)),                      SLOT(a_download(QNetworkRequest)));
		connect(this, SIGNAL(unsupportedContent(QNetworkReply*)),                      SLOT(a_unsupported(QNetworkReply*)));
		connect(&n,   SIGNAL(finished(QNetworkReply*)),                                SLOT(a_finished(QNetworkReply*)));
		connect(&n,   SIGNAL(authenticationRequired(QNetworkReply*, QAuthenticator*)), SLOT(a_auth(QNetworkReply*, QAuthenticator*)));
	}
	void get_creds () {
		QFile f (S+"credentials.txt");
		f.open(QIODevice::ReadOnly);
		QList<QByteArray> w;
		while (f.bytesAvailable()) {
			QByteArray x = f.readLine().trimmed();
			if (x.isEmpty() || x[0]=='#') {
				w.clear();
				continue;
			}
			int y = x.indexOf('=');
			if (y>0) for (int i=0;i<w.size();i++)
				c[w[i]].insert(x.left(y), x.right(x.size()-y-1));
			else {
				c.insert(x, QHash<QString, QString>());
				w.append(x);
			}
		}
		f.close();
	}
	QString save (QNetworkReply *r) {
		QString s = D+r->url().toString(QUrl::RemoveQuery|QUrl::StripTrailingSlash).section('/', -1, -1);
		while (QFileInfo(s).exists()) s += "+";
		QFile f (s);
		f.open(QIODevice::WriteOnly);
		f.write(r->readAll());
		f.close();
		return s;
	}
	QWebPage *createWindow (WebWindowType t) { return this; }
	QString userAgentForUrl (const QUrl &u) const { return U.isEmpty() ? QWebPage::userAgentForUrl(u) : U; }

	protected slots:
	void a_prefill (bool o) {
		QWebFrame *f = currentFrame();
		QByteArray s = f->url().toEncoded(QUrl::RemoveQuery);
		QList<QString> k = c[s].keys();
		for (int i=0;i<k.size();i++)
			f->documentElement().findFirst("input[name=\""+k[i]+"\"]").setAttribute("value", c[s][k[i]]);
	}
	void a_download (QNetworkRequest r) { d.append(n.get(r)); }
	void a_unsupported (QNetworkReply *r) { e.append(r); }
	void a_finished (QNetworkReply *r) {
		if (d.removeAll(r)) save(r);
		if (e.removeAll(r)) {
			QString s = save(r);
			QProcess().startDetached("xdg-open", QStringList(s));
		}
	}
	void a_auth (QNetworkReply *r, QAuthenticator *a) {
		QByteArray s = r->url().toEncoded(QUrl::RemoveQuery);
		a->setUser(c[s]["user"]);
		a->setPassword(c[s]["password"]);
	}
};


/* ****************************************************************************
 *
 * WEB VIEW
 *
 * Set sane defaults. Bind mouse events to zooming and spawning new instances.
 * Add print dialog.
 */


class WebView: public QWebView {
	QWebSettings *o;
	WebPage p;
	Q_OBJECT

	public:
	WebView () {
		setPage(&p);
		o = QWebSettings::globalSettings();
		o->setMaximumPagesInCache(5);
		o->setAttribute(QWebSettings::PluginsEnabled, true);
		o->setAttribute(QWebSettings::DnsPrefetchEnabled, true);
		o->setAttribute(QWebSettings::DeveloperExtrasEnabled, true);
		o->setAttribute(QWebSettings::PrintElementBackgrounds, false);
		o->setUserStyleSheetUrl(QUrl("file://"+S+"user.css"));
		o->setFontFamily(QWebSettings::SerifFont, "serif");
		o->setFontFamily(QWebSettings::CursiveFont, "serif");
		o->setFontFamily(QWebSettings::StandardFont, "serif");
		o->setFontFamily(QWebSettings::FixedFont, "monospace");
		o->setFontFamily(QWebSettings::FantasyFont, "sans-serif");
		o->setFontFamily(QWebSettings::SansSerifFont, "sans-serif");
		o->setThirdPartyCookiePolicy(QWebSettings::AlwaysBlockThirdPartyCookies);
		connect(new QShortcut(QKeySequence::Back,    this), SIGNAL(activated()), SLOT(back()));
		connect(new QShortcut(QKeySequence::Forward, this), SIGNAL(activated()), SLOT(forward()));
		connect(new QShortcut(QKeySequence::Refresh, this), SIGNAL(activated()), SLOT(reload()));
		connect(new QShortcut(QKeySequence::Print,   this), SIGNAL(activated()), SLOT(a_print()));
		connect(new QShortcut(QKeySequence::ZoomIn,  this), SIGNAL(activated()), SLOT(a_zoom_in()));
		connect(new QShortcut(QKeySequence::ZoomOut, this), SIGNAL(activated()), SLOT(a_zoom_out()));
	}
	void mouseReleaseEvent (QMouseEvent *e) {
		if (e->button()==Qt::MidButton) {
			QUrl u = p.frameAt(e->pos())->hitTestContent(e->pos()).linkUrl();
			if (!u.isEmpty()) {
				QProcess().startDetached(I, A+QStringList(u.toEncoded()));
				return e->accept();
			}
		}
		QWebView::mouseReleaseEvent(e);
	}
	void wheelEvent (QWheelEvent *e) {
		if (e->modifiers()==Qt::ControlModifier) {
			setZoomFactor(zoomFactor()*pow(1.0008, e->delta()));
			return e->accept();
		}
		QWebView::wheelEvent(e);
	}

	protected slots:
	void a_zoom_in () { setZoomFactor(zoomFactor()*1.1); }
	void a_zoom_out () { setZoomFactor(zoomFactor()/1.1); }
	void a_print () {
		QPrintDialog l (this);
		if (l.exec()==QDialog::Accepted) page()->currentFrame()->print(l.printer());
	}
};


/* ****************************************************************************
 *
 * MAIN WINDOW
 *
 * Populate status bar with read-only URL line and search line.
 */


class MainWindow: public QMainWindow {
	QLineEdit a;
	QLineEdit s;
	WebView v;
	Q_OBJECT

	public:
	MainWindow (QUrl u) {
		setCentralWidget(&v);
		v.setFocus();
		v.load(u);
		a.setReadOnly(true);
		a.setFont(QFont("monospace"));
		statusBar()->setSizeGripEnabled(false);
		statusBar()->addPermanentWidget(&a, 4);
		statusBar()->addPermanentWidget(&s, 1);
		connect(new QShortcut(QKeySequence::Find,     &v), SIGNAL(activated()),           SLOT(a_search_focus()));
		connect(new QShortcut(QKeySequence::FindNext, &v), SIGNAL(activated()),           SLOT(a_search_next()));
		connect(new QShortcut(QKeySequence("Return"), &v), SIGNAL(activated()),           SLOT(a_search_next()));
		connect(new QShortcut(QKeySequence("Escape"), &v), SIGNAL(activated()),           SLOT(a_search_unfocus()));
		connect(&s,                                        SIGNAL(textChanged(QString)),  SLOT(a_search_change(QString)));
		connect(&v,                                        SIGNAL(titleChanged(QString)), SLOT(a_title(QString)));
		connect(&v,                                        SIGNAL(urlChanged(QUrl)),      SLOT(a_url(QUrl)));
	}

	protected slots:
	void a_search_focus () {
		s.selectAll();
		s.setFocus();
	}
	void a_search_next () { v.page()->findText(s.text(), QWebPage::FindWrapsAroundDocument); }
	void a_search_unfocus () { v.setFocus(); }
	void a_search_change (QString s) {
		v.page()->findText("", QWebPage::HighlightAllOccurrences);
		v.page()->findText(s, QWebPage::HighlightAllOccurrences);
	}
	void a_title (QString t) { setWindowTitle(t); }
	void a_url (QUrl u) { a.setText(u.toString()); }
};


/* ****************************************************************************
 *
 * MAIN
 *
 * Process options and resource description. Run by Qt sugar.
 */


QStringList arg (QStringList v) {
	while (!v.isEmpty()) {
		if (v[0]=="-h") {
			H.append(v[1].toLocal8Bit());
			H.append(v[2].toLocal8Bit());
			A.append(v.takeAt(0));
			A.append(v.takeAt(0));
			A.append(v.takeAt(0));
		} else if (v[0]=="-u") {
			U = v[1];
			A.append(v.takeAt(0));
			A.append(v.takeAt(0));
		} else break;
	}
	return v;
}

QByteArray res (QStringList v) {
	QByteArray t (v.value(0,"").toLocal8Bit());
	if (t.contains("://")) return t;
	if (t.contains(".")) return "http://"+t;
	QByteArray s (v.value(1,"").toLocal8Bit());
	for (int i=2;i<v.size();i++) s += "+"+v[i].toLocal8Bit();
	QByteArray d ("https://duckduckgo.com/?q=");
	QFile f (S+"keys.txt");
	f.open(QIODevice::ReadOnly);
	while (f.bytesAvailable()) {
		QByteArray x = f.readLine().trimmed();
		int y = x.indexOf('=');
		if (y>0 && x.left(y)==t) {
			f.close();
			return x.right(x.size()-y-1)+s;
		} else if (y==0) d = x.right(x.size()-1);
	}
	f.close();
	return d+t+"+"+s;
}

int main (int argc, char *argv[]) {
	umask(S_IRWXG|S_IRWXO);
	QApplication x (argc, argv);
	x.setApplicationName("Squaw");
	x.setApplicationVersion("1.4");
	QStringList y = x.arguments();
	I = y.takeFirst();
	y = arg(y);

	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);

	MainWindow w (QUrl::fromEncoded(res(y)));
	w.show();
	return x.exec();
}

#include "squaw.moc"
