/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* gweather-timezone.c - Timezone handling
 *
 * Copyright 2008, Red Hat, Inc.
 *
 * This library 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 2.1 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, see
 * <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <string.h>

#include "gweather-timezone.h"
#include "gweather-parser.h"
#include "gweather-private.h"

/**
 * SECTION:gweathertimezone
 * @Title: GWeatherTimezone
 *
 * A timezone.
 *
 * Timezones are global to the GWeather world (as obtained by
 * gweather_location_get_world()); they can be gotten by passing
 * gweather_timezone_get_by_tzid() with a tzid like "America/New_York"
 * or "Europe/London".
 */

struct _GWeatherTimezone {
    char *id, *name;
    int offset, dst_offset;
    gboolean has_dst;

    int ref_count;
};

#define TZ_MAGIC "TZif"
#define TZ_HEADER_SIZE 44
#define TZ_TIMECNT_OFFSET 32
#define TZ_TRANSITIONS_OFFSET 44

#define TZ_TTINFO_SIZE 6
#define TZ_TTINFO_GMTOFF_OFFSET 0
#define TZ_TTINFO_ISDST_OFFSET 4

static gboolean
parse_tzdata (const char *tz_name, time_t start, time_t end,
	      int *offset, gboolean *has_dst, int *dst_offset)
{
    char *filename, *contents;
    gsize length;
    int timecnt, transitions_size, ttinfo_map_size;
    int initial_transition = -1, second_transition = -1;
    gint32 *transitions;
    char *ttinfo_map, *ttinfos;
    gint32 initial_offset, second_offset;
    char initial_isdst, second_isdst;
    int i;

    filename = g_build_filename (ZONEINFO_DIR, tz_name, NULL);
    if (!g_file_get_contents (filename, &contents, &length, NULL)) {
	g_free (filename);
	return FALSE;
    }
    g_free (filename);

    if (length < TZ_HEADER_SIZE ||
	strncmp (contents, TZ_MAGIC, strlen (TZ_MAGIC)) != 0) {
	g_free (contents);
	return FALSE;
    }

    timecnt = GUINT32_FROM_BE (*(guint32 *)(void *)(contents + TZ_TIMECNT_OFFSET));
    transitions = (void *)(contents + TZ_TRANSITIONS_OFFSET);
    transitions_size = timecnt * sizeof (*transitions);
    ttinfo_map = (void *)(contents + TZ_TRANSITIONS_OFFSET + transitions_size);
    ttinfo_map_size = timecnt;
    ttinfos = (void *)(ttinfo_map + ttinfo_map_size);

    /* @transitions is an array of @timecnt time_t values. We need to
     * find the transition into the current offset, which is the last
     * transition before @start. If the following transition is before
     * @end, then note that one too, since it presumably means we're
     * doing DST.
     */
    for (i = 1; i < timecnt && initial_transition == -1; i++) {
	if (GINT32_FROM_BE (transitions[i]) > start) {
	    initial_transition = ttinfo_map[i - 1];
	    if (GINT32_FROM_BE (transitions[i]) < end)
		second_transition = ttinfo_map[i];
	}
    }
    if (initial_transition == -1) {
	if (timecnt)
	    initial_transition = ttinfo_map[timecnt - 1];
	else
	    initial_transition = 0;
    }

    /* Copy the data out of the corresponding ttinfo structs */
    initial_offset = *(gint32 *)(void *)(ttinfos +
				 initial_transition * TZ_TTINFO_SIZE +
				 TZ_TTINFO_GMTOFF_OFFSET);
    initial_offset = GINT32_FROM_BE (initial_offset);
    initial_isdst = *(ttinfos +
		      initial_transition * TZ_TTINFO_SIZE +
		      TZ_TTINFO_ISDST_OFFSET);

    if (second_transition != -1) {
	second_offset = *(gint32 *)(void *)(ttinfos +
				    second_transition * TZ_TTINFO_SIZE +
				    TZ_TTINFO_GMTOFF_OFFSET);
	second_offset = GINT32_FROM_BE (second_offset);
	second_isdst = *(ttinfos +
			 second_transition * TZ_TTINFO_SIZE +
			 TZ_TTINFO_ISDST_OFFSET);

	*has_dst = (initial_isdst != second_isdst);
    } else
	*has_dst = FALSE;

    if (!*has_dst)
	*offset = initial_offset / 60;
    else {
	if (initial_isdst) {
	    *offset = second_offset / 60;
	    *dst_offset = initial_offset / 60;
	} else {
	    *offset = initial_offset / 60;
	    *dst_offset = second_offset / 60;
	}
    }

    g_free (contents);
    return TRUE;
}

/**
 * gweather_timezone_get_by_tzid:
 * @tzid: A timezone identifier, like "America/New_York" or "Europe/London"
 *
 * Get the #GWeatherTimezone for @tzid.
 *
 * Returns: (transfer none): A #GWeatherTimezone. This object
 * belongs to GWeather, do not unref it.
 *
 * Since: 3.12
 */
GWeatherTimezone *
gweather_timezone_get_by_tzid (const char *tzid)
{
    GWeatherLocation *world;

    world = gweather_location_get_world ();

    return g_hash_table_lookup (world->timezone_cache, tzid);
}

static GWeatherTimezone *
parse_timezone (GWeatherParser *parser)
{
    GWeatherTimezone *zone = NULL;
    char *id = NULL;
    char *name = NULL;
    int offset = 0, dst_offset = 0;
    gboolean has_dst = FALSE;

    id = (char *) xmlTextReaderGetAttribute (parser->xml, (xmlChar *) "id");
    if (!id) {
	xmlTextReaderNext (parser->xml);
	return NULL;
    }

    if (!xmlTextReaderIsEmptyElement (parser->xml)) {
	if (xmlTextReaderRead (parser->xml) != 1) {
	    xmlFree (id);
	    return NULL;
	}

	while (xmlTextReaderNodeType (parser->xml) != XML_READER_TYPE_END_ELEMENT) {
	    if (xmlTextReaderNodeType (parser->xml) != XML_READER_TYPE_ELEMENT) {
		if (xmlTextReaderRead (parser->xml) != 1)
		    break;
		continue;
	    }

	    if (!strcmp ((const char *) xmlTextReaderConstName (parser->xml), "name"))
		name = _gweather_parser_get_localized_value (parser);
	    else {
		if (xmlTextReaderNext (parser->xml) != 1)
		    break;
	    }
	}
    }

    if (parse_tzdata (id, parser->year_start, parser->year_end,
		      &offset, &has_dst, &dst_offset)) {
	zone = g_slice_new0 (GWeatherTimezone);
	zone->ref_count = 1;
	zone->id = g_strdup (id);
	zone->name = name;
	zone->offset = offset;
	zone->has_dst = has_dst;
	zone->dst_offset = dst_offset;

	g_hash_table_insert (parser->timezone_cache, zone->id, zone);

	name = NULL;
    }

    g_free (name);
    xmlFree (id);

    return zone;
}

GWeatherTimezone **
_gweather_timezones_parse_xml (GWeatherParser *parser)
{
    GPtrArray *zones;
    GWeatherTimezone *zone;
    const char *tagname;
    int tagtype;
    unsigned int i;

    zones = g_ptr_array_new ();

    if (xmlTextReaderRead (parser->xml) != 1)
	goto error_out;
    while ((tagtype = xmlTextReaderNodeType (parser->xml)) !=
	   XML_READER_TYPE_END_ELEMENT) {
	if (tagtype != XML_READER_TYPE_ELEMENT) {
	    if (xmlTextReaderRead (parser->xml) != 1)
		goto error_out;
	    continue;
	}

	tagname = (const char *) xmlTextReaderConstName (parser->xml);

	if (!strcmp (tagname, "timezone")) {
	    zone = parse_timezone (parser);
	    if (zone)
		g_ptr_array_add (zones, zone);
	}

	if (xmlTextReaderNext (parser->xml) != 1)
	    goto error_out;
    }
    if (xmlTextReaderRead (parser->xml) != 1)
	goto error_out;

    g_ptr_array_add (zones, NULL);
    return (GWeatherTimezone **)g_ptr_array_free (zones, FALSE);

error_out:
    for (i = 0; i < zones->len; i++)
	gweather_timezone_unref (zones->pdata[i]);
    g_ptr_array_free (zones, TRUE);
    return NULL;
}

/**
 * gweather_timezone_ref:
 * @zone: a #GWeatherTimezone
 *
 * Adds 1 to @zone's reference count.
 *
 * Return value: @zone
 **/
GWeatherTimezone *
gweather_timezone_ref (GWeatherTimezone *zone)
{
    g_return_val_if_fail (zone != NULL, NULL);

    zone->ref_count++;
    return zone;
}

/**
 * gweather_timezone_unref:
 * @zone: a #GWeatherTimezone
 *
 * Subtracts 1 from @zone's reference count and frees it if it reaches 0.
 **/
void
gweather_timezone_unref (GWeatherTimezone *zone)
{
    g_return_if_fail (zone != NULL);

    if (!--zone->ref_count) {
	g_free (zone->id);
	g_free (zone->name);
	g_slice_free (GWeatherTimezone, zone);
    }
}

GType
gweather_timezone_get_type (void)
{
    static volatile gsize type_volatile = 0;

    if (g_once_init_enter (&type_volatile)) {
	GType type = g_boxed_type_register_static (
	    g_intern_static_string ("GWeatherTimezone"),
	    (GBoxedCopyFunc) gweather_timezone_ref,
	    (GBoxedFreeFunc) gweather_timezone_unref);
	g_once_init_leave (&type_volatile, type);
    }
    return type_volatile;
}

/**
 * gweather_timezone_get_utc:
 *
 * Gets the UTC timezone.
 *
 * Return value: a #GWeatherTimezone for UTC, or %NULL on error.
 **/
GWeatherTimezone *
gweather_timezone_get_utc (void)
{
    GWeatherTimezone *zone = NULL;

    zone = g_slice_new0 (GWeatherTimezone);
    zone->ref_count = 1;
    zone->id = g_strdup ("GMT");
    zone->name = g_strdup (_("Greenwich Mean Time"));
    zone->offset = 0;
    zone->has_dst = FALSE;
    zone->dst_offset = 0;

    return zone;
}

/**
 * gweather_timezone_get_name:
 * @zone: a #GWeatherTimezone
 *
 * Gets @zone's name; a translated, user-presentable string.
 *
 * Note that the returned name might not be unique among timezones,
 * and may not make sense to the user unless it is presented along
 * with the timezone's country's name (or in some context where the
 * country is obvious).
 *
 * Return value: @zone's name
 **/
const char *
gweather_timezone_get_name (GWeatherTimezone *zone)
{
    g_return_val_if_fail (zone != NULL, NULL);
    return zone->name;
}

/**
 * gweather_timezone_get_tzid:
 * @zone: a #GWeatherTimezone
 *
 * Gets @zone's tzdata identifier, eg "America/New_York".
 *
 * Return value: @zone's tzid
 **/
const char *
gweather_timezone_get_tzid (GWeatherTimezone *zone)
{
    g_return_val_if_fail (zone != NULL, NULL);
    return zone->id;
}

/**
 * gweather_timezone_get_offset:
 * @zone: a #GWeatherTimezone
 *
 * Gets @zone's standard offset from UTC, in minutes. Eg, a value of
 * 120 would indicate "GMT+2".
 *
 * Return value: @zone's standard offset, in minutes
 **/
int
gweather_timezone_get_offset (GWeatherTimezone *zone)
{
    g_return_val_if_fail (zone != NULL, 0);
    return zone->offset;
}

/**
 * gweather_timezone_has_dst:
 * @zone: a #GWeatherTimezone
 *
 * Checks if @zone observes daylight/summer time for part of the year.
 *
 * Return value: %TRUE if @zone observes daylight/summer time.
 **/
gboolean
gweather_timezone_has_dst (GWeatherTimezone *zone)
{
    g_return_val_if_fail (zone != NULL, FALSE);
    return zone->has_dst;
}

/**
 * gweather_timezone_get_dst_offset:
 * @zone: a #GWeatherTimezone
 *
 * Gets @zone's daylight/summer time offset from UTC, in minutes. Eg,
 * a value of 120 would indicate "GMT+2". This is only meaningful if
 * gweather_timezone_has_dst() returns %TRUE.
 *
 * Return value: @zone's daylight/summer time offset, in minutes
 **/
int
gweather_timezone_get_dst_offset (GWeatherTimezone *zone)
{
    g_return_val_if_fail (zone != NULL, 0);
    g_return_val_if_fail (zone->has_dst, 0);
    return zone->dst_offset;
}

