/*
 * dhcpcd-qt
 * Copyright 2014-2017 Roy Marples <roy@marples.name>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <QCursor>
#include <QDebug>
#include <QList>
#include <QSocketNotifier>
#include <QtGui>

#include <cerrno>

#include "config.h"
#include "dhcpcd-qt.h"
#include "dhcpcd-about.h"
#include "dhcpcd-preferences.h"
#include "dhcpcd-wi.h"
#include "dhcpcd-ifmenu.h"
#include "dhcpcd-ssidmenu.h"

DhcpcdQt::DhcpcdQt()
{

	createActions();
	createTrayIcon();

	onLine = carrier = false;
	lastStatus = DHC_UNKNOWN;
	aniTimer = new QTimer(this);
	connect(aniTimer, SIGNAL(timeout()), this, SLOT(animate()));
	notifier = NULL;
	retryOpenTimer = NULL;

	about = NULL;
	preferences = NULL;

	wis = new QList<DhcpcdWi *>();
	ssidMenu = NULL;

	qDebug("%s", "Connecting ...");
	con = dhcpcd_new();
	if (con == NULL) {
		qCritical("libdhcpcd: %s", strerror(errno));
		exit(EXIT_FAILURE);
		return;
	}
	dhcpcd_set_progname(con, "dhcpcd-qt");
	dhcpcd_set_status_callback(con, dhcpcd_status_cb, this);
	dhcpcd_set_if_callback(con, dhcpcd_if_cb, this);
	dhcpcd_wpa_set_scan_callback(con, dhcpcd_wpa_scan_cb, this);
	dhcpcd_wpa_set_status_callback(con, dhcpcd_wpa_status_cb, this);
	tryOpen();
}

DhcpcdQt::~DhcpcdQt()
{

	if (ssidMenu) {
		ssidMenu->setVisible(false);
		ssidMenu->deleteLater();
	}

	if (con != NULL) {
		dhcpcd_close(con);
		dhcpcd_free(con);
	}

	for (auto &wi : *wis)
		wi->deleteLater();
	delete wis;
}

DHCPCD_CONNECTION *DhcpcdQt::getConnection()
{

	return con;
}

QList<DhcpcdWi *> *DhcpcdQt::getWis()
{

	return wis;
}

const char * DhcpcdQt::signalStrengthIcon(DHCPCD_WI_SCAN *scan)
{

	if (scan == NULL)
		return "network-wireless-connected-00";
	if (scan->strength.value > 80)
		return "network-wireless-connected-100";
	if (scan->strength.value > 55)
		return "network-wireless-connected-75";
	if (scan->strength.value > 30)
		return "network-wireless-connected-50";
	if (scan->strength.value > 5)
		return "network-wireless-connected-25";
	return "network-wireless-connected-00";
}

DHCPCD_WI_SCAN * DhcpcdQt::getStrongestSignal()
{
	DHCPCD_WI_SCAN *scan, *scans, *s;
	DHCPCD_WPA *wpa;
	DHCPCD_IF *i;

	scan = NULL;
	for (auto &wi : *wis) {
		wpa = wi->getWpa();
		i = dhcpcd_wpa_if(wpa);
		scans = wi->getScans();
		for (s = scans; s; s = s->next) {
			if (dhcpcd_wi_associated(i, s) &&
			    (scan == NULL ||
			    s->strength.value > scan->strength.value))
				scan = s;
		}
	}
	return scan;
}

void DhcpcdQt::animate()
{
	const char *icon;
	DHCPCD_WI_SCAN *scan;

	scan = getStrongestSignal();

	if (onLine) {
		if (aniCounter++ > 6) {
			aniTimer->stop();
			aniCounter = 0;
			return;
		}

		if (aniCounter % 2 == 0)
			icon = scan ? "network-wireless-connected-00" :
			    "network-idle";
		else
			icon = scan ? DhcpcdQt::signalStrengthIcon(scan) :
			    "network-transmit-receive";
	} else {
		if (scan) {
			switch(aniCounter++) {
			case 0:
				icon = "network-wireless-connected-00";
				break;
			case 1:
				icon = "network-wireless-connected-25";
				break;
			case 2:
				icon = "network-wireless-connected-50";
				break;
			case 3:
				icon = "network-wireless-connected-75";
				break;
			default:
				icon = "network-wireless-connected-100";
				aniCounter = 0;
			}
		} else {
			switch(aniCounter++) {
			case 0:
				icon = "network-transmit";
				break;
			case 1:
				icon = "network-receive";
				break;
			default:
				icon = "network-idle";
				aniCounter = 0;
			}
		}
	}

	setIcon("status", icon);
}

void DhcpcdQt::updateOnline(bool showIf)
{
	bool isOn, isCarrier;
	char *msg;
	DHCPCD_IF *ifs, *i;
	QString msgs;

	isOn = isCarrier = false;
	ifs = dhcpcd_interfaces(con);
	for (i = ifs; i; i = i->next) {
		if (i->type == DHT_LINK) {
			if (i->up)
				isCarrier = true;
		} else {
			if (i->up)
				isOn = true;
		}
		msg = dhcpcd_if_message(i, NULL);
		if (msg) {
			if (showIf)
				qDebug("%s", msg);
			if (msgs.isEmpty())
				msgs = QString::fromLatin1(msg);
			else
				msgs += '\n' + QString::fromLatin1(msg);
			free(msg);
		} else if (showIf)
			qDebug("%s: %s", i->ifname, i->reason);
	}

	if (onLine != isOn || carrier != isCarrier) {
		onLine = isOn;
		carrier = isCarrier;
		aniTimer->stop();
		aniCounter = 0;
		if (isOn) {
			animate();
			aniTimer->start(300);
		} else if (isCarrier) {
			animate();
			aniTimer->start(500);
		} else
			setIcon("status", "network-offline");
	}

	trayIcon->setToolTip(msgs);
}

void DhcpcdQt::statusCallback(unsigned int status, const char *status_msg)
{

	qDebug("Status changed to %s", status_msg);
	if (status == DHC_DOWN) {
		aniTimer->stop();
		aniCounter = 0;
		onLine = carrier = false;
		setIcon("status", "network-offline");
		trayIcon->setToolTip(tr("Not connected to dhcpcd"));
		/* Close down everything */
		if (notifier) {
			notifier->setEnabled(false);
			notifier->deleteLater();
			notifier = NULL;
		}
		if (ssidMenu) {
			ssidMenu->deleteLater();
			ssidMenu = NULL;
		}
		preferencesAction->setEnabled(false);
		if (preferences) {
			preferences->deleteLater();
			preferences = NULL;
		}
	} else {
		bool refresh;

		if (lastStatus == DHC_UNKNOWN || lastStatus == DHC_DOWN) {
			qDebug("Connected to dhcpcd-%s", dhcpcd_version(con));
			refresh = true;
		} else
			refresh = lastStatus == DHC_OPENED ? true : false;
		updateOnline(refresh);
	}

	lastStatus = status;

	if (status == DHC_DOWN) {
		if (retryOpenTimer == NULL) {
			retryOpenTimer = new QTimer(this);
			connect(retryOpenTimer, SIGNAL(timeout()),
			    this, SLOT(tryOpen()));
			retryOpenTimer->start(DHCPCD_RETRYOPEN);
		}
	}
}

void DhcpcdQt::dhcpcd_status_cb(_unused DHCPCD_CONNECTION *con,
    unsigned int status, const char *status_msg, void *d)
{
	DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;

	dhcpcdQt->statusCallback(status, status_msg);
}

void DhcpcdQt::ifCallback(DHCPCD_IF *i)
{
	char *msg;
	bool new_msg;

	if (i->state == DHS_RENEW ||
	    i->state == DHS_STOP || i->state == DHS_STOPPED)
	{
		msg = dhcpcd_if_message(i, &new_msg);
		if (msg) {
			qDebug("%s", msg);
			if (new_msg) {
				QString t = tr("Network Event");
				QString m = msg;
				QString icon;

				if (i->up)
					icon = "network-transmit-receive";
				//else
				//	icon = "network-transmit";
				if (!i->up)
					icon = "network-offline";
				notify(t, m, icon);
			}
			free(msg);
		}
	}

	updateOnline(false);

	if (i->wireless) {
		for (auto &wi : *wis) {
			DHCPCD_WPA *wpa = wi->getWpa();
			if (dhcpcd_wpa_if(wpa) == i) {
				DHCPCD_WI_SCAN *scans;

				scans = dhcpcd_wi_scans(i);
				processScans(wi, scans);
			}
		}
	}
}

void DhcpcdQt::dhcpcd_if_cb(DHCPCD_IF *i, void *d)
{
	DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;

	dhcpcdQt->ifCallback(i);
}

DhcpcdWi *DhcpcdQt::findWi(DHCPCD_WPA *wpa)
{

	for (auto &wi : *wis) {
		if (wi->getWpa() == wpa)
			return wi;
	}
	return NULL;
}

void DhcpcdQt::processScans(DhcpcdWi *wi, DHCPCD_WI_SCAN *scans)
{

	/* Don't spam the user if we're already connected. */
	if (lastStatus != DHC_CONNECTED) {
		QString title = tr("New Access Point");
		QString txt;
		DHCPCD_WI_SCAN *s1, *s2;

		for (s1 = scans; s1; s1 = s1->next) {
			for (s2 = wi->getScans(); s2; s2 = s2->next) {
				if (strcmp(s1->ssid, s2->ssid) == 0)
					break;
			}
			if (s2 == NULL) {
				if (!txt.isEmpty()) {
					title = tr("New Access Points");
					txt += '\n';
				}
				txt += s1->ssid;
			}
		}
		if (!txt.isEmpty() &&
		    (ssidMenu == NULL || !ssidMenu->isVisible()))
			notify(title, txt, "network-wireless");
	}

	if (wi->setScans(scans) && ssidMenu && ssidMenu->isVisible())
		ssidMenu->popup(ssidMenuPos);
}

void DhcpcdQt::scanCallback(DHCPCD_WPA *wpa)
{
	DHCPCD_WI_SCAN *scans;
	int fd = dhcpcd_wpa_get_fd(wpa);
	DhcpcdWi *wi;

	wi = findWi(wpa);
	if (fd == -1) {
		qCritical("No fd for WPA");
		if (wi) {
			wis->removeOne(wi);
			wi->close();
			wi->deleteLater();
		}
		return;
	}

	DHCPCD_IF *i = dhcpcd_wpa_if(wpa);
	if (i == NULL) {
		qCritical("No interface for WPA");
		if (wi) {
			wis->removeOne(wi);
			wi->close();
			wi->deleteLater();
		}
		return;
	}

	qDebug("%s: Received scan results", i->ifname);
	scans = dhcpcd_wi_scans(i);
	if (wi == NULL) {
		wi = new DhcpcdWi(this, wpa);
		if (wi->open()) {
			wis->append(wi);
			wi->setScans(scans);
		} else {
			wi->close();
			wi->deleteLater();
		}
	} else
		processScans(wi, scans);

	if (!aniTimer->isActive()) {
		DHCPCD_WI_SCAN *scan;
		const char *icon;

		scan = getStrongestSignal();
		if (scan)
			icon = DhcpcdQt::signalStrengthIcon(scan);
		else if (onLine)
			icon = "network-transmit-receive";
		else
			icon = "network-offline";

		setIcon("status", icon);
	}
}

void DhcpcdQt::dhcpcd_wpa_scan_cb(DHCPCD_WPA *wpa, void *d)
{
	DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;

	dhcpcdQt->scanCallback(wpa);
}

void DhcpcdQt::wpaStatusCallback(DHCPCD_WPA *wpa,
    unsigned int status, const char *status_msg)
{
	DHCPCD_IF *i;

	i = dhcpcd_wpa_if(wpa);
	qDebug("%s: WPA status %s", i->ifname, status_msg);
	if (status == DHC_DOWN) {
		DhcpcdWi *wi = findWi(wpa);
		if (wi) {
			wis->removeOne(wi);
			wi->close();
			wi->deleteLater();
		}
	}
}

void DhcpcdQt::dhcpcd_wpa_status_cb(DHCPCD_WPA *wpa,
    unsigned int status, const char *status_msg, void *d)
{
	DhcpcdQt *dhcpcdQt = (DhcpcdQt *)d;

	dhcpcdQt->wpaStatusCallback(wpa, status, status_msg);
}

void DhcpcdQt::tryOpen() {
	int fd = dhcpcd_open(con), error;
	static int last_error;
	const char *errt;

	if (fd == -1) {
		if (errno != last_error) {
		        last_error = errno;
			errt = strerror(errno);
			qCritical("dhcpcd_open: %s", errt);
			trayIcon->setToolTip(
			    tr("Error connecting to dhcpcd: %1").arg(errt));
		}
		if (retryOpenTimer == NULL) {
			retryOpenTimer = new QTimer(this);
			connect(retryOpenTimer, SIGNAL(timeout()),
			    this, SLOT(tryOpen()));
			retryOpenTimer->start(DHCPCD_RETRYOPEN);
		}
		return;
	}

	notifier = new QSocketNotifier(fd, QSocketNotifier::Read);
	connect(notifier, SIGNAL(activated(int)), this, SLOT(dispatch()));

	preferencesAction->setEnabled(dhcpcd_privileged(con));

	if (retryOpenTimer != NULL) {
		retryOpenTimer->stop();
		retryOpenTimer->deleteLater();
		retryOpenTimer = NULL;
	}

	error = dhcpcd_error(con);
	if (error != 0) {
		if (error != last_error) {
			last_error = error;
			errt = strerror(error);
			qCritical("dhcpcd_error: %s", errt);
			trayIcon->setToolTip(
			    tr("Error communicating with dhcpcd: %1").arg(errt));
		}
		/* dhcpcd will need to be restarted */
		return;
	}

	/* Start listening to WPA events */
	dhcpcd_wpa_start(con);
}

void DhcpcdQt::dispatch()
{

	dhcpcd_dispatch(con);
}

void DhcpcdQt::notify(const QString &title, const QString &msg,
    const QString &icon)
{
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
	const QIcon i = getIcon("status", icon);
#else
	QSystemTrayIcon::MessageIcon i = QSystemTrayIcon::Information;

	if (icon.compare("network-offline") == 0)
		i = QSystemTrayIcon::Warning;
#endif
	trayIcon->showMessage(title, msg, i);
}

void DhcpcdQt::closeEvent(QCloseEvent *event)
{

	if (trayIcon->isVisible()) {
		hide();
		event->ignore();
	}
}

QIcon DhcpcdQt::getIcon(QString category, QString name)
{
	QIcon icon;

	if (QIcon::hasThemeIcon(name))
		icon = QIcon::fromTheme(name);
	else {
		/* For some reason, SVG no longer displays ... */
		QString file = QString("%1/hicolor/22x22/%2/%3.png")
		    .arg(ICONDIR, category, name);
		icon = QIcon(file);
	}

	return icon;
}

void DhcpcdQt::setIcon(QString category, QString name)
{
	QIcon icon = getIcon(category, name);

	trayIcon->setIcon(icon);
}

QIcon DhcpcdQt::icon()
{

	return getIcon("status", "network-transmit-receive");
}

void DhcpcdQt::menuDeleted(QMenu *menu)
{

	if (ssidMenu == menu)
		ssidMenu = NULL;
}

void DhcpcdQt::createSsidMenu()
{

	if (ssidMenu) {
		ssidMenu->deleteLater();
		ssidMenu = NULL;
	}
	if (wis->size() == 0)
		return;

	ssidMenu = new QMenu(this);
	if (wis->size() == 1)
		wis->first()->createMenu(ssidMenu);
	else {
		for (auto &wi : *wis)
			ssidMenu->addMenu(wi->createIfMenu(ssidMenu));
	}
	ssidMenuPos = QCursor::pos();
	ssidMenu->popup(ssidMenuPos);
}

void DhcpcdQt::iconActivated(QSystemTrayIcon::ActivationReason reason)
{

	if (reason == QSystemTrayIcon::Trigger)
		createSsidMenu();
}

void DhcpcdQt::dialogClosed(QDialog *dialog)
{

	if (dialog == about)
		about = NULL;
	else if (dialog == preferences)
		preferences = NULL;
}

void DhcpcdQt::showPreferences()
{

	if (preferences == NULL) {
		preferences = new DhcpcdPreferences(this);
		preferences->show();
	} else
		preferences->activateWindow();
}

void DhcpcdQt::showAbout()
{

	if (about == NULL) {
		about = new DhcpcdAbout(this);
		about->show();
	} else
		about->activateWindow();
}

void DhcpcdQt::createActions()
{

	preferencesAction = new QAction(tr("&Preferences"), this);
	preferencesAction->setIcon(QIcon::fromTheme("preferences-system-network"));
	preferencesAction->setEnabled(false);
	connect(preferencesAction, SIGNAL(triggered()),
	    this, SLOT(showPreferences()));

	aboutAction = new QAction(tr("&About"), this);
	aboutAction->setIcon(QIcon::fromTheme("help-about"));
	connect(aboutAction, SIGNAL(triggered()), this, SLOT(showAbout()));

	quitAction = new QAction(tr("&Quit"), this);
	quitAction->setIcon(QIcon::fromTheme("application-exit"));
	connect(quitAction, SIGNAL(triggered()), qApp, SLOT(quit()));

}

void DhcpcdQt::createTrayIcon()
{

        trayIconMenu = new QMenu(this);
	trayIconMenu->addAction(preferencesAction);
	trayIconMenu->addSeparator();
	trayIconMenu->addAction(aboutAction);
	trayIconMenu->addAction(quitAction);

	trayIcon = new QSystemTrayIcon(this);
	setIcon("status", "network-offline");
	trayIcon->setContextMenu(trayIconMenu);

	connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
	    this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));

	trayIcon->show();
}
