/* -*- mode: c++; indent-tabs-mode: nil -*- */
/*
    QC_TimeZone.qpp

    Qore Programming Language

    Copyright (C) 2003 - 2023 Qore Technologies, s.r.o.

    Permission is hereby granted, free of charge, to any person obtaining a
    copy of this software and associated documentation files (the "Software"),
    to deal in the Software without restriction, including without limitation
    the rights to use, copy, modify, merge, publish, distribute, sublicense,
    and/or sell copies of the Software, and to permit persons to whom the
    Software is furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
    DEALINGS IN THE SOFTWARE.

    Note that the Qore library is released under a choice of three open-source
    licenses: MIT (as above), LGPL 2+, or GPL 2+; see README-LICENSE for more
    information.
*/

#include <qore/Qore.h>
#include "qore/intern/QC_TimeZone.h"
#include "qore/intern/QC_Serializable.h"

static void create_timezone_from_region(const QoreString& region, QoreObject& self, ExceptionSink* xsink) {
    bool is_path = (!region.empty() && region.c_str()[0] == '.') || q_absolute_path(region.c_str());
    if (is_path && runtime_check_parse_option(PO_NO_FILESYSTEM)) {
        xsink->raiseException("ILLEGAL-FILESYSTEM-ACCESS", "cannot create a TimeZone object from absolute path '%s' "
            "when sandboxing restriction PO_NO_FILESYSTEM is set", region.c_str());
    }
    const AbstractQoreZoneInfo* zone = is_path
        ? QTZM.findLoadRegionFromPath(region.c_str(), xsink)
        : QTZM.findLoadRegion(region.c_str(), xsink);
    if (*xsink) {
        return;
    }

    self.setPrivate(CID_TIMEZONE, new TimeZoneData(zone));
}

static QoreHashNode* TimeZone_serializer(const QoreObject& self, const TimeZoneData& z,
        QoreSerializationContext& context, ExceptionSink* xsink) {
    ReferenceHolder<QoreHashNode> rv(new QoreHashNode(autoTypeInfo), xsink);

    if (dynamic_cast<const QoreOffsetZoneInfo*>(*z)) {
        int utc_offset = AbstractQoreZoneInfo::getUTCOffset(*z);
        rv->setKeyValue("offset", utc_offset, xsink);
    } else {
        rv->setKeyValue("region", new QoreStringNode(AbstractQoreZoneInfo::getRegionName(*z)), xsink);
    }

    return rv.release();
}

static void TimeZone_deserializer(QoreObject& self, const QoreHashNode* sdata, QoreDeserializationContext& context,
        ExceptionSink* xsink) {
    if (sdata) {
        QoreValue val = sdata->getKeyValue("region");
        if (val) {
            if (val.getType() != NT_STRING) {
                xsink->raiseException("DESERIALIZATION-ERROR", "TimeZone serialization key 'region' has type '%s'; "
                    "expecting 'string'", val.getTypeName());
                return;
            }
            create_timezone_from_region(*val.get<QoreStringNode>(), self, xsink);
            return;
        }

        val = sdata->getKeyValue("offset");
        if (val) {
            const AbstractQoreZoneInfo* zone = QTZM.findCreateOffsetZone(val.getAsBigInt());
            self.setPrivate(CID_TIMEZONE, new TimeZoneData(zone));
            return;
        }
    }

    xsink->raiseException("DESERIALIZATION-ERROR", "missing serialization data for the TimeZone class");
}

//! The TimeZone class provides access to time zone functionality
/** TimeZone objects based on zoneinfo region files (on UNIX) or registry information (on Windows) can
    have daylight savings time information; those based on UTC offsets have none.

    The TimeZone class supports @ref Serializable "data serialization"; deserialization can fail if the object is based on
    a region that is not present on the target machine.
*/
qclass TimeZone [arg=TimeZoneData* z; vparent=Serializable; serializer=reinterpret_cast<q_serializer_t>(TimeZone_serializer); deserializer=TimeZone_deserializer];

//! Creates the TimeZone object based on the region name (ex: \c "America/Chicago")
/** @param region The region name for the time zone (ex: \c "America/Chicago"); if the zoneinfo file for the region cannot be found or parsed (on UNIX) or if the registry entry cannot be found (on Windows), then an exception is thrown

    @note On Windows zoneinfo region names (ex: \c "Europe/Prague") are converted to registry entries under <tt>HKEY_LOCAL_MACHINE SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones</tt> (ex: \c "Central Europe Standard Time"); on Windows you can also pass a registry key directly (ex: \c "Central Europe Standard Time") as the region, in this case it will be used to find the timezone info in registry, however the region name will then be converted to the first corresponding zoneinfo region according to alphabet (ex: \c "Central Europe Standard Time" -> \c "Europe/Belgrade"); if an absolute path to a file is given as the argument, then a zoneinfo file is read and parsed from the given location

    @par Example:
    @code{.py}
TimeZone tz("Europe/Prague");
    @endcode

    @throw TZINFO-ERROR Unable to read zoneinfo file; invalid file magic; error parsing zoneinfo file, etc
    @throw ILLEGAL-FILESYSTEM-ACCESS thrown if a path was passed for \a region and the @ref Qore::PO_NO_FILESYSTEM sandboxing restriction is set

    @since %Qore 0.8.12 an absolute path to a zoneinfo file can be provided if the @ref Qore::PO_NO_FILESYSTEM sandboxing restriction is not in place (checked at runtime if an absolute path is passed)
 */
TimeZone::constructor(string region) {
    create_timezone_from_region(region, *self, xsink);
}

//! Creates the TimeZone object based on the number of seconds east of UTC (3600 = UTC +01)
/** @param seconds_east The number of seconds east of UTC for the time zone; for zones west of UTC, use negative numbers

    @par Example:
    @code{.py}
TimeZone tz(3600);
    @endcode
 */
TimeZone::constructor(softint seconds_east) {
    const AbstractQoreZoneInfo* zone = QTZM.findCreateOffsetZone(seconds_east);
    self->setPrivate(CID_TIMEZONE, new TimeZoneData(zone));
}

//! Creates a copy of the TimeZone object
/**
    @par Example:
    @code{.py}
TimeZone newzone = tz.copy();
    @endcode
 */
TimeZone::copy() {
    self->setPrivate(CID_TIMEZONE, new TimeZoneData(*z));
}

//! Returns the number of seconds east of UTC for the zone; negative numbers indicate a zone west of UTC
/** @return the number of seconds east of UTC for the zone; negative numbers indicate a zone west of UTC

    @par Example:
    @code{.py}
int offset = tz.UTCOffset();
    @endcode
 */
int TimeZone::UTCOffset() [flags=CONSTANT] {
    return AbstractQoreZoneInfo::getUTCOffset(z->get());
}

//! Returns @ref True if the zone has daylight saving time rules, @ref False if not
/** TimeZone objects based on zoneinfo region files (on UNIX) or registry information (on Windows) can have (but do not necessarily have) daylight savings time information; those based on UTC offsets have none

    @return @ref True if the current zone has daylight saving time rules, @ref False if not

    @par Example:
    @code{.py}
bool hasdst = tz.hasDST();
    @endcode
 */
bool TimeZone::hasDST() [flags=CONSTANT] {
    return AbstractQoreZoneInfo::hasDST(z->get());
}

//! Returns the region name as a string; if the current zone is based on a UTC offset, then the UTC offset is returned as a string like \c "+01:00"
/** @return the region name as a string; if the current zone is based on a UTC offset, then the UTC offset is returned as a string like \c "+01:00"

    @par Example:
    @code{.py}
string region = tz.region();
    @endcode
 */
string TimeZone::region() [flags=CONSTANT] {
    return new QoreStringNode(AbstractQoreZoneInfo::getRegionName(z->get()));
}

//! Returns the equivalent date in the time zone of the current object
/** @param secs offset are in seconds from 1970-01-01Z
    @param us offset are in microseconds from \c 1970-01-01Z

    @par Example:
    @code{.py}
date dt = tz.date(secs);
    @endcode
 */
date TimeZone::date(softint secs, softint us = 0) [flags=CONSTANT] {
    return DateTimeNode::makeAbsolute(z->get(), secs, (int)us);
}

//! Returns the equivalent date in the time zone of the current object
/** @param d A date that will be used to create the date in the time zone of the objects; the same point in time will be returned but in the time zone of the object

    @par Example:
    @code{.py}
date dt = tz.date(2012-01-01T13:56:23+01:00);
    @endcode
 */
date TimeZone::date(date d) [flags=CONSTANT] {
    return DateTimeNode::makeAbsolute(z->get(), d->getEpochSecondsUTC(), d->getMicrosecond());
}

//! Returns the equivalent date in the time zone of the current object
/** @param dtstr The string to be used to return a date in the object's time zone

    @par Example:
    @code{.py}
date dt = tz.date("2012-01-01T13:56:23");
    @endcode

    @since %Qore 0.8.4
 */
date TimeZone::date(string dtstr) [flags=CONSTANT] {
    return new DateTimeNode(z->get(), dtstr->getBuffer());
}

//! Returns a @ref date "date/time" value in the current TimeZone corresponding to parsing a string argument according to a @ref date_mask "format mask"
/**
    @param dtstr a string giving a date
    @param mask the mask for the date value; see @ref date_mask for information on the format of the
    @ref date_mask "format mask"

    @return the @ref date "date/time" value in the current TimeZone corresponding to parsing the \a dtstr string
    argument according to \a mask serving as a @ref date_mask "format mask"

    @par Example:
    @code{.py}
TimeZone tz("Europe/London");
printf("%y\n", tz.date("05/02/2012", "DD/MM/YYYY")); # outputs: 2012-02-05 00:00:00 Sun Z (GMT)
    @endcode

    @throw DATE-CONVERT-ERROR invalid mask specification

    @see similar to but more useful than @ref Qore::date(string, string)

    @since %Qore 0.8.6
*/
date TimeZone::date(string dtstr, string mask) [flags=RET_VALUE_ONLY] {
    return make_date_with_mask(z->get(), *dtstr, *mask, xsink);
}

//! Returns a date in the object's zone based on an offsets in milliseconds from \c 1970-01-01Z
/** @param ms an offsets in milliseconds from \c 1970-01-01Z

    @return a date in the object's zone based on the given offsets in milliseconds from \c 1970-01-01Z

    @par Example:
    @code{.py}
date dt = tz.dateMs(offset_ms);
    @endcode
 */
date TimeZone::dateMs(softint ms) [flags=CONSTANT] {
    return DateTimeNode::makeAbsolute(z->get(), ms / 1000, (int)((ms % 1000) * 1000));
}

//! Returns a date in the object's zone based on an offsets in microseconds from \c 1970-01-01Z
/** @param us an offsets in microseconds from \c 1970-01-01Z

    @return a date in the object's zone based on the given offsets in microseconds from \c 1970-01-01Z

    @par Example:
    @code{.py}
date dt = tz.dateUs(offset_uss);
    @endcode
 */
date TimeZone::dateUs(softint us) [flags=CONSTANT] {
    return DateTimeNode::makeAbsolute(z->get(), us / 1000000, (int)(us % 1000000));
}

//! Returns a TimeZone object for the current time zone
/**
    @return a TimeZone object for the current time zone

    @par Example:
    @code{.py}
TimeZone tz = TimeZone::get();
    @endcode
 */
static TimeZone TimeZone::get() [flags=CONSTANT] {
    return new QoreObject(QC_TIMEZONE, 0, new TimeZoneData(currentTZ()));
}

//! Sets the time zone for the current Program object from a TimeZone object
/**
    @param zone the time zone to set

    @par Example:
    @code{.py}
TimeZone tz("Europe/Prague");
TimeZone::set(tz);
    @endcode
 */
static nothing TimeZone::set(TimeZone[TimeZoneData] zone) [dom=LOCALE_CONTROL] {
    ReferenceHolder<TimeZoneData> holder(zone, xsink);
    getProgram()->setTZ(zone->get());
}

//! Sets the time zone for the current Program object from an integer offset in seconds east of UTC
/**
    @param seconds_east the number of seconds east of UTC for the new time zone (negative numbers give seconds west
    of UTC)

    @par Example:
    @code{.py}
TimeZone::setUTCOffset(3600);
    @endcode
 */
static nothing TimeZone::setUTCOffset(softint seconds_east) [dom=LOCALE_CONTROL] {
    const AbstractQoreZoneInfo *zone = QTZM.findCreateOffsetZone(seconds_east);
    getProgram()->setTZ(zone);
}

//! Sets the time zone for the current Program object from a time zone region name
/**
    @param region the region name to set

    @note On Windows you can use both zoneinfo region names (ex: \c "Europe/Prague") as well as the Windows registry
    keys under <tt>HKEY_LOCAL_MACHINE SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones</tt> (ex:
    \c "Central Europe Standard Time"); note that in this case the Windows-style names will only be used to load the
    timezone info from registry and the name will then be converted to the first corresponding zoneinfo region
    according to alphabet (ex: \c "Central Europe Standard Time" -> \c "Europe/Belgrade").

    @par Example:
    @code{.py}
TimeZone::setRegion("Europe/Prague");
    @endcode
 */
static nothing TimeZone::setRegion(string region) [dom=LOCALE_CONTROL] {
    const AbstractQoreZoneInfo *zone = QTZM.findLoadRegion(region->getBuffer(), xsink);
    if (!*xsink) {
        getProgram()->setTZ(zone);
    }
}
