/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* 
 * Authors : 
 *  Chenthill Palanisamy <pchenthill@novell.com>
 *
 * Copyright 2007, Novell, Inc.
 *
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of version 2 of the GNU Lesser General Public 
 * License as published by the Free Software Foundation.
 *
 * This program 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, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, 
 * Boston, MA 02110-1301, USA.
 */

/*
 * Compile with
 * gcc -c -Wall -DDISABLE_ICALTZUTIL_GET_ZONE_DIRECTORY  -DICALTZ_UTIL_MAIN -DHAVE_UNISTD_H  -DHAVE_ENDIAN_H -DHAVE_BYTESWAP_H $(pkg-config --cflags --libs libical) -o icaltz-util icaltz-util.c
 * to get an utility which will print the VTIMEZONE definition of
 * a certain location, for example "Europe/Berlin".
 *
 * With -DDISABLE_ICALTZUTIL_GET_ZONE_DIRECTORY the
 * icaltzutil_get_zone_directory() from libical will be used.
 */

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

#ifdef ICALTZ_UTIL_MAIN
#include <stdio.h>
#endif

#ifdef EVOLUTION_ICAL_COMPATIBILITY
# include "eds_abi_wrapper.h"
# else
# include <libical/icaltimezone.h>
#endif

#ifdef HAVE_STDINT_H
#include <stdint.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#if defined(sun) && defined(__SVR4)
#include <sys/types.h>
#include <sys/byteorder.h>
#else
# ifdef HAVE_BYTESWAP_H
#  include <byteswap.h>
# endif
# ifdef HAVE_ENDIAN_H
#  include <endian.h>
# else
# ifdef HAVE_SYS_ENDIAN_H
#  include <sys/endian.h>
#  ifdef bswap32
#   define bswap_32 bswap32
#  else
#   define bswap_32 swap32
#  endif
# endif
# endif
#endif

#ifdef _MSC_VER
#if !defined(HAVE_BYTESWAP_H) && !defined(HAVE_SYS_ENDIAN_H) && !defined(HAVE_ENDIAN_H)
#define bswap_16(x) (((x) << 8) & 0xff00) | (((x) >> 8 ) & 0xff)
#define bswap_32(x) (((x) << 24) & 0xff000000)  \
                    | (((x) << 8) & 0xff0000)   \
                    | (((x) >> 8) & 0xff00)     \
                    | (((x) >> 24) & 0xff )
#define bswap_64(x) ((((x) & 0xff00000000000000ull) >> 56) \
                    | (((x) & 0x00ff000000000000ull) >> 40) \
                    | (((x) & 0x0000ff0000000000ull) >> 24) \
                    | (((x) & 0x000000ff00000000ull) >> 8) \
                    | (((x) & 0x00000000ff000000ull) << 8) \
                    | (((x) & 0x0000000000ff0000ull) << 24) \
                    | (((x) & 0x000000000000ff00ull) << 40) \
                    | (((x) & 0x00000000000000ffull) << 56))
#endif
#include <io.h>
#endif

#if defined(__APPLE__)
#define bswap_16(x) (((x) << 8) & 0xff00) | (((x) >> 8 ) & 0xff)
#define bswap_32 __builtin_bswap32
#define bswap_64 __builtin_bswap64
#endif

#ifndef PATH_MAX
#define PATH_MAX 512
#endif

#ifndef F_OK
#define F_OK 0
#endif

#ifndef R_OK
#define R_OK 4
#endif

#include <limits.h>
#include <time.h>
#include <stdlib.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <libical/icalerror.h>
#include "icaltz-util.h"

typedef struct 
{
	char	ttisgmtcnt [4];	
	char	ttisstdcnt[4];	
	char	leapcnt[4];		
	char	timecnt[4];	
	char	typecnt[4];
	char	charcnt[4];			
} tzinfo; 

static int r_pos [] = {1, 2, 3, -2, -1};

#ifndef DISABLE_ICALTZUTIL_GET_ZONE_DIRECTORY
static char *search_paths [] = {"/usr/share/zoneinfo","/usr/lib/zoneinfo","/etc/zoneinfo","/usr/share/lib/zoneinfo"};
static char *zdir = NULL;
#endif

#define NUM_SEARCH_PATHS (sizeof (search_paths)/ sizeof (search_paths [0]))
#define EFREAD(buf,size,num,fs) \
	if (fread (buf, size, num, fs) == 0  && ferror (fs)) {\
		icalerror_set_errno (ICAL_FILE_ERROR);		\
		goto error;					\
	}							\

typedef struct
{
	long int gmtoff;
	unsigned char isdst;
	unsigned int abbr;	
	unsigned char isstd;
	unsigned char isgmt;
	char *zname;

} ttinfo;

typedef struct
{
	time_t transition;
	long int change;
} leap;

#ifndef EVOLUTION_ICAL_COMPATIBILITY
extern const char *ical_tzid_prefix;
#endif

static int
decode (const void *ptr)
{
#if defined(sun) && defined(__SVR4)
    if (sizeof (int) == 4)
#ifdef _BIG_ENDIAN
        return *(const int *) ptr;
#else
        return BSWAP_32 (*(const int *) ptr);
#endif
#else
    if ((BYTE_ORDER == BIG_ENDIAN) && sizeof (int) == 4)
        return *(const int *) ptr;
    else if (BYTE_ORDER == LITTLE_ENDIAN && sizeof (int) == 4)
        return bswap_32 (*(const int *) ptr);
#endif
	else
	{
		const unsigned char *p = ptr;
		unsigned result = *p & (1 << (CHAR_BIT - 1)) ? ~0 : 0;

		result = (result << 8) | *p++;
		result = (result << 8) | *p++;
		result = (result << 8) | *p++;
		result = (result << 8) | *p++;

		return result;
	}
}

static char *
zname_from_stridx (char *str, long int idx) 
{
	int i = 0;
	char *ret;
	size_t size;

	i = idx;

	while (str [i] != '\0') 
		i++;

	size = i - idx;
	str += idx;
	ret = (char *) malloc (size + 1);
	ret = strncpy (ret, str, size);
	ret [size] = '\0';

	return ret;
}

static void 
find_transidx (time_t *transitions, ttinfo *types, int *trans_idx, long int num_trans, int *stdidx, int *dstidx) 
{
	time_t now, year_start;
	int i, found = 0;
	struct icaltimetype itime;

	now = time (NULL);
	itime = icaltime_from_timet_with_zone (now, 0, NULL);
	itime.month = itime.day = 1;
	itime.hour = itime.minute = itime.second = 0;
	year_start = icaltime_as_timet(itime);

	/* Set this by default */
	*stdidx = (num_trans - 1);

	for (i = (num_trans - 1); i >= 0; --i)
		if (year_start < transitions [i]) {
			int idx;
			found = 1;
			idx = trans_idx [i];
			(types [idx].isdst) ? (*dstidx = i) : (*stdidx = i);
		}

	/* If the transition found is the last among the list, prepare to use the last two transtions. 
	 * Using this will most likely throw the DTSTART of the resulting component off by 1 or 2 days
	 * but it would set right by the adjustment made. 
	 * NOTE: We need to use the last two transitions only because there is no data for the future 
	 * transitions. 
	 */
	if (found && (*dstidx == -1)) {
		*dstidx = ((*stdidx) - 1);
	}

	return;
}

#ifndef DISABLE_ICALTZUTIL_GET_ZONE_DIRECTORY
static void
set_zonedir (void)
{
	char file_path[PATH_MAX];
	const char *fname = ZONES_TAB_SYSTEM_FILENAME;
	int i;	

	for (i = 0;i < NUM_SEARCH_PATHS; i++) {
		sprintf (file_path, "%s/%s", search_paths [i], fname);
		if (!access (file_path, F_OK|R_OK)) {
			zdir = search_paths [i];
			break;
		}
	}
}


const char *
icaltzutil_get_zone_directory (void)
{
	if (!zdir)
		set_zonedir ();

	return zdir;
}
#endif

/* Calculate the relative position of the week in a month from a date */
static int
calculate_pos (icaltimetype icaltime)
{
	int pos;

	pos = (icaltime.day -1) / 7;

	/* Check if pos 3 is the last occurence of the week day in the month */	
	if (pos == 3 && ((icaltime.day + 7) > icaltime_days_in_month (icaltime.month, icaltime.year))) 
		pos = 4;

	return r_pos [pos];
}

static void
adjust_dtstart_day_to_rrule (icalcomponent *comp, struct icalrecurrencetype rule)
{
	time_t now, year_start;
	struct icaltimetype start, comp_start, iter_start, itime;
	icalrecur_iterator *iter;

	now = time (NULL);
	itime = icaltime_from_timet_with_zone (now, 0, NULL);
	itime.month = itime.day = 1;
	itime.hour = itime.minute = itime.second = 0;
	year_start = icaltime_as_timet(itime);

	comp_start = icalcomponent_get_dtstart (comp);
	start = icaltime_from_timet_with_zone (year_start, 0, NULL);

	iter = icalrecur_iterator_new (rule, start);
	iter_start = icalrecur_iterator_next (iter);
	icalrecur_iterator_free (iter);

	if (iter_start.day != comp_start.day) {
		comp_start.day = iter_start.day;
		icalcomponent_set_dtstart (comp, comp_start);
	}
}

icalcomponent*
icaltzutil_fetch_timezone (const char *location)
{
	int ret = 0;
	FILE *f;
	tzinfo type_cnts;
	unsigned int i, num_trans, num_types = 0, num_chars, num_leaps, num_isstd, num_isgmt;
	time_t *transitions = NULL;
	time_t trans;
	int *trans_idx = NULL, dstidx = -1, stdidx = -1, pos, sign, zidx, zp_idx;
	ttinfo *types = NULL;
	char *znames = NULL, *full_path, *tzid, *r_trans, *temp = NULL;
	leap *leaps = NULL;
	icalcomponent *tz_comp = NULL, *dst_comp = NULL, *std_comp = NULL;
	icalproperty *icalprop;
	icaltimetype dtstart, icaltime;
	struct icalrecurrencetype ical_recur;
	const char *basedir;

	/* if (!location) return NULL; */

	basedir = icaltzutil_get_zone_directory();
	if (!basedir) {
		icalerror_set_errno (ICAL_FILE_ERROR);
		return NULL;
	}

	full_path = (char *) malloc (strlen (basedir) + strlen (location) + 2);
	sprintf (full_path,"%s/%s",basedir, location);

	if ((f = fopen (full_path, "rb")) == 0) {
		icalerror_set_errno (ICAL_FILE_ERROR);
		free (full_path);
		return NULL;
	}

	if ((ret = fseek (f, 20, SEEK_SET)) != 0) {
		icalerror_set_errno (ICAL_FILE_ERROR);
		goto error;	
	}

	EFREAD(&type_cnts, 24, 1, f);

	num_isgmt = decode (type_cnts.ttisgmtcnt);
	num_leaps = decode (type_cnts.leapcnt);
	num_chars = decode (type_cnts.charcnt);
	num_trans = decode (type_cnts.timecnt);
	num_isstd = decode (type_cnts.ttisstdcnt);
	num_types = decode (type_cnts.typecnt);

	transitions = calloc (num_trans, sizeof (time_t));
	temp = calloc (num_trans, 4);
	r_trans = temp;
	EFREAD(r_trans, 4, num_trans, f);

	if (num_trans) {
		trans_idx = calloc (num_trans, sizeof (int));
		for (i = 0; i < num_trans; i++) {
			trans_idx [i] = fgetc (f);
			transitions [i] = decode (r_trans);
			r_trans += 4;
		}
	}
	
	free (temp);
	temp = NULL;

	types = calloc (num_types, sizeof (ttinfo));
	for (i = 0; i < num_types; i++) {
		unsigned char a [4];
		int c;

		EFREAD(a, 4, 1, f);
		c = fgetc (f);
		types [i].isdst = c;
		if((c = fgetc (f)) < 0) {
		   break;
		}
		types [i].abbr = c;
		types [i].gmtoff = decode (a);
	}

	znames = (char *) malloc (num_chars);
	EFREAD(znames, num_chars, 1, f);

	/* We got all the information which we need */

	leaps = calloc (num_leaps, sizeof (leap));
	for (i = 0; i < num_leaps; i++) {
		char c [4];

		EFREAD (c, 4, 1, f);
		leaps [i].transition = decode (c);

		EFREAD (c, 4, 1, f);
		leaps [i].change = decode (c);
	}

	for (i = 0; i < num_isstd; ++i) {
		int c = getc (f);
		types [i].isstd = c != 0;
	}

	while (i < num_types)
		types [i++].isstd = 0;

	for (i = 0; i <  num_isgmt; ++i) {
		int c = getc (f);
		types [i].isgmt = c != 0;
	}

	while (i < num_types)
		types [i++].isgmt = 0;

	/* Read all the contents now */

	for (i = 0; i < num_types; i++) 
		types [i].zname = zname_from_stridx (znames, types [i].abbr);

	if (num_trans != 0)
		find_transidx (transitions, types, trans_idx, num_trans, &stdidx, &dstidx);
	else
		stdidx = 0;

	tz_comp = icalcomponent_new (ICAL_VTIMEZONE_COMPONENT);

	/* Add tzid property */
	tzid = (char *) malloc (strlen (ical_tzid_prefix) + strlen (location) + 8);
	sprintf (tzid, "%sTzfile/%s", ical_tzid_prefix, location);
	icalprop = icalproperty_new_tzid (tzid);
	icalcomponent_add_property (tz_comp, icalprop);
	free (tzid);

	icalprop = icalproperty_new_x (location);
	icalproperty_set_x_name (icalprop, "X-LIC-LOCATION");
	icalcomponent_add_property (tz_comp, icalprop);
	
	if (stdidx != -1) {
		if (num_trans != 0)
			zidx = trans_idx [stdidx];
		else 
			zidx = 0;

		std_comp = icalcomponent_new (ICAL_XSTANDARD_COMPONENT);
		icalprop = icalproperty_new_tzname (types [zidx].zname);
		icalcomponent_add_property (std_comp, icalprop);

		if (dstidx != -1)
			zp_idx = trans_idx [stdidx-1]; 
		else
			zp_idx = zidx;
		/* DTSTART localtime uses TZOFFSETFROM UTC offset */
		if (num_trans != 0)
			trans = transitions [stdidx] + types [zp_idx].gmtoff;
		else
			trans = types [zp_idx].gmtoff;
		icaltime = icaltime_from_timet_with_zone (trans, 0, NULL);
		dtstart = icaltime;
		dtstart.year = 1970;
		dtstart.minute = dtstart.second = 0;
		icalprop = icalproperty_new_dtstart (dtstart);
		icalcomponent_add_property (std_comp, icalprop);

		/* If DST changes are present use RRULE */
		if (dstidx != -1) {
			icalrecurrencetype_clear (&ical_recur);
			ical_recur.freq = ICAL_YEARLY_RECURRENCE;
			ical_recur.by_month [0] = icaltime.month;
			pos = calculate_pos (icaltime);
			pos < 0 ? (sign = -1): (sign = 1);
			ical_recur.by_day [0] = sign * ((abs (pos) * 8) + icaltime_day_of_week (icaltime));
			icalprop = icalproperty_new_rrule (ical_recur);
			icalcomponent_add_property (std_comp, icalprop);

			adjust_dtstart_day_to_rrule (std_comp, ical_recur);
		}
        icalprop = icalproperty_new_tzoffsetfrom (types [zp_idx].gmtoff);
        icalcomponent_add_property (std_comp, icalprop);

		icalprop = icalproperty_new_tzoffsetto (types [zidx].gmtoff);
		icalcomponent_add_property (std_comp, icalprop);

		icalcomponent_add_component (tz_comp, std_comp);
	} else 
		icalerror_set_errno (ICAL_MALFORMEDDATA_ERROR);
	
	if (dstidx != -1) {
		zidx = trans_idx [dstidx];
		zp_idx = trans_idx [dstidx-1];
		dst_comp = icalcomponent_new (ICAL_XDAYLIGHT_COMPONENT);
		icalprop = icalproperty_new_tzname (types [zidx].zname);
		icalcomponent_add_property (dst_comp, icalprop);

		/* DTSTART localtime uses TZOFFSETFROM UTC offset */
		if (num_trans != 0)
			trans = transitions [dstidx] + types [zp_idx].gmtoff;
		else
			trans = types [zp_idx].gmtoff;
		icaltime = icaltime_from_timet_with_zone (trans, 0, NULL);
		dtstart = icaltime;
		dtstart.year = 1970;
		dtstart.minute = dtstart.second = 0;
		icalprop = icalproperty_new_dtstart (dtstart);
		icalcomponent_add_property (dst_comp, icalprop);

		icalrecurrencetype_clear (&ical_recur);
		ical_recur.freq = ICAL_YEARLY_RECURRENCE;
		ical_recur.by_month [0] = icaltime.month;
		pos = calculate_pos (icaltime);
		pos < 0 ? (sign = -1): (sign = 1);
		ical_recur.by_day [0] = sign * ((abs (pos) * 8) + icaltime_day_of_week (icaltime));
		icalprop = icalproperty_new_rrule (ical_recur);
		icalcomponent_add_property (dst_comp, icalprop);

		adjust_dtstart_day_to_rrule (dst_comp, ical_recur);

		icalprop = icalproperty_new_tzoffsetfrom (types [zp_idx].gmtoff);
		icalcomponent_add_property (dst_comp, icalprop);

		icalprop = icalproperty_new_tzoffsetto (types [zidx].gmtoff);
		icalcomponent_add_property (dst_comp, icalprop);

		icalcomponent_add_component (tz_comp, dst_comp);
	}

error:
	if (f)
		fclose  (f);

	if (transitions)
		free (transitions);
	if (trans_idx)
		free (trans_idx);
	if (types) {
		for (i = 0; i < num_types; i++) 
			if (types [i].zname)
				free (types [i].zname);
		free (types);
	}
	if (znames)
		free (znames);
	free (full_path);
	if (leaps)
		free (leaps);
	if (temp)
		free (temp);

	return tz_comp;
}

/*
 * What follows is copied and slightly simplified (not thread-safe!)
 * code from icaltimezone.c.
 *
 * This is necessary because otherwise, when libsynthesis.so calls
 * icaltimezone_get_component(), libical.1.so uses its own builtin
 * icaltzutil_fetch_timezone().  Apparently it exports that without
 * looking it up via the dynamic linker itself. Therefore we have to
 * redirect the original icaltimezone_get_component() call.
 */

static void
icaltimezone_load_builtin_timezone	(icaltimezone *zone)
{
    icalcomponent *subcomp;
    const char *location;

    /* If the location isn't set, it isn't a builtin timezone. */
    location = icaltimezone_get_location(zone);
    if (!location || !location[0])
	return;

    subcomp = icaltzutil_fetch_timezone (location);

    if (!subcomp) {
	icalerror_set_errno(ICAL_PARSE_ERROR);
	return;
    }

    icaltimezone_set_component(zone, subcomp);
}

#undef icaltimezone_get_component
icalcomponent *icaltimezone_get_component(icaltimezone *zone)
{
	icalcomponent *comp;
	/* If this is a floating time, without a timezone, return NULL. */
	if (!zone)
		return NULL;

	/*
	 * Without this check, icaltimezone_set_component() in
	 * icaltimezone_load_builtin_timezone() will discard the
	 * already loaded component of builtin timezones and replace
	 * it with the new one, so there is no leak. It's just
	 * inefficient.
	 *
	 * However, this method also gets called for non-internal
	 * timezones which were created from a VTIMEZONE and in
	 * that case not using the existing component is wrong.
	 *
	 * Hack: duplicate the internal _icaltimezone struct (from
	 * icaltimezoneimpl.h).
	 */
	struct _my_icaltimezone {
		char *tzid;
		char *location;
		char *tznames;
		double latitude;
		double longitude;
		icalcomponent *component;
	};
	comp = ((struct _my_icaltimezone *)zone)->component;
	if (!comp) {
		icaltimezone_load_builtin_timezone (zone);
		comp = ((struct _my_icaltimezone *)zone)->component;
	}

	return comp;
}

#ifdef ICALTZ_UTIL_MAIN
int 
main (int argc, char *argv [])
{
	icalcomponent *comp = icaltzutil_fetch_timezone (argv [1]);
	const char *str = comp ? icalcomponent_as_ical_string(comp) : "no such zone";
	puts(str);
	return 0;
}
#endif
