/*
 *  Copyright (C) 2010 Tuomo Penttinen, all rights reserved.
 *
 *  Author: Tuomo Penttinen <tp@herqq.org>
 *
 *  This file is part of Herqq UPnP (HUPnP) library.
 *
 *  Herqq UPnP is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Herqq UPnP 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 Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with Herqq UPnP. If not, see <http://www.gnu.org/licenses/>.
 */

#include "hupnp_global.h"
#include "hupnp_global_p.h"

#include "../socket/hendpoint.h"
#include "../dataelements/hproduct_tokens.h"

#include "../../utils/hlogger_p.h"

#include <QtXml/QDomElement>
#include <QtCore/QTextStream>
#include <QtCore/QMutexLocker>
#include <QtNetwork/QNetworkInterface>

#if defined(Q_OS_LINUX)
#include <sys/utsname.h>
#endif

/*!
 * \namespace Herqq The main namespace of Herqq libraries. This namespace contains
 * the global enumerations, typedefs, functions and classes that are used
 * and referenced throughout the Herqq libraries.
 *
 * \namespace Herqq::Upnp The namespace that contains all of the Herqq UPnP
 * core functionality.
 */

namespace Herqq
{

namespace Upnp
{

/*!
 * \defgroup hupnp_core HUPnP Core
 * HUPnP Core is a library that provides an implementation of the
 * <a href="http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf">
 * UPnP Device Architecture version 1.1 specification</a>.
 */

/*!
 * \defgroup hupnp_common Common
 * \ingroup hupnp_core
 */

void SetLoggingLevel(HLogLevel level)
{
    HLogger::setTraceLevel(static_cast<HLogger::HLogLevel>(level));
}

void EnableNonStdBehaviourWarnings(bool arg)
{
    HLogger::enableNonStdWarnings(arg);
}

QString readElementValue(
    const QString elementTagToSearch, const QDomElement& parentElement,
    bool* wasDefined)
{
    QDomElement element =
        parentElement.firstChildElement(elementTagToSearch);

    if (element.isNull())
    {
        if (wasDefined)
        {
            *wasDefined = false;
        }

        return "";
    }

    if (wasDefined)
    {
        *wasDefined = true;
    }

    return element.text();
}

QString toString(const QDomElement& e)
{
    QString buf;
    QTextStream ts(&buf, QIODevice::ReadWrite);
    e.save(ts, 0);

    return buf;
}

/*******************************************************************************
 * HSysInfo
 *******************************************************************************/
QScopedPointer<HSysInfo> HSysInfo::s_instance;
QMutex HSysInfo::s_initMutex;

HSysInfo::HSysInfo()
{
    createProductTokens();
    createLocalNetworks();
}

HSysInfo::~HSysInfo()
{
}

HSysInfo& HSysInfo::instance()
{
    QMutexLocker lock(&s_initMutex);

    if (s_instance)
    {
        return *s_instance;
    }

    s_instance.reset(new HSysInfo());
    return *s_instance;
}

void HSysInfo::createProductTokens()
{
#if defined(Q_OS_WIN)
    QString server = "MicrosoftWindows/";
    switch(QSysInfo::WindowsVersion)
    {
    case QSysInfo::WV_2000:
        server.append("5.0");
        break;
    case QSysInfo::WV_XP:
        server.append("5.1");
        break;
    case QSysInfo::WV_2003:
        server.append("5.2");
        break;
    case QSysInfo::WV_VISTA:
        server.append("6.0");
        break;
    case QSysInfo::WV_WINDOWS7:
        server.append("6.1");
        break;
    default:
        server.append("-1");
    }
#elif defined(Q_OS_DARWIN)
    QString server = "AppleMacOSX/";
    switch(QSysInfo::MacintoshVersion)
    {
    case QSysInfo::MV_10_3:
        server.append("10.3");
        break;
    case QSysInfo::MV_10_4:
        server.append("10.4");
        break;
    case QSysInfo::MV_10_5:
        server.append("10.5");
        break;
    case QSysInfo::MV_10_6:
        server.append("10.6");
        break;
    default:
        server.append("-1");
    }
#elif defined(Q_OS_LINUX)
    QString server;
    struct utsname sysinfo;
    if (!uname(&sysinfo))
    {
        server = QString("%1/%2").arg(sysinfo.sysname, sysinfo.release);
    }
    else
    {
        server = "Undefined/-1";
    }
#else
    QString server = "Undefined/-1";
#endif

    m_productTokens.reset(
        new HProductTokens(QString("%1 UPnP/1.1 HUPnP/0.7").arg(server)));
}

void HSysInfo::createLocalNetworks()
{
    foreach(const QNetworkInterface& iface, QNetworkInterface::allInterfaces())
    {
        QList<QNetworkAddressEntry> entries = iface.addressEntries();
        foreach(const QNetworkAddressEntry& entry, entries)
        {
            QHostAddress ha = entry.ip();
            if (ha.protocol() != QAbstractSocket::IPv4Protocol)
            {
                continue;
            }

            quint32 nm = entry.netmask().toIPv4Address();
            m_localNetworks.append(qMakePair(ha.toIPv4Address() & nm, nm));
        }
    }
}

bool HSysInfo::localNetwork(const QHostAddress& ha, quint32* retVal) const
{
    Q_ASSERT(retVal);

    QList<QPair<quint32, quint32> >::const_iterator ci;
    for(ci = m_localNetworks.begin(); ci != m_localNetworks.end(); ++ci)
    {
        if ((ha.toIPv4Address() & ci->second) == ci->first)
        {
            *retVal = ci->first;
            return true;
        }
    }

    return false;
}

bool HSysInfo::isLocalAddress(const QHostAddress& ha) const
{
    quint32 tmp;
    return localNetwork(ha, &tmp);
}

bool HSysInfo::areLocalAddresses(const QList<QHostAddress>& addresses) const
{
    QList<QHostAddress> localAddresses = QNetworkInterface::allAddresses();
    foreach(const QHostAddress& ha, addresses)
    {
        bool matched = false;
        foreach(const QHostAddress& localAddress, localAddresses)
        {
            if (localAddress == ha)
            {
                matched = true;
                break;
            }
        }

        if (!matched)
        {
            return false;
        }
    }

    return true;
}

HEndpoints convertHostAddressesToEndpoints(const QList<QHostAddress>& addrs)
{
    HEndpoints retVal;
    foreach(const QHostAddress& ha, addrs)
    {
        retVal.append(HEndpoint(ha));
    }
    return retVal;
}

bool verifySpecVersion(const QDomElement& rootElement, QString* err)
{
    QDomElement specVersionElement = rootElement.firstChildElement("specVersion");
    if (specVersionElement.isNull())
    {
        if (err)
        {
            *err = "Missing mandatory <specVersion> element";
        }
        return false;
    }

    QString minorVersion = readElementValue("minor", specVersionElement);
    QString majorVersion = readElementValue("major", specVersionElement);

    bool ok;
    qint32 major = majorVersion.toInt(&ok);
    if (!ok || major != 1)
    {
        if (err)
        {
            *err = "Major element of <specVersion> is not 1";
        }
        return false;
    }

    qint32 minor = minorVersion.toInt(&ok);
    if (!ok || (minor != 1 && minor != 0))
    {
        if (err)
        {
            *err = "minor element of <specVersion> is not 0 or 1";
        }
        return false;
    }

    return true;
}

qint32 readConfigId(const QDomElement& rootElement)
{
    bool ok = false;

    QString cid    = readElementValue("configId", rootElement);
    qint32 retVal  = cid.toInt(&ok);
    if (!ok || retVal < 0 || retVal > ((1 << 24)-1))
    {
        return 0;
    }

    return retVal;
}

bool verifyName(const QString& name, QString* err)
{
    HLOG(H_AT, H_FUN);

    if (name.isEmpty())
    {
        if (err)
        {
            *err = "[name] cannot be empty";
        }
        return false;
    }

    if (!name[0].isLetterOrNumber() && name[0] != '_')
    {
        if (err)
        {
            *err = QString("[name: %1] has invalid first character").arg(name);
        }
        return false;
    }

    foreach(const QChar& c, name)
    {
        if (!c.isLetterOrNumber() && c != '_' && c != '.')
        {
            if (err)
            {
                *err = QString(
                    "[name: %1] contains invalid character(s)").arg(name);
            }
            return false;
        }
    }

    if (name.size() > 32)
    {
        HLOG_WARN(QString("[name: %1] longer than 32 characters").arg(name));
    }

    return true;
}

QString urlsAsStr(const QList<QUrl>& urls)
{
    QString retVal;

    for(qint32 i = 0; i < urls.size(); ++i)
    {
        retVal.append(QString("#%1 %2\n").arg(
            QString::number(i), urls[i].toString()));
    }

    return retVal;
}

QString extractBaseUrl(const QString& url)
{
    if (url.endsWith('/'))
    {
        return url;
    }
    else if (!url.contains('/'))
    {
        return "";
    }

    QString base = url.section('/', 0, -2, QString::SectionIncludeTrailingSep);
    return base;
}

QUrl resolveUri(const QUrl& baseUrl, const QUrl& other)
{
    QString otherReq(extractRequestPart(other));

    if (otherReq.startsWith('/'))
    {
        return QString("%1%2").arg(extractHostPart(baseUrl), otherReq);
    }

    QString basePath(baseUrl.toString());

    if (!basePath.endsWith('/'))  { basePath.append('/'); }
    if (otherReq.startsWith('/')) { otherReq.remove(0, 1); }

    basePath.append(otherReq);

    return basePath;
}

QUrl appendUrls(const QUrl& baseUrl, const QUrl& other)
{
    QString otherReq(extractRequestPart(other));

    QString basePath(baseUrl.toString());

    if (!basePath.endsWith('/'))  { basePath.append('/'); }
    if (otherReq.startsWith('/')) { otherReq.remove(0, 1); }

    basePath.append(otherReq);

    return basePath;
}

}
}
