/*
   This file is part of the KDE libraries
   Copyright (c) 2005-2008 David Jarvie <djarvie@kde.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 "ktzfiletimezone.h"

#include <config-date.h>

#if HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#if HAVE_TIME_H
#include <time.h>
#endif

#include <QDebug>
#include <QFile>
#include <QDataStream>
#include <QVector>

// Use this replacement for QDateTime::setTime_t(uint) since our time
// values are signed.
static QDateTime fromTime_t(qint32 seconds)
{
    static const QDate epochDate(1970, 1, 1);
    static const QTime epochTime(0, 0, 0);
    int days = seconds / 86400;
    seconds -= days * 86400;
    if (seconds < 0) {
        --days;
        seconds += 86400;
    }
    return QDateTime(epochDate.addDays(days), epochTime.addSecs(seconds), Qt::UTC);
}

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

KTzfileTimeZoneBackend::KTzfileTimeZoneBackend(KTzfileTimeZoneSource *source, const QString &name,
        const QString &countryCode, float latitude, float longitude, const QString &comment)
    : KTimeZoneBackend(source, name, countryCode, latitude, longitude, comment)
{}

KTzfileTimeZoneBackend::~KTzfileTimeZoneBackend()
{}

KTimeZoneBackend *KTzfileTimeZoneBackend::clone() const
{
    return new KTzfileTimeZoneBackend(*this);
}

QByteArray KTzfileTimeZoneBackend::type() const
{
    return "KTzfileTimeZone";
}

bool KTzfileTimeZoneBackend::hasTransitions(const KTimeZone *caller) const
{
    Q_UNUSED(caller)
    return true;
}

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

KTzfileTimeZone::KTzfileTimeZone(KTzfileTimeZoneSource *source, const QString &name,
                                 const QString &countryCode, float latitude, float longitude,
                                 const QString &comment)
    : KTimeZone(new KTzfileTimeZoneBackend(source, name, countryCode, latitude, longitude, comment))
{}

KTzfileTimeZone::~KTzfileTimeZone()
{}

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

class KTzfileTimeZoneDataPrivate
{
public:
};

KTzfileTimeZoneData::KTzfileTimeZoneData()
//  : d(new KTzfileTimeZoneDataPrivate)
{ }

KTzfileTimeZoneData::KTzfileTimeZoneData(const KTzfileTimeZoneData &rhs)
    : KTimeZoneData(rhs)
//    d(new KTzfileTimeZoneDataPrivate)
{
}

KTzfileTimeZoneData::~KTzfileTimeZoneData()
{
//    delete d;
}

KTzfileTimeZoneData &KTzfileTimeZoneData::operator=(const KTzfileTimeZoneData &rhs)
{
    KTimeZoneData::operator=(rhs);
    return *this;
}

KTimeZoneData *KTzfileTimeZoneData::clone() const
{
    return new KTzfileTimeZoneData(*this);
}

bool KTzfileTimeZoneData::hasTransitions() const
{
    return true;
}

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

class KTzfileTimeZoneSourcePrivate
{
public:
    KTzfileTimeZoneSourcePrivate(const QString &loc)
        : location(loc) {}
    ~KTzfileTimeZoneSourcePrivate() {}

    QString location;
};

KTzfileTimeZoneSource::KTzfileTimeZoneSource(const QString &location)
    : d(new KTzfileTimeZoneSourcePrivate(location))
{
    if (location.length() > 1  &&  location.endsWith(QLatin1Char('/'))) {
        d->location.chop(1);
    }
}

KTzfileTimeZoneSource::~KTzfileTimeZoneSource()
{
    delete d;
}

QString KTzfileTimeZoneSource::location() const
{
    return d->location;
}

KTimeZoneData *KTzfileTimeZoneSource::parse(const KTimeZone &zone) const
{
    quint32 abbrCharCount;     // the number of characters of time zone abbreviation strings
    quint32 ttisgmtcnt;
    quint8  is;
    quint8  T_, Z_, i_, f_;    // tzfile identifier prefix

    QString path = zone.name();
    if (!path.startsWith(QLatin1Char('/'))) {
        if (d->location == QLatin1String("/")) {
            path.prepend(d->location);
        } else {
            path = d->location + QLatin1Char('/') + path;
        }
    }
    QFile f(path);
    if (!f.open(QIODevice::ReadOnly)) {
        qCritical() << "Cannot open " << f.fileName() << endl;
        return nullptr;
    }
    QDataStream str(&f);

    // Read the file type identifier
    str >> T_ >> Z_ >> i_ >> f_;
    if (T_ != 'T' || Z_ != 'Z' || i_ != 'i' || f_ != 'f') {
        qCritical() << "Not a TZFILE: " << f.fileName() << endl;
        return nullptr;
    }
    // Discard 16 bytes reserved for future use
    unsigned i;
    for (i = 0; i < 4; ++i) {
        str >> ttisgmtcnt;
    }

    KTzfileTimeZoneData *data = new KTzfileTimeZoneData;

    // Read the sizes of arrays held in the file
    quint32 nTransitionTimes;
    quint32 nLocalTimeTypes;
    quint32 nLeapSecondAdjusts;
    quint32 nIsStandard;
    quint32 nIsUtc;
    str >> nIsUtc
        >> nIsStandard
        >> nLeapSecondAdjusts
        >> nTransitionTimes
        >> nLocalTimeTypes
        >> abbrCharCount;
    // qDebug() << "header: " << nIsUtc << ", " << nIsStandard << ", " << nLeapSecondAdjusts << ", " <<
    //    nTransitionTimes << ", " << nLocalTimeTypes << ", " << abbrCharCount << endl;

    // Read the transition times, at which the rules for computing local time change
    struct TransitionTime {
        qint32 time;            // time (as returned by time(2)) at which the rules for computing local time change
        quint8 localTimeIndex;  // index into the LocalTimeType array
    };
//qDebug()<<"Reading zone "<<zone.name();
    TransitionTime *transitionTimes = new TransitionTime[nTransitionTimes];
    for (i = 0;  i < nTransitionTimes;  ++i) {
        str >> transitionTimes[i].time;
    }
    for (i = 0;  i < nTransitionTimes;  ++i) {
        str >> transitionTimes[i].localTimeIndex;
//qDebug() << "Transition time "<<i<<": "<<transitionTimes[i].time<<"   lt index="<<(int)transitionTimes[i].localTimeIndex;
    }

    // Read the local time types
    struct LocalTimeType {
        qint32 gmtoff;     // number of seconds to be added to UTC
        bool   isdst;      // whether tm_isdst should be set by localtime(3)
        quint8 abbrIndex;  // index into the list of time zone abbreviations
        bool   isutc;      // transition times are in UTC. If UTC, isstd is ignored.
        bool   isstd;      // if true, transition times are in standard time;
        // if false, transition times are in wall clock time,
        // i.e. standard time or daylight savings time
        // whichever is current before the transition
    };
    LocalTimeType *localTimeTypes = new LocalTimeType[nLocalTimeTypes];
    LocalTimeType *ltt = localTimeTypes;
    for (i = 0;  i < nLocalTimeTypes;  ++ltt, ++i) {
        str >> ltt->gmtoff;
        str >> is;
        ltt->isdst = (is != 0);
        str >> ltt->abbrIndex;
        // qDebug() << "local type: " << ltt->gmtoff << ", " << is << ", " << ltt->abbrIndex;
        ltt->isstd = false;   // default if no data
        ltt->isutc = false;   // default if no data
    }

    // Read the timezone abbreviations. They are stored as null terminated strings in
    // a character array.
    // Make sure we don't fall foul of maliciously coded time zone abbreviations.
    if (abbrCharCount > 64) {
        qCritical() << "excessive length for timezone abbreviations: " << abbrCharCount << endl;
        delete data;
        delete[] transitionTimes;
        delete[] localTimeTypes;
        return nullptr;
    }
    QByteArray array(abbrCharCount, 0);
    str.readRawData(array.data(), array.size());
    const char *abbrs = array.constData();
    if (abbrs[abbrCharCount - 1] != 0) {
        // These abbreviations are corrupt!
        qCritical() << "timezone abbreviations not null terminated: " << abbrs[abbrCharCount - 1] << endl;
        delete data;
        delete[] transitionTimes;
        delete[] localTimeTypes;
        return nullptr;
    }
    quint8 n = 0;
    QList<QByteArray> abbreviations;
    for (i = 0;  i < abbrCharCount;  ++n, i += strlen(abbrs + i) + 1) {
        abbreviations += QByteArray(abbrs + i);
        // Convert the LocalTimeTypes pointer to a sequential index
        ltt = localTimeTypes;
        for (unsigned j = 0;  j < nLocalTimeTypes;  ++ltt, ++j) {
            if (ltt->abbrIndex == i) {
                ltt->abbrIndex = n;
            }
        }
    }

    // Read the leap second adjustments
    qint32  t;
    quint32 s;
    QList<KTimeZone::LeapSeconds> leapChanges;
    for (i = 0;  i < nLeapSecondAdjusts;  ++i) {
        str >> t >> s;
        // qDebug() << "leap entry: " << t << ", " << s;
        // Don't use QDateTime::setTime_t() because it takes an unsigned argument
        leapChanges += KTimeZone::LeapSeconds(fromTime_t(t), static_cast<int>(s));
    }
    data->setLeapSecondChanges(leapChanges);

    // Read the standard/wall time indicators.
    // These are true if the transition times associated with local time types
    // are specified as standard time, false if wall clock time.
    for (i = 0;  i < nIsStandard;  ++i) {
        str >> is;
        localTimeTypes[i].isstd = (is != 0);
        // qDebug() << "standard: " << is;
    }

    // Read the UTC/local time indicators.
    // These are true if the transition times associated with local time types
    // are specified as UTC, false if local time.
    for (i = 0;  i < nIsUtc;  ++i) {
        str >> is;
        localTimeTypes[i].isutc = (is != 0);
        // qDebug() << "UTC: " << is;
    }

    // Find the starting offset from UTC to use before the first transition time.
    // This is first non-daylight savings local time type, or if there is none,
    // the first local time type.
    LocalTimeType *firstLtt = nullptr;
    ltt = localTimeTypes;
    for (i = 0;  i < nLocalTimeTypes;  ++ltt, ++i) {
        if (!ltt->isdst) {
            firstLtt = ltt;
            break;
        }
    }

    // Compile the time type data into a list of KTimeZone::Phase instances.
    // Also check for local time types which are identical (this does happen)
    // and use the same Phase instance for each.
    QByteArray abbrev;
    QList<KTimeZone::Phase> phases;
    QList<QByteArray> phaseAbbrevs;
    QVector<int> lttLookup(nLocalTimeTypes);
    ltt = localTimeTypes;
    for (i = 0;  i < nLocalTimeTypes;  ++ltt, ++i) {
        if (ltt->abbrIndex >= abbreviations.count()) {
            qCritical() << "KTzfileTimeZoneSource::parse(): abbreviation index out of range" << endl;
            abbrev = "???";
        } else {
            abbrev = abbreviations[ltt->abbrIndex];
        }
        // Check for an identical Phase
        int phindex = 0;
        for (int j = 0, jend = phases.count();  j < jend;  ++j, ++phindex) {
            if (ltt->gmtoff == phases[j].utcOffset()
                    && (bool)ltt->isdst == phases[j].isDst()
                    &&  abbrev == phaseAbbrevs[j]) {
                break;
            }
        }
        lttLookup[i] = phindex;
        if (phindex == phases.count()) {
            phases += KTimeZone::Phase(ltt->gmtoff, abbrev, ltt->isdst);
            phaseAbbrevs += abbrev;
        }
    }
    KTimeZone::Phase prePhase(firstLtt->gmtoff,
                              (firstLtt->abbrIndex < abbreviations.count() ? abbreviations[firstLtt->abbrIndex] : ""),
                              false);
    data->setPhases(phases, prePhase);

    // Compile the transition list
    QList<KTimeZone::Transition> transitions;
    TransitionTime *tt = transitionTimes;
    for (i = 0;  i < nTransitionTimes;  ++tt, ++i) {
        if (tt->localTimeIndex >= nLocalTimeTypes) {
            qCritical() << "KTzfileTimeZoneSource::parse(): transition ignored: local time type out of range: " << (int)tt->localTimeIndex << " > " << nLocalTimeTypes << endl;
            continue;
        }

        // Convert local transition times to UTC
        ltt = &localTimeTypes[tt->localTimeIndex];
        const KTimeZone::Phase phase = phases[lttLookup[tt->localTimeIndex]];
//qDebug(161) << "Transition time "<<i<<": "<<fromTime_t(tt->time)<<", offset="<<phase.utcOffset()/60;
        transitions += KTimeZone::Transition(fromTime_t(tt->time), phase);
    }
    data->setTransitions(transitions);
//for(int xxx=1;xxx<data->transitions().count();xxx++)
//qDebug(161) << "Transition time "<<xxx<<": "<<data->transitions()[xxx].time()<<", offset="<<data->transitions()[xxx].phase().utcOffset()/60;
    delete[] localTimeTypes;
    delete[] transitionTimes;

    return data;
}
