/*
   This file is part of the KDE libraries
   Copyright (c) 2005-2010 David Jarvie <djarvie@kde.org>
   Copyright (c) 2005 S.R.Haque <srhaque@iee.org>.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "config-date.h"
#include "config-prefix.h"

#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QFileSystemWatcher>
#include <QElapsedTimer>

#include "kglobal.h"
#include "kdebug.h"
#include "ksystemtimezone.h"
#include "kde_file.h"

#include <sys/time.h>
#include <time.h>

static const QString s_localtime = QString::fromLatin1("/etc/localtime");

QString zoneinfoDir()
{
    static const QStringList zoneinfodirs = QStringList()
        << QString::fromLatin1("/share/zoneinfo")
        << QString::fromLatin1("/lib/zoneinfo")
        << QString::fromLatin1("/usr/share/zoneinfo")
        << QString::fromLatin1("/usr/lib/zoneinfo")
        << QString::fromLatin1("/usr/local/share/zoneinfo")
        << QString::fromLatin1("/usr/local/lib/zoneinfo")
        << (QString::fromLatin1(KDEDIR) + QLatin1String("/share/zoneinfo"))
        << (QString::fromLatin1(KDEDIR) + QLatin1String("/lib/zoneinfo"));
    foreach (const QString &zoneinfodir, zoneinfodirs) {
        KDE_struct_stat statbuf;
        if (KDE::stat(zoneinfodir, &statbuf) == 0 && S_ISDIR(statbuf.st_mode)) {
            return zoneinfodir;
        }
    }
    return zoneinfodirs.last();
}

// Convert sHHMM or sHHMMSS to a floating point number of degrees.
static float convertCoordinate(const QByteArray &coordinate)
{
    int value = coordinate.toInt();
    int degrees = 0;
    int minutes = 0;
    int seconds = 0;

    if (coordinate.length() > 6) {
        degrees = value / 10000;
        value -= degrees * 10000;
        minutes = value / 100;
        value -= minutes * 100;
        seconds = value;
    } else {
        degrees = value / 100;
        value -= degrees * 100;
        minutes = value;
    }
    value = degrees * 3600 + minutes * 60 + seconds;
    return value / 3600.0;
}

static QList<float> splitZoneTabCoordinates(const QByteArray &zonetabcoordinates)
{
    QList<float> result;
    int startindex = 0;
    if (zonetabcoordinates.startsWith('+') || zonetabcoordinates.startsWith('-')) {
        startindex = 1;
    }
    int signindex = zonetabcoordinates.indexOf('+', startindex);
    if (signindex < startindex) {
        signindex = zonetabcoordinates.indexOf('-', startindex);
    }
    if (signindex < startindex) {
        return result;
    }
    const float latitude = convertCoordinate(zonetabcoordinates.mid(0, signindex));
    const float longitude = convertCoordinate(zonetabcoordinates.mid(signindex, zonetabcoordinates.size() - signindex));
    result.append(latitude);
    result.append(longitude);
    return result;
}

class KSystemTimeZonesPrivate : public QObject, public KTimeZones
{
    Q_OBJECT
public:
    KSystemTimeZonesPrivate();

    KTimeZone m_localtz;
    QString m_zoneinfoDir;
    KTimeZoneSource* m_tzfileSource;

private Q_SLOTS:
    void update(const QString &path);

private:
    QFileSystemWatcher *m_watcher;
};

KSystemTimeZonesPrivate::KSystemTimeZonesPrivate()
    : m_tzfileSource(nullptr),
    m_watcher(nullptr)
{
    update(QString());
}

void KSystemTimeZonesPrivate::update(const QString &path)
{
    Q_UNUSED(path);

#ifndef NDEBUG
    QElapsedTimer updatetimer;
    updatetimer.start();
#endif

    m_localtz = KTimeZone::utc();
    static const QByteArray kdezoneinfodir = qgetenv("KDE_ZONEINFO_DIR");
    m_zoneinfoDir = QFile::decodeName(kdezoneinfodir);
    if (m_zoneinfoDir.isEmpty()) {
        m_zoneinfoDir = zoneinfoDir();
    }
    delete m_tzfileSource;
    m_tzfileSource = new KTimeZoneSource(m_zoneinfoDir);

    KTimeZones::clear();

    const QString zonetab = m_zoneinfoDir + QLatin1String("/zone.tab");
    QFile zonetabfile(zonetab);
    if (!zonetabfile.open(QFile::ReadOnly)) {
        kWarning() << "Could not open zone.tab" << zonetab;
        return;
    }

    QStringList watchlist = QStringList()
        << zonetab
        << s_localtime;
    QString reallocaltime(s_localtime);
    QFileInfo localtimeinfo(s_localtime);
    if (localtimeinfo.isSymLink()) {
        reallocaltime = localtimeinfo.readLink();
        watchlist.append(reallocaltime);
    }
    if (m_watcher) {
        m_watcher->deleteLater();
    }
    m_watcher = new QFileSystemWatcher(this);
    m_watcher->addPaths(watchlist);
    connect(m_watcher, SIGNAL(fileChanged(QString)), this, SLOT(update(QString)));

    kDebug() << "Parsing" << zonetab;
    char zonecode[4];
    char zonecoordinates[32];
    char zonename[128];
    char zonecomment[1024];
    while (!zonetabfile.atEnd()) {
        const QByteArray zonetabline = zonetabfile.readLine().trimmed();
        if (zonetabline.isEmpty() || zonetabline.startsWith('#')) {
            continue;
        }

        ::memset(zonecode, '\0', sizeof(zonecode));
        ::memset(zonecoordinates, '\0', sizeof(zonecode));
        ::memset(zonename, '\0', sizeof(zonename));
        ::memset(zonecomment, '\0', sizeof(zonecomment));
        const int sscanfresult = sscanf(
            zonetabline.constData(),
            "%3s %31s %127s %1023[^\n]",
            zonecode, zonecoordinates, zonename, zonecomment
        );

        if (Q_UNLIKELY(sscanfresult < 3 || sscanfresult > 4)) {
            kWarning() << "Invalid zone.tab entry" << zonetabline;
            continue;
        }

        const QList<float> zonetabcoordinates = splitZoneTabCoordinates(
            QByteArray::fromRawData(zonecoordinates, qstrlen(zonecoordinates))
        );
        if (Q_UNLIKELY(zonetabcoordinates.size() != 2)) {
            kWarning() << "Invalid zone.tab coordinates" << zonetabline;
            continue;
        }

        const KTimeZone ktimezone(
            m_tzfileSource,
            QString::fromLatin1(zonename),
            QString::fromLatin1(zonecode),
            zonetabcoordinates.at(0), zonetabcoordinates.at(1),
            QString::fromLatin1(zonecomment)
        );
        KTimeZones::add(ktimezone);
    }

    if (localtimeinfo.isSymLink()) {
        const int zonediroffset = (m_zoneinfoDir.size() + 1);
        const QString localtz = reallocaltime.mid(zonediroffset, reallocaltime.size() - zonediroffset);
        m_localtz = KTimeZones::zone(localtz);
#ifndef NDEBUG
        kDebug() << "Zones update took" << updatetimer.elapsed() << "ms";
#endif
        return;
    }

    time_t ltime;
    ::time(&ltime);
    ::tzset();
    struct tm res;
    struct tm *t = ::localtime_r(&ltime, &res);
#if defined(HAVE_STRUCT_TM_TM_ZONE)
    const QByteArray localtz(t->tm_zone);
#else
    const QByteArray localtz(tzname[t->tm_isdst]);
#endif
    const KTimeZones::ZoneMap allzones = KTimeZones::zones();
    KTimeZones::ZoneMap::const_iterator it = allzones.constBegin();
    while (it != allzones.constEnd()) {
        if (it.value().abbreviations().contains(localtz)) {
            m_localtz = KTimeZones::zone(it.key());
#ifndef NDEBUG
            kDebug() << "Zones update took" << updatetimer.elapsed() << "ms";
#endif
            break;
        }
        it++;
    }
}

K_GLOBAL_STATIC(KSystemTimeZonesPrivate, s_systemzones);

/******************************************************************************/

KTimeZone KSystemTimeZones::local()
{
    const QByteArray envtz = qgetenv("TZ");
    if (!envtz.isEmpty()) {
        if (envtz.at(0) == ':') {
            return KSystemTimeZones::zone(QString::fromLocal8Bit(envtz.constData() + 1, envtz.size() - 1));
        }
        return KSystemTimeZones::zone(QString::fromLocal8Bit(envtz.constData(), envtz.size()));
    }

    return s_systemzones->m_localtz;
}

QString KSystemTimeZones::zoneinfoDir()
{
    return s_systemzones->m_zoneinfoDir;
}

KTimeZones *KSystemTimeZones::timeZones()
{
    return s_systemzones;
}

KTimeZone KSystemTimeZones::readZone(const QString &name)
{
    return KTimeZone(s_systemzones->m_tzfileSource, name);
}

const KTimeZones::ZoneMap KSystemTimeZones::zones()
{
    return s_systemzones->zones();
}

KTimeZone KSystemTimeZones::zone(const QString &name)
{
    return s_systemzones->zone(name);
}

#include "ksystemtimezone.moc"
