/*
  This file is part of the kcalcore library.

  Copyright (c) 2005-2007 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 <config-kcalcore.h>

#include "icaltimezones.h"
#include "icalformat.h"
#include "icalformat_p.h"
#include "recurrence.h"
#include "recurrencerule.h"

#include "kcalcore_debug.h"
#include <KDateTime>
#include <KSystemTimeZone>

#include <QtCore/QDateTime>
#include <QtCore/QFile>
#include <QtCore/QTextStream>

extern "C" {
#include <libical/ical.h>
#include <icaltimezone.h>
}

#if defined(HAVE_UUID_UUID_H)
#include <uuid/uuid.h>
#endif

using namespace KCalCore;

// Minimum repetition counts for VTIMEZONE RRULEs
static const int minRuleCount = 5;   // for any RRULE
static const int minPhaseCount = 8;  // for separate STANDARD/DAYLIGHT component

// Convert an ical time to QDateTime, preserving the UTC indicator
static QDateTime toQDateTime(const icaltimetype &t)
{
    return QDateTime(QDate(t.year, t.month, t.day),
                     QTime(t.hour, t.minute, t.second),
                     (t.is_utc ? Qt::UTC : Qt::LocalTime));
}

// Maximum date for time zone data.
// It's not sensible to try to predict them very far in advance, because
// they can easily change. Plus, it limits the processing required.
static QDateTime MAX_DATE()
{
    static QDateTime dt;
    if (!dt.isValid()) {
        dt = QDateTime(QDate::currentDate().addYears(20), QTime(0, 0, 0));
    }
    return dt;
}

static icaltimetype writeLocalICalDateTime(const QDateTime &utc, int offset)
{
    const QDateTime local = utc.addSecs(offset);
    icaltimetype t = icaltime_null_time();
    t.year = local.date().year();
    t.month = local.date().month();
    t.day = local.date().day();
    t.hour = local.time().hour();
    t.minute = local.time().minute();
    t.second = local.time().second();
    t.is_date = 0;
    t.zone = nullptr;
    t.is_utc = 0;
    return t;
}

namespace KCalCore
{

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

//@cond PRIVATE
class ICalTimeZonesPrivate
{
public:
    ICalTimeZonesPrivate() {}
    ICalTimeZones::ZoneMap zones;
};
//@endcond

ICalTimeZones::ICalTimeZones()
    : d(new ICalTimeZonesPrivate)
{
}

ICalTimeZones::ICalTimeZones(const ICalTimeZones &rhs)
    : d(new ICalTimeZonesPrivate())
{
    d->zones = rhs.d->zones;
}

ICalTimeZones &ICalTimeZones::operator=(const ICalTimeZones &rhs)
{
    // check for self assignment
    if (&rhs == this) {
        return *this;
    }
    d->zones = rhs.d->zones;
    return *this;
}

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

const ICalTimeZones::ZoneMap ICalTimeZones::zones() const
{
    return d->zones;
}

bool ICalTimeZones::add(const ICalTimeZone &zone)
{
    if (!zone.isValid()) {
        return false;
    }
    if (d->zones.constFind(zone.name()) != d->zones.cend()) {
        return false;    // name already exists
    }

    d->zones.insert(zone.name(), zone);
    return true;
}

ICalTimeZone ICalTimeZones::remove(const ICalTimeZone &zone)
{
    if (zone.isValid()) {
        for (ZoneMap::Iterator it = d->zones.begin(), end = d->zones.end();  it != end;  ++it) {
            if (it.value() == zone) {
                d->zones.erase(it);
                return (zone == ICalTimeZone::utc()) ? ICalTimeZone() : zone;
            }
        }
    }
    return ICalTimeZone();
}

ICalTimeZone ICalTimeZones::remove(const QString &name)
{
    if (!name.isEmpty()) {
        ZoneMap::Iterator it = d->zones.find(name);
        if (it != d->zones.end()) {
            const ICalTimeZone zone = it.value();
            d->zones.erase(it);
            return (zone == ICalTimeZone::utc()) ? ICalTimeZone() : zone;
        }
    }
    return ICalTimeZone();
}

void ICalTimeZones::clear()
{
    d->zones.clear();
}

int ICalTimeZones::count()
{
    return d->zones.count();
}

ICalTimeZone ICalTimeZones::zone(const QString &name) const
{
    if (!name.isEmpty()) {
        ZoneMap::ConstIterator it = d->zones.constFind(name);
        if (it != d->zones.constEnd()) {
            return it.value();
        }
    }
    return ICalTimeZone();   // error
}

ICalTimeZone ICalTimeZones::zone(const ICalTimeZone &zone) const
{
    if (zone.isValid()) {
        QMapIterator<QString, ICalTimeZone> it(d->zones);
        while (it.hasNext()) {
            it.next();
            const ICalTimeZone tz = it.value();
            const QList<KTimeZone::Transition> list1 = tz.transitions();
            const QList<KTimeZone::Transition> list2 = zone.transitions();
            if (list1.size() == list2.size()) {
                int i = 0;
                int matches = 0;
                for (; i < list1.size(); ++i) {
                    const KTimeZone::Transition t1 = list1[ i ];
                    const KTimeZone::Transition t2 = list2[ i ];
                    if ((t1.time() == t2.time()) &&
                            (t1.phase().utcOffset() == t2.phase().utcOffset()) &&
                            (t1.phase().isDst() == t2.phase().isDst())) {
                        matches++;
                    }
                }
                if (matches == i) {
                    // Existing zone has all the transitions of the given zone.
                    return tz;
                }
            }
        }
    }
    return ICalTimeZone(); // not found
}

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

ICalTimeZoneBackend::ICalTimeZoneBackend()
    : KTimeZoneBackend()
{}

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

ICalTimeZoneBackend::ICalTimeZoneBackend(const KTimeZone &tz, const QDate &earliest)
    : KTimeZoneBackend(nullptr, tz.name(), tz.countryCode(), tz.latitude(), tz.longitude(), tz.comment())
{
    Q_UNUSED(earliest);
}

ICalTimeZoneBackend::~ICalTimeZoneBackend()
{}

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

QByteArray ICalTimeZoneBackend::type() const
{
    return "ICalTimeZone";
}

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

void ICalTimeZoneBackend::virtual_hook(int id, void *data)
{
    Q_UNUSED(id);
    Q_UNUSED(data);
}

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

ICalTimeZone::ICalTimeZone()
    : KTimeZone(new ICalTimeZoneBackend())
{}

ICalTimeZone::ICalTimeZone(ICalTimeZoneSource *source, const QString &name,
                           ICalTimeZoneData *data)
    : KTimeZone(new ICalTimeZoneBackend(source, name))
{
    setData(data);
}

ICalTimeZone::ICalTimeZone(const KTimeZone &tz, const QDate &earliest)
    : KTimeZone(new ICalTimeZoneBackend(nullptr, tz.name(), tz.countryCode(),
                                        tz.latitude(), tz.longitude(),
                                        tz.comment()))
{
    const KTimeZoneData *data = tz.data(true);
    if (data) {
        const ICalTimeZoneData *icaldata = dynamic_cast<const ICalTimeZoneData *>(data);
        if (icaldata) {
            setData(new ICalTimeZoneData(*icaldata));
        } else {
            setData(new ICalTimeZoneData(*data, tz, earliest));
        }
    }
}

ICalTimeZone::~ICalTimeZone()
{}

QString ICalTimeZone::city() const
{
    const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData *>(data());
    return dat ? dat->city() : QString();
}

QByteArray ICalTimeZone::url() const
{
    const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData *>(data());
    return dat ? dat->url() : QByteArray();
}

QDateTime ICalTimeZone::lastModified() const
{
    const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData *>(data());
    return dat ? dat->lastModified() : QDateTime();
}

QByteArray ICalTimeZone::vtimezone() const
{
    const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData *>(data());
    return dat ? dat->vtimezone() : QByteArray();
}

icaltimezone *ICalTimeZone::icalTimezone() const
{
    const ICalTimeZoneData *dat = static_cast<const ICalTimeZoneData *>(data());
    return dat ? dat->icalTimezone() : nullptr;
}

bool ICalTimeZone::update(const ICalTimeZone &other)
{
    if (!updateBase(other)) {
        return false;
    }

    KTimeZoneData *otherData = other.data() ? other.data()->clone() : nullptr;
    setData(otherData, other.source());
    return true;
}

ICalTimeZone ICalTimeZone::utc()
{
    static ICalTimeZone utcZone;
    if (!utcZone.isValid()) {
        ICalTimeZoneSource tzs;
        utcZone = tzs.parse(icaltimezone_get_utc_timezone());
    }
    return utcZone;
}

void ICalTimeZone::virtual_hook(int id, void *data)
{
    Q_UNUSED(id);
    Q_UNUSED(data);
}
/******************************************************************************/

//@cond PRIVATE
class ICalTimeZoneDataPrivate
{
public:
    ICalTimeZoneDataPrivate() : icalComponent(nullptr) {}

    ~ICalTimeZoneDataPrivate()
    {
        if (icalComponent) {
            icalcomponent_free(icalComponent);
        }
    }

    icalcomponent *component() const
    {
        return icalComponent;
    }
    void setComponent(icalcomponent *c)
    {
        if (icalComponent) {
            icalcomponent_free(icalComponent);
        }
        icalComponent = c;
    }

    QString       location;       // name of city for this time zone
    QByteArray    url;            // URL of published VTIMEZONE definition (optional)
    QDateTime     lastModified;   // time of last modification of the VTIMEZONE component (optional)

private:
    icalcomponent *icalComponent; // ical component representing this time zone
};
//@endcond

ICalTimeZoneData::ICalTimeZoneData()
    : d(new ICalTimeZoneDataPrivate())
{
}

ICalTimeZoneData::ICalTimeZoneData(const ICalTimeZoneData &rhs)
    : KTimeZoneData(rhs),
      d(new ICalTimeZoneDataPrivate())
{
    d->location = rhs.d->location;
    d->url = rhs.d->url;
    d->lastModified = rhs.d->lastModified;
    d->setComponent(icalcomponent_new_clone(rhs.d->component()));
}

ICalTimeZoneData::ICalTimeZoneData(const KTimeZoneData &rhs,
                                   const KTimeZone &tz, const QDate &earliest)
    : KTimeZoneData(rhs),
      d(new ICalTimeZoneDataPrivate())
{
    // VTIMEZONE RRULE types
    enum {
        DAY_OF_MONTH          = 0x01,
        WEEKDAY_OF_MONTH      = 0x02,
        LAST_WEEKDAY_OF_MONTH = 0x04
    };

    if (tz.type() == "KSystemTimeZone") {
        // Try to fetch a system time zone in preference, on the grounds
        // that system time zones are more likely to be up to date than
        // built-in libical ones.
        icalcomponent *c = nullptr;
        const KTimeZone ktz = KSystemTimeZones::readZone(tz.name());
        if (ktz.isValid()) {
            if (ktz.data(true)) {
                const ICalTimeZone icaltz(ktz, earliest);
                icaltimezone *itz = icaltz.icalTimezone();
                if (itz) {
                    c = icalcomponent_new_clone(icaltimezone_get_component(itz));
                    icaltimezone_free(itz, 1);
                }
            }
        }
        if (!c) {
            // Try to fetch a built-in libical time zone.
            icaltimezone *itz = icaltimezone_get_builtin_timezone(tz.name().toUtf8());
            c = icalcomponent_new_clone(icaltimezone_get_component(itz));
        }
        if (c) {
            // TZID in built-in libical time zones has a standard prefix.
            // To make the VTIMEZONE TZID match TZID references in incidences
            // (as required by RFC2445), strip off the prefix.
            icalproperty *prop = icalcomponent_get_first_property(c, ICAL_TZID_PROPERTY);
            if (prop) {
                icalvalue *value = icalproperty_get_value(prop);
                const char *tzid = icalvalue_get_text(value);
                const QByteArray icalprefix = ICalTimeZoneSource::icalTzidPrefix();
                const int len = icalprefix.size();
                if (!strncmp(icalprefix, tzid, len)) {
                    const char *s = strchr(tzid + len, '/');      // find third '/'
                    if (s) {
                        const QByteArray tzidShort(s + 1);   // deep copy (needed by icalvalue_set_text())
                        icalvalue_set_text(value, tzidShort);

                        // Remove the X-LIC-LOCATION property, which is only used by libical
                        prop = icalcomponent_get_first_property(c, ICAL_X_PROPERTY);
                        const char *xname = icalproperty_get_x_name(prop);
                        if (xname && !strcmp(xname, "X-LIC-LOCATION")) {
                            icalcomponent_remove_property(c, prop);
                            icalproperty_free(prop);
                        }
                    }
                }
            }
        }
        d->setComponent(c);
    } else {
        // Write the time zone data into an iCal component
        icalcomponent *tzcomp = icalcomponent_new(ICAL_VTIMEZONE_COMPONENT);
        icalcomponent_add_property(tzcomp, icalproperty_new_tzid(tz.name().toUtf8()));
//    icalcomponent_add_property(tzcomp, icalproperty_new_location( tz.name().toUtf8() ));

        // Compile an ordered list of transitions so that we can know the phases
        // which occur before and after each transition.
        QList<KTimeZone::Transition> transits = transitions();
        if (transits.isEmpty()) {
            // If there is no way to compile a complete list of transitions
            // transitions() can return an empty list
            // In that case try get one transition to write a valid VTIMEZONE entry.
            if (transits.isEmpty()) {
                qCDebug(KCALCORE_LOG) << "No transition information available VTIMEZONE will be invalid.";
            }
        }
        if (earliest.isValid()) {
            // Remove all transitions earlier than those we are interested in
            for (int i = 0, end = transits.count();  i < end;  ++i) {
                if (transits.at(i).time().date() >= earliest) {
                    if (i > 0) {
                        transits.erase(transits.begin(), transits.begin() + i);
                    }
                    break;
                }
            }
        }
        int trcount = transits.count();
        QVector<bool> transitionsDone(trcount);
        transitionsDone.fill(false);

        // Go through the list of transitions and create an iCal component for each
        // distinct combination of phase after and UTC offset before the transition.
        icaldatetimeperiodtype dtperiod;
        dtperiod.period = icalperiodtype_null_period();
        for (;;) {
            int i = 0;
            for (;  i < trcount && transitionsDone[i];  ++i) {
                ;
            }
            if (i >= trcount) {
                break;
            }
            // Found a phase combination which hasn't yet been processed
            const int preOffset = (i > 0) ?
                                  transits.at(i - 1).phase().utcOffset() :
                                  rhs.previousUtcOffset();
            const KTimeZone::Phase phase = transits.at(i).phase();
            if (phase.utcOffset() == preOffset) {
                transitionsDone[i] = true;
                while (++i < trcount) {
                    if (transitionsDone[i] ||
                            transits.at(i).phase() != phase ||
                            transits.at(i - 1).phase().utcOffset() != preOffset) {
                        continue;
                    }
                    transitionsDone[i] = true;
                }
                continue;
            }
            icalcomponent *phaseComp =
                icalcomponent_new(phase.isDst() ? ICAL_XDAYLIGHT_COMPONENT : ICAL_XSTANDARD_COMPONENT);
            const QList<QByteArray> abbrevs = phase.abbreviations();
            for (int a = 0, aend = abbrevs.count();  a < aend;  ++a) {
                icalcomponent_add_property(phaseComp,
                                           icalproperty_new_tzname(
                                               static_cast<const char *>(abbrevs[a])));
            }
            if (!phase.comment().isEmpty()) {
                icalcomponent_add_property(phaseComp,
                                           icalproperty_new_comment(phase.comment().toUtf8()));
            }
            icalcomponent_add_property(phaseComp,
                                       icalproperty_new_tzoffsetfrom(preOffset));
            icalcomponent_add_property(phaseComp,
                                       icalproperty_new_tzoffsetto(phase.utcOffset()));
            // Create a component to hold initial RRULE if any, plus all RDATEs
            icalcomponent *phaseComp1 = icalcomponent_new_clone(phaseComp);
            icalcomponent_add_property(phaseComp1,
                                       icalproperty_new_dtstart(
                                           writeLocalICalDateTime(transits.at(i).time(),
                                                   preOffset)));
            bool useNewRRULE = false;

            // Compile the list of UTC transition dates/times, and check
            // if the list can be reduced to an RRULE instead of multiple RDATEs.
            QTime time;
            QDate date;
            int year = 0, month = 0, daysInMonth = 0, dayOfMonth = 0; // avoid compiler warnings
            int dayOfWeek = 0;      // Monday = 1
            int nthFromStart = 0;   // nth (weekday) of month
            int nthFromEnd = 0;     // nth last (weekday) of month
            int newRule;
            int rule = 0;
            QList<QDateTime> rdates;// dates which (probably) need to be written as RDATEs
            QList<QDateTime> times;
            QDateTime qdt = transits.at(i).time();     // set 'qdt' for start of loop
            times += qdt;
            transitionsDone[i] = true;
            do {
                if (!rule) {
                    // Initialise data for detecting a new rule
                    rule = DAY_OF_MONTH | WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH;
                    time = qdt.time();
                    date = qdt.date();
                    year = date.year();
                    month = date.month();
                    daysInMonth = date.daysInMonth();
                    dayOfWeek = date.dayOfWeek();   // Monday = 1
                    dayOfMonth = date.day();
                    nthFromStart = (dayOfMonth - 1) / 7 + 1;     // nth (weekday) of month
                    nthFromEnd = (daysInMonth - dayOfMonth) / 7 + 1;     // nth last (weekday) of month
                }
                if (++i >= trcount) {
                    newRule = 0;
                    times += QDateTime();   // append a dummy value since last value in list is ignored
                } else {
                    if (transitionsDone[i] ||
                            transits.at(i).phase() != phase ||
                            transits.at(i - 1).phase().utcOffset() != preOffset) {
                        continue;
                    }
                    transitionsDone[i] = true;
                    qdt = transits.at(i).time();
                    if (!qdt.isValid()) {
                        continue;
                    }
                    newRule = rule;
                    times += qdt;
                    date = qdt.date();
                    if (qdt.time() != time ||
                            date.month() != month ||
                            date.year() != ++year) {
                        newRule = 0;
                    } else {
                        const int day = date.day();
                        if ((newRule & DAY_OF_MONTH) && day != dayOfMonth) {
                            newRule &= ~DAY_OF_MONTH;
                        }
                        if (newRule & (WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH)) {
                            if (date.dayOfWeek() != dayOfWeek) {
                                newRule &= ~(WEEKDAY_OF_MONTH | LAST_WEEKDAY_OF_MONTH);
                            } else {
                                if ((newRule & WEEKDAY_OF_MONTH) &&
                                        (day - 1) / 7 + 1 != nthFromStart) {
                                    newRule &= ~WEEKDAY_OF_MONTH;
                                }
                                if ((newRule & LAST_WEEKDAY_OF_MONTH) &&
                                        (daysInMonth - day) / 7 + 1 != nthFromEnd) {
                                    newRule &= ~LAST_WEEKDAY_OF_MONTH;
                                }
                            }
                        }
                    }
                }
                if (!newRule) {
                    // The previous rule (if any) no longer applies.
                    // Write all the times up to but not including the current one.
                    // First check whether any of the last RDATE values fit this rule.
                    int yr = times[0].date().year();
                    while (!rdates.isEmpty()) {
                        qdt = rdates.last();
                        date = qdt.date();
                        if (qdt.time() != time  ||
                                date.month() != month ||
                                date.year() != --yr) {
                            break;
                        }
                        const int day = date.day();
                        if (rule & DAY_OF_MONTH) {
                            if (day != dayOfMonth) {
                                break;
                            }
                        } else {
                            if (date.dayOfWeek() != dayOfWeek ||
                                    ((rule & WEEKDAY_OF_MONTH) &&
                                     (day - 1) / 7 + 1 != nthFromStart) ||
                                    ((rule & LAST_WEEKDAY_OF_MONTH) &&
                                     (daysInMonth - day) / 7 + 1 != nthFromEnd)) {
                                break;
                            }
                        }
                        times.prepend(qdt);
                        rdates.pop_back();
                    }
                    if (times.count() > (useNewRRULE ? minPhaseCount : minRuleCount)) {
                        // There are enough dates to combine into an RRULE
                        icalrecurrencetype r;
                        icalrecurrencetype_clear(&r);
                        r.freq = ICAL_YEARLY_RECURRENCE;
                        r.count = (year >= 2030) ? 0 : times.count() - 1;
                        r.by_month[0] = month;
                        if (rule & DAY_OF_MONTH) {
                            r.by_month_day[0] = dayOfMonth;
                        } else if (rule & WEEKDAY_OF_MONTH) {
                            r.by_day[0] = (dayOfWeek % 7 + 1) + (nthFromStart * 8);       // Sunday = 1
                        } else if (rule & LAST_WEEKDAY_OF_MONTH) {
                            r.by_day[0] = -(dayOfWeek % 7 + 1) - (nthFromEnd * 8);       // Sunday = 1
                        }
                        icalproperty *prop = icalproperty_new_rrule(r);
                        if (useNewRRULE) {
                            // This RRULE doesn't start from the phase start date, so set it into
                            // a new STANDARD/DAYLIGHT component in the VTIMEZONE.
                            icalcomponent *c = icalcomponent_new_clone(phaseComp);
                            icalcomponent_add_property(
                                c, icalproperty_new_dtstart(writeLocalICalDateTime(times[0], preOffset)));
                            icalcomponent_add_property(c, prop);
                            icalcomponent_add_component(tzcomp, c);
                        } else {
                            icalcomponent_add_property(phaseComp1, prop);
                        }
                    } else {
                        // Save dates for writing as RDATEs
                        for (int t = 0, tend = times.count() - 1;  t < tend;  ++t) {
                            rdates += times[t];
                        }
                    }
                    useNewRRULE = true;
                    // All date/time values but the last have been added to the VTIMEZONE.
                    // Remove them from the list.
                    qdt = times.last();   // set 'qdt' for start of loop
                    times.clear();
                    times += qdt;
                }
                rule = newRule;
            } while (i < trcount);

            // Write remaining dates as RDATEs
            for (int rd = 0, rdend = rdates.count();  rd < rdend;  ++rd) {
                dtperiod.time = writeLocalICalDateTime(rdates[rd], preOffset);
                icalcomponent_add_property(phaseComp1, icalproperty_new_rdate(dtperiod));
            }
            icalcomponent_add_component(tzcomp, phaseComp1);
            icalcomponent_free(phaseComp);
        }

        d->setComponent(tzcomp);
    }
}

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

ICalTimeZoneData &ICalTimeZoneData::operator=(const ICalTimeZoneData &rhs)
{
    // check for self assignment
    if (&rhs == this) {
        return *this;
    }

    KTimeZoneData::operator=(rhs);
    d->location = rhs.d->location;
    d->url = rhs.d->url;
    d->lastModified = rhs.d->lastModified;
    d->setComponent(icalcomponent_new_clone(rhs.d->component()));
    return *this;
}

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

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

QByteArray ICalTimeZoneData::url() const
{
    return d->url;
}

QDateTime ICalTimeZoneData::lastModified() const
{
    return d->lastModified;
}

QByteArray ICalTimeZoneData::vtimezone() const
{
    const QByteArray result(icalcomponent_as_ical_string(d->component()));
    icalmemory_free_ring();
    return result;
}

icaltimezone *ICalTimeZoneData::icalTimezone() const
{
    icaltimezone *icaltz = icaltimezone_new();
    if (!icaltz) {
        return nullptr;
    }
    icalcomponent *c = icalcomponent_new_clone(d->component());
    if (!icaltimezone_set_component(icaltz, c)) {
        icalcomponent_free(c);
        icaltimezone_free(icaltz, 1);
        return nullptr;
    }
    return icaltz;
}

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

void ICalTimeZoneData::virtual_hook(int id, void *data)
{
    Q_UNUSED(id);
    Q_UNUSED(data);
}

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

//@cond PRIVATE
class ICalTimeZoneSourcePrivate
{
public:
    static QList<QDateTime> parsePhase(icalcomponent *, bool daylight,
                                       int &prevOffset, KTimeZone::Phase &);
    static QByteArray icalTzidPrefix;

#if defined(HAVE_UUID_UUID_H)
    static void parseTransitions(const MSSystemTime &date, const KTimeZone::Phase &phase,
                                 int prevOffset, QList<KTimeZone::Transition> &transitions);
#endif
};

QByteArray ICalTimeZoneSourcePrivate::icalTzidPrefix;
//@endcond

ICalTimeZoneSource::ICalTimeZoneSource()
    : KTimeZoneSource(false),
      d(nullptr)
{
    Q_UNUSED(d);
}

ICalTimeZoneSource::~ICalTimeZoneSource()
{
}

bool ICalTimeZoneSource::parse(const QString &fileName, ICalTimeZones &zones)
{
    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly)) {
        return false;
    }
    QTextStream ts(&file);
    ts.setCodec("ISO 8859-1");
    const QByteArray text = ts.readAll().trimmed().toLatin1();
    file.close();

    bool result = false;
    icalcomponent *calendar = icalcomponent_new_from_string(text.data());
    if (calendar) {
        if (icalcomponent_isa(calendar) == ICAL_VCALENDAR_COMPONENT) {
            result = parse(calendar, zones);
        }
        icalcomponent_free(calendar);
    }
    return result;
}

bool ICalTimeZoneSource::parse(icalcomponent *calendar, ICalTimeZones &zones)
{
    for (icalcomponent *c = icalcomponent_get_first_component(calendar, ICAL_VTIMEZONE_COMPONENT);
            c;  c = icalcomponent_get_next_component(calendar, ICAL_VTIMEZONE_COMPONENT)) {
        const ICalTimeZone zone = parse(c);
        if (!zone.isValid()) {
            return false;
        }
        ICalTimeZone oldzone = zones.zone(zone.name());
        if (oldzone.isValid()) {
            // The zone already exists in the collection, so update the definition
            // of the zone rather than using a newly created one.
            oldzone.update(zone);
        } else if (!zones.add(zone)) {
            return false;
        }
    }
    return true;
}

ICalTimeZone ICalTimeZoneSource::parse(icalcomponent *vtimezone)
{
    QString name;
    QString xlocation;
    ICalTimeZoneData *data = new ICalTimeZoneData();

    // Read the fixed properties which can only appear once in VTIMEZONE
    icalproperty *p = icalcomponent_get_first_property(vtimezone, ICAL_ANY_PROPERTY);
    while (p) {
        icalproperty_kind kind = icalproperty_isa(p);
        switch (kind) {

        case ICAL_TZID_PROPERTY:
            name = QString::fromUtf8(icalproperty_get_tzid(p));
            break;

        case ICAL_TZURL_PROPERTY:
            data->d->url = icalproperty_get_tzurl(p);
            break;

        case ICAL_LOCATION_PROPERTY:
            // This isn't mentioned in RFC2445, but libical reads it ...
            data->d->location = QString::fromUtf8(icalproperty_get_location(p));
            break;

        case ICAL_X_PROPERTY: {
            // use X-LIC-LOCATION if LOCATION is missing
            const char *xname = icalproperty_get_x_name(p);
            if (xname && !strcmp(xname, "X-LIC-LOCATION")) {
                xlocation = QString::fromUtf8(icalproperty_get_x(p));
            }
            break;
        }
        case ICAL_LASTMODIFIED_PROPERTY: {
            const icaltimetype t = icalproperty_get_lastmodified(p);
            if (t.is_utc) {
                data->d->lastModified = toQDateTime(t);
            } else {
                qCDebug(KCALCORE_LOG) << "LAST-MODIFIED not UTC";
            }
            break;
        }
        default:
            break;
        }
        p = icalcomponent_get_next_property(vtimezone, ICAL_ANY_PROPERTY);
    }

    if (name.isEmpty()) {
        qCDebug(KCALCORE_LOG) << "TZID missing";
        delete data;
        return ICalTimeZone();
    }
    if (data->d->location.isEmpty() && !xlocation.isEmpty()) {
        data->d->location = xlocation;
    }
    const QString prefix = QString::fromUtf8(icalTzidPrefix());
    if (name.startsWith(prefix)) {
        // Remove the prefix from libical built in time zone TZID
        const int i = name.indexOf(QLatin1Char('/'), prefix.length());
        if (i > 0) {
            name = name.mid(i + 1);
        }
    }
    //qCDebug(KCALCORE_LOG) << "---zoneId: \"" << name << '"';

    /*
     * Iterate through all time zone rules for this VTIMEZONE,
     * and create a Phase object containing details for each one.
     */
    int prevOffset = 0;
    QList<KTimeZone::Transition> transitions;
    QDateTime earliest;
    QList<KTimeZone::Phase> phases;
    for (icalcomponent *c = icalcomponent_get_first_component(vtimezone, ICAL_ANY_COMPONENT);
            c;  c = icalcomponent_get_next_component(vtimezone, ICAL_ANY_COMPONENT)) {
        int prevoff = 0;
        KTimeZone::Phase phase;
        QList<QDateTime> times;
        icalcomponent_kind kind = icalcomponent_isa(c);
        switch (kind) {

        case ICAL_XSTANDARD_COMPONENT:
            //qCDebug(KCALCORE_LOG) << "---standard phase: found";
            times = ICalTimeZoneSourcePrivate::parsePhase(c, false, prevoff, phase);
            break;

        case ICAL_XDAYLIGHT_COMPONENT:
            //qCDebug(KCALCORE_LOG) << "---daylight phase: found";
            times = ICalTimeZoneSourcePrivate::parsePhase(c, true, prevoff, phase);
            break;

        default:
            qCDebug(KCALCORE_LOG) << "Unknown component:" << int(kind);
            break;
        }
        const int tcount = times.count();
        if (tcount) {
            phases += phase;
            transitions.reserve(tcount);
            for (int t = 0;  t < tcount;  ++t) {
                transitions += KTimeZone::Transition(times[t], phase);
            }
            if (!earliest.isValid() || times[0] < earliest) {
                prevOffset = prevoff;
                earliest = times[0];
            }
        }
    }
    // Set phases used by the time zone, but note that VTIMEZONE doesn't contain
    // time zone abbreviation before first transition.
    data->setPhases(phases, prevOffset);
    // Remove any "duplicate" transitions, i.e. those where two consecutive
    // transitions have the same phase.
    qSort(transitions);
    for (int t = 1, tend = transitions.count();  t < tend;) {
        if (transitions[t].phase() == transitions[t - 1].phase()) {
            transitions.removeAt(t);
            --tend;
        } else {
            ++t;
        }
    }
    data->setTransitions(transitions);

    data->d->setComponent(icalcomponent_new_clone(vtimezone));
    //qCDebug(KCALCORE_LOG) << "VTIMEZONE" << name;
    return ICalTimeZone(this, name, data);
}

#if defined(HAVE_UUID_UUID_H)
ICalTimeZone ICalTimeZoneSource::parse(MSTimeZone *tz, ICalTimeZones &zones)
{
    const ICalTimeZone zone = parse(tz);
    if (!zone.isValid()) {
        return ICalTimeZone(); // error
    }
    const ICalTimeZone oldzone = zones.zone(zone);
    if (oldzone.isValid()) {
        // A similar zone already exists in the collection, so don't add this
        // new zone, return old zone instead.
        return oldzone;
    } else if (zones.add(zone)) {
        // No similar zone, add and return new one.
        return zone;
    }
    return ICalTimeZone(); // error
}

ICalTimeZone ICalTimeZoneSource::parse(MSTimeZone *tz)
{
    ICalTimeZoneData kdata;

    // General properties.
    uuid_t uuid;
    char suuid[64];
    uuid_generate_random(uuid);
    uuid_unparse(uuid, suuid);
    QString name = QString::fromLatin1(suuid);

    // Create phases.
    QList<KTimeZone::Phase> phases;

    QList<QByteArray> standardAbbrevs;
    standardAbbrevs += tz->StandardName.toLatin1();
    const KTimeZone::Phase standardPhase(
        (tz->Bias + tz->StandardBias) * -60,
        standardAbbrevs, false,
        QStringLiteral("Microsoft TIME_ZONE_INFORMATION"));
    phases += standardPhase;

    QList<QByteArray> daylightAbbrevs;
    daylightAbbrevs += tz->DaylightName.toLatin1();
    const KTimeZone::Phase daylightPhase(
        (tz->Bias + tz->DaylightBias) * -60,
        daylightAbbrevs, true,
        QStringLiteral("Microsoft TIME_ZONE_INFORMATION"));
    phases += daylightPhase;

    // Set phases used by the time zone, but note that previous time zone
    // abbreviation is not known.
    const int prevOffset = tz->Bias * -60;
    kdata.setPhases(phases, prevOffset);

    // Create transitions
    QList<KTimeZone::Transition> transitions;
    ICalTimeZoneSourcePrivate::parseTransitions(
        tz->StandardDate, standardPhase, prevOffset, transitions);
    ICalTimeZoneSourcePrivate::parseTransitions(
        tz->DaylightDate, daylightPhase, prevOffset, transitions);

    qSort(transitions);
    kdata.setTransitions(transitions);

    ICalTimeZoneData *idata = new ICalTimeZoneData(kdata, KTimeZone(name), QDate());

    return ICalTimeZone(this, name, idata);
}
#endif // HAVE_UUID_UUID_H

ICalTimeZone ICalTimeZoneSource::parse(const QString &name, const QStringList &tzList,
                                       ICalTimeZones &zones)
{
    const ICalTimeZone zone = parse(name, tzList);
    if (!zone.isValid()) {
        return ICalTimeZone(); // error
    }

    ICalTimeZone oldzone = zones.zone(zone);
    // First off see if the zone is same as oldzone - _exactly_ same
    if (oldzone.isValid()) {
        return oldzone;
    }

    oldzone = zones.zone(name);
    if (oldzone.isValid()) {
        // The zone already exists, so update
        oldzone.update(zone);
        return zone;
    } else if (zones.add(zone)) {
        // No similar zone, add and return new one.
        return zone;
    }
    return ICalTimeZone(); // error
}

ICalTimeZone ICalTimeZoneSource::parse(const QString &name, const QStringList &tzList)
{
    ICalTimeZoneData kdata;
    QList<KTimeZone::Phase> phases;
    QList<KTimeZone::Transition> transitions;
    bool daylight;

    for (QStringList::ConstIterator it = tzList.begin(); it != tzList.end(); ++it) {
        QString value = *it;
        daylight = false;
        const QString tzName = value.mid(0, value.indexOf(QLatin1Char(';')));
        value = value.mid((value.indexOf(QLatin1Char(';')) + 1));
        const QString tzOffset = value.mid(0, value.indexOf(QLatin1Char(';')));
        value = value.mid((value.indexOf(QLatin1Char(';')) + 1));
        const QString tzDaylight = value.mid(0, value.indexOf(QLatin1Char(';')));
        const KDateTime tzDate = KDateTime::fromString(value.mid((value.lastIndexOf(QLatin1Char(';')) + 1)));
        if (tzDaylight == QLatin1String("true")) {
            daylight = true;
        }

        const KTimeZone::Phase tzPhase(
            tzOffset.toInt(),
            QByteArray(tzName.toLatin1()), daylight, QStringLiteral("VCAL_TZ_INFORMATION"));
        phases += tzPhase;
        transitions += KTimeZone::Transition(tzDate.dateTime(), tzPhase);
    }

    kdata.setPhases(phases, 0);
    qSort(transitions);
    kdata.setTransitions(transitions);

    ICalTimeZoneData *idata = new ICalTimeZoneData(kdata, KTimeZone(name), QDate());
    return ICalTimeZone(this, name, idata);
}

#if defined(HAVE_UUID_UUID_H)
//@cond PRIVATE
void ICalTimeZoneSourcePrivate::parseTransitions(const MSSystemTime &date,
        const KTimeZone::Phase &phase, int prevOffset,
        QList<KTimeZone::Transition> &transitions)
{
    // NOTE that we need to set start and end times and they cannot be
    // to far in either direction to avoid bloating the transitions list
    const KDateTime klocalStart(QDateTime(QDate(2000, 1, 1), QTime(0, 0, 0)),
                                KDateTime::Spec::ClockTime());
    const KDateTime maxTime(MAX_DATE(), KDateTime::Spec::ClockTime());

    if (date.wYear) {
        // Absolute change time.
        if (date.wYear >= 1601 && date.wYear <= 30827 &&
                date.wMonth >= 1 && date.wMonth <= 12 &&
                date.wDay >= 1 && date.wDay <= 31) {
            const QDate dt(date.wYear, date.wMonth, date.wDay);
            const QTime tm(date.wHour, date.wMinute, date.wSecond, date.wMilliseconds);
            const QDateTime datetime(dt, tm);
            if (datetime.isValid()) {
                transitions += KTimeZone::Transition(datetime, phase);
            }
        }
    } else {
        // The normal way, for example: 'First Sunday in April at 02:00'.
        if (date.wDayOfWeek >= 0 && date.wDayOfWeek <= 6 &&
                date.wMonth >= 1 && date.wMonth <= 12 &&
                date.wDay >= 1 && date.wDay <= 5) {
            RecurrenceRule r;
            r.setRecurrenceType(RecurrenceRule::rYearly);
            r.setDuration(-1);
            r.setFrequency(1);
            QList<int> lst;
            lst.append(date.wMonth);
            r.setByMonths(lst);
            QList<RecurrenceRule::WDayPos> wdlst;
            RecurrenceRule::WDayPos pos;
            pos.setDay(date.wDayOfWeek ? date.wDayOfWeek : 7);
            pos.setPos(date.wDay < 5 ? date.wDay : -1);
            wdlst.append(pos);
            r.setByDays(wdlst);
            r.setStartDt(klocalStart);
            r.setWeekStart(1);
            const DateTimeList dtl = r.timesInInterval(klocalStart, maxTime);
            for (int i = 0, end = dtl.count();  i < end;  ++i) {
                QDateTime utc = dtl[i].dateTime();
                utc.setTimeSpec(Qt::UTC);
                transitions += KTimeZone::Transition(utc.addSecs(-prevOffset), phase);
            }
        }
    }
}
//@endcond
#endif // HAVE_UUID_UUID_H

ICalTimeZone ICalTimeZoneSource::parse(icaltimezone *tz)
{
    /* Parse the VTIMEZONE component stored in the icaltimezone structure.
     * This is both easier and provides more complete information than
     * extracting already parsed data from icaltimezone.
     */
    return tz ? parse(icaltimezone_get_component(tz)) : ICalTimeZone();
}

//@cond PRIVATE
QList<QDateTime> ICalTimeZoneSourcePrivate::parsePhase(icalcomponent *c,
        bool daylight,
        int &prevOffset,
        KTimeZone::Phase &phase)
{
    QList<QDateTime> transitions;

    // Read the observance data for this standard/daylight savings phase
    QList<QByteArray> abbrevs;
    QString comment;
    prevOffset = 0;
    int utcOffset = 0;
    bool recurs = false;
    bool found_dtstart = false;
    bool found_tzoffsetfrom = false;
    bool found_tzoffsetto = false;
    icaltimetype dtstart = icaltime_null_time();

    // Now do the ical reading.
    icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
    while (p) {
        icalproperty_kind kind = icalproperty_isa(p);
        switch (kind) {

        case ICAL_TZNAME_PROPERTY: {   // abbreviated name for this time offset
            // TZNAME can appear multiple times in order to provide language
            // translations of the time zone offset name.

            // TODO: Does this cope with multiple language specifications?
            QByteArray name = icalproperty_get_tzname(p);
            // Outlook (2000) places "Standard Time" and "Daylight Time" in the TZNAME
            // strings, which is totally useless. So ignore those.
            if ((!daylight && name == "Standard Time") ||
                    (daylight && name == "Daylight Time")) {
                break;
            }
            if (!abbrevs.contains(name)) {
                abbrevs += name;
            }
            break;
        }
        case ICAL_DTSTART_PROPERTY:      // local time at which phase starts
            dtstart = icalproperty_get_dtstart(p);
            found_dtstart = true;
            break;

        case ICAL_TZOFFSETFROM_PROPERTY:    // UTC offset immediately before start of phase
            prevOffset = icalproperty_get_tzoffsetfrom(p);
            found_tzoffsetfrom = true;
            break;

        case ICAL_TZOFFSETTO_PROPERTY:
            utcOffset = icalproperty_get_tzoffsetto(p);
            found_tzoffsetto = true;
            break;

        case ICAL_COMMENT_PROPERTY:
            comment = QString::fromUtf8(icalproperty_get_comment(p));
            break;

        case ICAL_RDATE_PROPERTY:
        case ICAL_RRULE_PROPERTY:
            recurs = true;
            break;

        default:
            qCDebug(KCALCORE_LOG) << "Unknown property:" << int(kind);
            break;
        }
        p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
    }

    // Validate the phase data
    if (!found_dtstart || !found_tzoffsetfrom || !found_tzoffsetto) {
        qCDebug(KCALCORE_LOG) << "DTSTART/TZOFFSETFROM/TZOFFSETTO missing";
        return transitions;
    }

    // Convert DTSTART to QDateTime, and from local time to UTC
    const QDateTime localStart = toQDateTime(dtstart);     // local time
    dtstart.second -= prevOffset;
    dtstart.is_utc = 1;
    const QDateTime utcStart = toQDateTime(icaltime_normalize(dtstart));       // UTC

    transitions += utcStart;
    if (recurs) {
        /* RDATE or RRULE is specified. There should only be one or the other, but
         * it doesn't really matter - the code can cope with both.
         * Note that we had to get DTSTART, TZOFFSETFROM, TZOFFSETTO before reading
         * recurrences.
         */
        const KDateTime klocalStart(localStart, KDateTime::Spec::ClockTime());
        const KDateTime maxTime(MAX_DATE(), KDateTime::Spec::ClockTime());
        Recurrence recur;
        icalproperty *p = icalcomponent_get_first_property(c, ICAL_ANY_PROPERTY);
        while (p) {
            icalproperty_kind kind = icalproperty_isa(p);
            switch (kind) {

            case ICAL_RDATE_PROPERTY: {
                icaltimetype t = icalproperty_get_rdate(p).time;
                if (icaltime_is_date(t)) {
                    // RDATE with a DATE value inherits the (local) time from DTSTART
                    t.hour = dtstart.hour;
                    t.minute = dtstart.minute;
                    t.second = dtstart.second;
                    t.is_date = 0;
                    t.is_utc = 0;    // dtstart is in local time
                }
                // RFC2445 states that RDATE must be in local time,
                // but we support UTC as well to be safe.
                if (!t.is_utc) {
                    t.second -= prevOffset;    // convert to UTC
                    t.is_utc = 1;
                    t = icaltime_normalize(t);
                }
                transitions += toQDateTime(t);
                break;
            }
            case ICAL_RRULE_PROPERTY: {
                RecurrenceRule r;
                ICalFormat icf;
                ICalFormatImpl impl(&icf);
                impl.readRecurrence(icalproperty_get_rrule(p), &r);
                r.setStartDt(klocalStart);
                // The end date time specified in an RRULE should be in UTC.
                // Convert to local time to avoid timesInInterval() getting things wrong.
                if (r.duration() == 0) {
                    KDateTime end(r.endDt());
                    if (end.timeSpec() == KDateTime::Spec::UTC()) {
                        end.setTimeSpec(KDateTime::Spec::ClockTime());
                        r.setEndDt(end.addSecs(prevOffset));
                    }
                }
                const DateTimeList dts = r.timesInInterval(klocalStart, maxTime);
                for (int i = 0, end = dts.count();  i < end;  ++i) {
                    QDateTime utc = dts[i].dateTime();
                    utc.setTimeSpec(Qt::UTC);
                    transitions += utc.addSecs(-prevOffset);
                }
                break;
            }
            default:
                break;
            }
            p = icalcomponent_get_next_property(c, ICAL_ANY_PROPERTY);
        }
        qSortUnique(transitions);
    }

    phase = KTimeZone::Phase(utcOffset, abbrevs, daylight, comment);
    return transitions;
}
//@endcond

ICalTimeZone ICalTimeZoneSource::standardZone(const QString &zone, bool icalBuiltIn)
{
    if (!icalBuiltIn) {
        // Try to fetch a system time zone in preference, on the grounds
        // that system time zones are more likely to be up to date than
        // built-in libical ones.
        QString tzid = zone;
        const QString prefix = QString::fromUtf8(icalTzidPrefix());
        if (zone.startsWith(prefix)) {
            const int i = zone.indexOf(QLatin1Char('/'), prefix.length());
            if (i > 0) {
                tzid = zone.mid(i + 1);     // strip off the libical prefix
            }
        }
        const KTimeZone ktz = KSystemTimeZones::readZone(tzid);
        if (ktz.isValid()) {
            if (ktz.data(true)) {
                const ICalTimeZone icaltz(ktz);
                //qCDebug(KCALCORE_LOG) << zone << " read from system database";
                return icaltz;
            }
        }
    }
    // Try to fetch a built-in libical time zone.
    // First try to look it up as a geographical location (e.g. Europe/London)
    const QByteArray zoneName = zone.toUtf8();
    icaltimezone *icaltz = icaltimezone_get_builtin_timezone(zoneName);
    if (!icaltz) {
        // This will find it if it includes the libical prefix
        icaltz = icaltimezone_get_builtin_timezone_from_tzid(zoneName);
        if (!icaltz) {
            return ICalTimeZone();
        }
    }
    return parse(icaltz);
}

QByteArray ICalTimeZoneSource::icalTzidPrefix()
{
    if (ICalTimeZoneSourcePrivate::icalTzidPrefix.isEmpty()) {
        icaltimezone *icaltz = icaltimezone_get_builtin_timezone("Europe/London");
        const QByteArray tzid = icaltimezone_get_tzid(icaltz);
        if (tzid.right(13) == "Europe/London") {
            int i = tzid.indexOf('/', 1);
            if (i > 0) {
                ICalTimeZoneSourcePrivate::icalTzidPrefix = tzid.left(i + 1);
                return ICalTimeZoneSourcePrivate::icalTzidPrefix;
            }
        }
        qCritical() << "failed to get libical TZID prefix";
    }
    return ICalTimeZoneSourcePrivate::icalTzidPrefix;
}

void ICalTimeZoneSource::virtual_hook(int id, void *data)
{
    Q_UNUSED(id);
    Q_UNUSED(data);
    Q_ASSERT(false);
}

}  // namespace KCalCore
