/**************************************************************************
* Otter Browser: Web browser controlled by the user, not vice-versa.
* Copyright (C) 2013 - 2016 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
* Copyright (C) 2014 Jan Bajer aka bajasoft <jbajer@gmail.com>
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*
**************************************************************************/

#include "Console.h"
#include "NetworkAutomaticProxy.h"

#include <QtCore/QCoreApplication>
#include <QtCore/QDate>
#include <QtNetwork/QHostInfo>
#include <QtNetwork/QNetworkInterface>

namespace Otter
{

QStringList PacUtils::m_months = QStringList() << QLatin1String("jan") << QLatin1String("feb") << QLatin1String("mar") << QLatin1String("apr") << QLatin1String("may") << QLatin1String("jun") << QLatin1String("jul") << QLatin1String("aug") << QLatin1String("sep") << QLatin1String("oct") << QLatin1String("nov") << QLatin1String("dec");
QStringList PacUtils::m_days = QStringList() << QLatin1String("mon") << QLatin1String("tue") << QLatin1String("wed") << QLatin1String("thu") << QLatin1String("fri") << QLatin1String("sat") << QLatin1String("sun");

PacUtils::PacUtils(QObject *parent) : QObject(parent)
{
}

void PacUtils::alert(const QString &message) const
{
	Console::addMessage(message, NetworkMessageCategory, WarningMessageLevel);
}

QString PacUtils::dnsResolve(const QString &host) const
{
	const QHostInfo hostInformation(QHostInfo::fromName(host));

	if (hostInformation.error() == QHostInfo::NoError && !hostInformation.addresses().isEmpty())
	{
		return hostInformation.addresses().first().toString();
	}

	return QString();
}

QString PacUtils::myIpAddress() const
{
	const QList<QHostAddress> addresses(QNetworkInterface::allAddresses());

	for (int i = 0; i < addresses.count(); ++i)
	{
		if (!addresses.at(i).isNull() && addresses.at(i) != QHostAddress::LocalHost && addresses.at(i) != QHostAddress::LocalHostIPv6 && addresses.at(i) != QHostAddress::Null && addresses.at(i) != QHostAddress::Broadcast && addresses.at(i) != QHostAddress::Any && addresses.at(i) != QHostAddress::AnyIPv6)
		{
			return addresses.at(i).toString();
		}
	}

	return QString();
}

int PacUtils::dnsDomainLevels(const QString &host) const
{
	if (host.startsWith(QLatin1String("www."), Qt::CaseInsensitive))
	{
		return host.mid(4).count(QLatin1Char('.'));
	}

	return host.count(QLatin1Char('.'));
}

bool PacUtils::isInNet(const QString &host, const QString &pattern, const QString &mask) const
{
	const QHostAddress address(host);
	const QHostAddress netaddress(pattern);
	const QHostAddress netmask(mask);

	return (address.protocol() == QAbstractSocket::IPv4Protocol && (netaddress.toIPv4Address() & netmask.toIPv4Address()) == (address.toIPv4Address() & netmask.toIPv4Address()));
}

bool PacUtils::isPlainHostName(const QString &host) const
{
	return !host.contains(QLatin1Char('.'));
}

bool PacUtils::isResolvable(const QString &host) const
{
	return (QHostInfo::fromName(host).error() == QHostInfo::NoError);
}

bool PacUtils::localHostOrDomainIs(const QString &host, QString domain) const
{
// address "google.com" or "maps.google.com" or "www.google.com", domain ".google.com" - return true
	const QString address(QUrl(host).host().toLower());

	domain = domain.toLower();

	if (!address.contains(QLatin1Char('.')) || address.endsWith(domain))
	{
		return true;
	}

	return (domain.indexOf(QLatin1Char('.')) == 0 && domain.replace(0, 1, QString()) == address);
}

bool PacUtils::dnsDomainIs(const QString &host, const QString &domain) const
{
	return host.contains(domain);
}

bool PacUtils::shExpMatch(const QString &string, const QString &expression) const
{
	return QRegExp(expression, Qt::CaseInsensitive, QRegExp::Wildcard).exactMatch(string);
}

bool PacUtils::weekdayRange(QString fromDay, QString toDay, const QString &gmt) const
{
	fromDay = fromDay.toLower();
	toDay = toDay.toLower();

	const int currentDay(((gmt.toLower() == QLatin1String("gmt")) ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime()).date().dayOfWeek());
	int fromDayNumber(-1);
	int toDayNumber(-1);

	for (int i = 0; i < m_days.count(); ++i)
	{
		if (fromDay == m_days.at(i))
		{
			fromDayNumber = (i + 1);
		}

		if (toDay == m_days.at(i))
		{
			toDayNumber = (i + 1);
		}
	}

	if (toDayNumber == -1)
	{
		toDayNumber = fromDayNumber;
	}

	return ((fromDayNumber != -1 && toDayNumber != -1) && isInRange(fromDayNumber, toDayNumber, currentDay));
}

bool PacUtils::dateRange(const QVariant &arg1, const QVariant &arg2, const QVariant &arg3, const QVariant &arg4, const QVariant &arg5, const QVariant &arg6, const QString &gmt) const
{
	QVariantList rawArguments;
	rawArguments << arg1 << arg2 << arg3 << arg4 << arg5 << arg6;

	QList<int> arguments;
	const QDate currentDay(((gmt.toLower() == QLatin1String("gmt")) ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime()).date());

	for (int i = 0; i < rawArguments.count(); ++i)
	{
		if (rawArguments.at(i).isNull())
		{
			break;
		}

		if (rawArguments.at(i).type() == QVariant::String)
		{
			const int month = (m_months.indexOf(rawArguments.at(i).toString().toLower()) + 1);

			if (month < 1)
			{
				return false;
			}

			arguments.append(month);
		}
		else
		{
			arguments.append(rawArguments.at(i).toInt());
		}
	}

	if (arguments.count() == 1 && arguments.at(0) > 1500)
	{
		return (currentDay.year() == arguments.at(0));
	}

	if (arguments.count() == 1 && rawArguments.at(0).type() == QVariant::Int)
	{
		return (currentDay.day() == arguments.at(0));
	}

	if (arguments.count() == 1)
	{
		return (currentDay.month() == arguments.at(0));
	}

	if (arguments.count() == 2 && arguments.at(0) > 1500 && arguments.at(1) > 1500)
	{
		return isInRange(arguments.at(0), arguments.at(1), currentDay.year());
	}

	if (arguments.count() == 2 && rawArguments.at(0).type() == QVariant::Int && rawArguments.at(1).type() == QVariant::Int)
	{
		return isInRange(arguments.at(0), arguments.at(1), currentDay.day());
	}

	if (arguments.count() == 2)
	{
		return isInRange(arguments.at(0), arguments.at(1), currentDay.month());
	}

	if (arguments.count() == 3)
	{
		const QDate dateOne(arguments.at(2), arguments.at(1), arguments.at(0));

		return isInRange(dateOne, dateOne, currentDay);
	}

	if (arguments.count() == 4 && arguments.at(1) > 1500 && arguments.at(3) > 1500)
	{
		return isInRange(QDate(arguments.at(1), arguments.at(0), currentDay.day()), QDate(arguments.at(3), arguments.at(2), currentDay.day()), currentDay);
	}

	if (arguments.count() == 4)
	{
		return isInRange(QDate(currentDay.year(), arguments.at(1), arguments.at(0)), QDate(currentDay.year(), arguments.at(3), arguments.at(2)), currentDay);
	}

	if (arguments.count() == 6)
	{
		return isInRange(QDate(arguments.at(2), arguments.at(1), arguments.at(0)), QDate(arguments.at(5), arguments.at(4), arguments.at(3)), currentDay);
	}

	return false;
}

bool PacUtils::timeRange(const QVariant &arg1, const QVariant &arg2, const QVariant &arg3, const QVariant &arg4, const QVariant &arg5, const QVariant &arg6, const QString &gmt) const
{
	QVariantList rawArguments;
	rawArguments << arg1 << arg2 << arg3 << arg4 << arg5 << arg6;

	QList<int> arguments;
	const QTime currentTime(((gmt.toLower() == QLatin1String("gmt")) ? QDateTime::currentDateTimeUtc() : QDateTime::currentDateTime()).time());

	for (int i = 0; i < rawArguments.count(); ++i)
	{
		if (rawArguments.at(i).isNull())
		{
			break;
		}

		if (rawArguments.at(i).type() != QVariant::Int)
		{
			return false;
		}

		arguments.append(rawArguments.at(i).toInt());
	}

	if (arguments.count() == 1)
	{
		return isInRange(arguments.at(0), arguments.at(0), currentTime.hour());
	}

	if (arguments.count() == 2)
	{
		return isInRange(arguments.at(0), arguments.at(1), currentTime.hour());
	}

	if (arguments.count() == 4)
	{
		return isInRange(QTime(arguments.at(0), arguments.at(1)), QTime(arguments.at(2), arguments.at(3)), currentTime);
	}

	if (arguments.count() == 6)
	{
		return isInRange(QTime(arguments.at(0), arguments.at(1), arguments.at(2)), QTime(arguments.at(3), arguments.at(4), arguments.at(5)), currentTime);
	}

	return false;
}

bool PacUtils::isInRange(const QVariant &valueOne, const QVariant &valueTwo, const QVariant &actualValue) const
{
	return (actualValue >= valueOne && actualValue <= valueTwo);
}

NetworkAutomaticProxy::NetworkAutomaticProxy(QObject *parent) : QObject(parent)
{
	m_engine.globalObject().setProperty(QLatin1String("PacUtils"), m_engine.newQObject(new PacUtils(this)));

	QStringList functions;
	functions << QLatin1String("alert") << QLatin1String("dnsResolve") << QLatin1String("myIpAddress") << QLatin1String("dnsDomainLevels") << QLatin1String("isInNet") << QLatin1String("isPlainHostName") << QLatin1String("isResolvable") << QLatin1String("localHostOrDomainIs") << QLatin1String("dnsDomainIs") << QLatin1String("shExpMatch") << QLatin1String("weekdayRange") << QLatin1String("dateRange") << QLatin1String("timeRange");

	for (int i = 0; i < functions.count(); ++i)
	{
		m_engine.evaluate(QStringLiteral("function %1() { return PacUtils.%1.apply(null, arguments); }").arg(functions.at(i))).isError();
	}

	m_proxies.insert(QLatin1String("ERROR"), QList<QNetworkProxy>() << QNetworkProxy(QNetworkProxy::DefaultProxy));
	m_proxies.insert(QLatin1String("DIRECT"), QList<QNetworkProxy>() << QNetworkProxy(QNetworkProxy::NoProxy));
}

QList<QNetworkProxy> NetworkAutomaticProxy::getProxy(const QString &url, const QString &host)
{
	QJSValueList arguments;
	arguments << m_engine.toScriptValue(url) << m_engine.toScriptValue(host);

	const QJSValue result = m_findProxy.call(arguments);

	if (result.isError())
	{
		return m_proxies[QLatin1String("ERROR")];
	}

	const QString configuration(result.toString().remove(QLatin1Char(' ')));

	if (!m_proxies.value(configuration).isEmpty())
	{
		return m_proxies[configuration];
	}

// proxy format: "PROXY host:port; PROXY host:port", "PROXY host:port; SOCKS host:port" etc.
// can be combination of DIRECT, PROXY, SOCKS
	const QStringList proxies(configuration.split(QLatin1Char(';')));
	QList<QNetworkProxy> proxiesForQuery;

	for (int i = 0; i < proxies.count(); ++i)
	{
		const QStringList proxy(proxies.at(i).split(QLatin1Char(':')));
		QString proxyHost(proxy.at(0));

		if (proxy.count() == 2 && proxyHost.indexOf(QLatin1String("PROXY"), Qt::CaseInsensitive) == 0)
		{
			proxiesForQuery << QNetworkProxy(QNetworkProxy::HttpProxy, proxyHost.replace(0, 5, QString()), proxy.at(1).toInt());

			continue;
		}

		if (proxy.count() == 2 && proxyHost.indexOf(QLatin1String("SOCKS"), Qt::CaseInsensitive) == 0)
		{
			proxiesForQuery << QNetworkProxy(QNetworkProxy::Socks5Proxy, proxyHost.replace(0, 5, QString()), proxy.at(1).toInt());

			continue;
		}

		if (proxy.count() == 1 && proxyHost.indexOf(QLatin1String("DIRECT"), Qt::CaseInsensitive) == 0)
		{
			proxiesForQuery << QNetworkProxy(QNetworkProxy::NoProxy);

			continue;
		}

		Console::addMessage(QCoreApplication::translate("main", "Failed to parse entry of proxy auto-config (PAC): %1").arg(proxies.at(i)), NetworkMessageCategory, ErrorMessageLevel);

		return m_proxies[QLatin1String("ERROR")];
	}

	m_proxies.insert(configuration, proxiesForQuery);

	return m_proxies[configuration];
}

bool NetworkAutomaticProxy::setup(const QString &script)
{
	if (m_engine.evaluate(script).isError())
	{
		return false;
	}

	m_findProxy = m_engine.globalObject().property(QLatin1String("FindProxyForURL"));

	return m_findProxy.isCallable();
}

}
