/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/* Generic timezone utilities.
 *
 * Copyright (C) 2000-2001 Ximian, Inc.
 *
 * Authors: Hans Petter Jansson <hpj@ximian.com>
 * 
 * Largely based on Michael Fulbright's work on Anaconda.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */


#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <math.h>
#include <string.h>
#include <ctype.h>
#include "tz.h"


/* Forward declarations for private functions */

static float convert_pos (gchar *pos, int digits);
static int compare_country_names (const void *a, const void *b);
static void sort_locations_by_country (GPtrArray *locations);
static gchar * tz_data_file_get (void);
static void load_backward_tz (TzDB *tz_db);

/* ---------------- *
 * Public interface *
 * ---------------- */
TzDB *
tz_load_db (void)
{
	gchar *tz_data_file;
	TzDB *tz_db;
	FILE *tzfile;
	char buf[4096];

	tz_data_file = tz_data_file_get ();
	if (!tz_data_file) {
		g_warning ("Could not get the TimeZone data file name");
		return NULL;
	}
	tzfile = fopen (tz_data_file, "r");
	if (!tzfile) {
		g_warning ("Could not open *%s*\n", tz_data_file);
		g_free (tz_data_file);
		return NULL;
	}

	tz_db = g_new0 (TzDB, 1);
	tz_db->locations = g_ptr_array_new ();

	while (fgets (buf, sizeof(buf), tzfile))
	{
		gchar **tmpstrarr;
		gchar *latstr, *lngstr, *p;
		TzLocation *loc;

		if (*buf == '#') continue;

		g_strchomp(buf);
		tmpstrarr = g_strsplit(buf,"\t", 6);
		
		latstr = g_strdup (tmpstrarr[1]);
		p = latstr + 1;
		while (*p != '-' && *p != '+') p++;
		lngstr = g_strdup (p);
		*p = '\0';
		
		loc = g_new0 (TzLocation, 1);
		loc->country = g_strdup (tmpstrarr[0]);
		loc->zone = g_strdup (tmpstrarr[2]);
		loc->latitude  = convert_pos (latstr, 2);
		loc->longitude = convert_pos (lngstr, 3);
		
#ifdef __sun
		if (tmpstrarr[3] && *tmpstrarr[3] == '-' && tmpstrarr[4])
			loc->comment = g_strdup (tmpstrarr[4]);

		if (tmpstrarr[3] && *tmpstrarr[3] != '-' && !islower(loc->zone)) {
			TzLocation *locgrp;

			/* duplicate entry */
			locgrp = g_new0 (TzLocation, 1);
			locgrp->country = g_strdup (tmpstrarr[0]);
			locgrp->zone = g_strdup (tmpstrarr[3]);
			locgrp->latitude  = convert_pos (latstr, 2);
			locgrp->longitude = convert_pos (lngstr, 3);
			locgrp->comment = (tmpstrarr[4]) ? g_strdup (tmpstrarr[4]) : NULL;

			g_ptr_array_add (tz_db->locations, (gpointer) locgrp);
		}
#else
		loc->comment = (tmpstrarr[3]) ? g_strdup(tmpstrarr[3]) : NULL;
#endif

		g_ptr_array_add (tz_db->locations, (gpointer) loc);

		g_free (latstr);
		g_free (lngstr);
		g_strfreev (tmpstrarr);
	}
	
	fclose (tzfile);
	
	/* now sort by country */
	sort_locations_by_country (tz_db->locations);
	
	g_free (tz_data_file);

	/* Load up the hashtable of backward links */
	load_backward_tz (tz_db);

	return tz_db;
}

void
tz_location_free (TzLocation *loc)
{
	g_free (loc->country);
	g_free (loc->zone);
	g_free (loc->comment);

	g_free (loc);
}

void
tz_db_free (TzDB *db)
{
	g_ptr_array_foreach (db->locations, (GFunc) tz_location_free, NULL);
	g_ptr_array_free (db->locations, TRUE);
	g_hash_table_destroy (db->backward);
	g_free (db);
}

GPtrArray *
tz_get_locations (TzDB *db)
{
	return db->locations;
}


gchar *
tz_location_get_country (TzLocation *loc)
{
	return loc->country;
}


gchar *
tz_location_get_zone (TzLocation *loc)
{
	return loc->zone;
}


gchar *
tz_location_get_comment (TzLocation *loc)
{
	return loc->comment;
}


void
tz_location_get_position (TzLocation *loc, double *longitude, double *latitude)
{
	*longitude = loc->longitude;
	*latitude = loc->latitude;
}

glong
tz_location_get_utc_offset (TzLocation *loc)
{
	TzInfo *tz_info;
	glong offset;

	tz_info = tz_info_from_location (loc);
	offset = tz_info->utc_offset;
	tz_info_free (tz_info);
	return offset;
}

TzInfo *
tz_info_from_location (TzLocation *loc)
{
	TzInfo *tzinfo;
	time_t curtime;
	struct tm *curzone;
	gchar *tz_env_value;
	
	g_return_val_if_fail (loc != NULL, NULL);
	g_return_val_if_fail (loc->zone != NULL, NULL);
	
	tz_env_value = g_strdup (getenv ("TZ"));
	setenv ("TZ", loc->zone, 1);
	
#if 0
	tzset ();
#endif
	tzinfo = g_new0 (TzInfo, 1);

	curtime = time (NULL);
	curzone = localtime (&curtime);

#ifndef __sun
	/* Currently this solution doesnt seem to work - I get that */
	/* America/Phoenix uses daylight savings, which is wrong    */
	tzinfo->tzname_normal = g_strdup (curzone->tm_zone);
	if (curzone->tm_isdst) 
		tzinfo->tzname_daylight =
			g_strdup (&curzone->tm_zone[curzone->tm_isdst]);
	else
		tzinfo->tzname_daylight = NULL;

	tzinfo->utc_offset = curzone->tm_gmtoff;
#else
	tzinfo->tzname_normal = NULL;
	tzinfo->tzname_daylight = NULL;
	tzinfo->utc_offset = 0;
#endif

	tzinfo->daylight = curzone->tm_isdst;

	if (tz_env_value)
		setenv ("TZ", tz_env_value, 1);
	else
		unsetenv ("TZ");

	g_free (tz_env_value);
	
	return tzinfo;
}


void
tz_info_free (TzInfo *tzinfo)
{
	g_return_if_fail (tzinfo != NULL);
	
	if (tzinfo->tzname_normal) g_free (tzinfo->tzname_normal);
	if (tzinfo->tzname_daylight) g_free (tzinfo->tzname_daylight);
	g_free (tzinfo);
}

struct {
	const char *orig;
	const char *dest;
} aliases[] = {
	{ "Asia/Istanbul",  "Europe/Istanbul" },	/* Istanbul is in both Europe and Asia */
	{ "Europe/Nicosia", "Asia/Nicosia" },		/* Ditto */
	{ "EET",            "Europe/Istanbul" },	/* Same tz as the 2 above */
	{ "HST",            "Pacific/Honolulu" },
	{ "WET",            "Europe/Brussels" },	/* Other name for the mainland Europe tz */
	{ "CET",            "Europe/Brussels" },	/* ditto */
	{ "MET",            "Europe/Brussels" },
	{ "Etc/Zulu",       "Etc/GMT" },
	{ "Etc/UTC",        "Etc/GMT" },
	{ "GMT",            "Etc/GMT" },
	{ "Greenwich",      "Etc/GMT" },
	{ "Etc/UCT",        "Etc/GMT" },
	{ "Etc/GMT0",       "Etc/GMT" },
	{ "Etc/GMT+0",      "Etc/GMT" },
	{ "Etc/GMT-0",      "Etc/GMT" },
	{ "Etc/Universal",  "Etc/GMT" },
	{ "PST8PDT",        "America/Los_Angeles" },	/* Other name for the Atlantic tz */
	{ "EST",            "America/New_York" },	/* Other name for the Eastern tz */
	{ "EST5EDT",        "America/New_York" },	/* ditto */
	{ "CST6CDT",        "America/Chicago" },	/* Other name for the Central tz */
	{ "MST",            "America/Denver" },		/* Other name for the mountain tz */
	{ "MST7MDT",        "America/Denver" },		/* ditto */
};

static gboolean
compare_timezones (const char *a,
		   const char *b)
{
	if (g_str_equal (a, b))
		return TRUE;
	if (strchr (b, '/') == NULL) {
		char *prefixed;

		prefixed = g_strdup_printf ("/%s", b);
		if (g_str_has_suffix (a, prefixed)) {
			g_free (prefixed);
			return TRUE;
		}
		g_free (prefixed);
	}

	return FALSE;
}

char *
tz_info_get_clean_name (TzDB *tz_db,
			const char *tz)
{
	char *ret;
	const char *timezone;
	guint i;
	gboolean replaced;

	/* Remove useless prefixes */
	if (g_str_has_prefix (tz, "right/"))
		tz = tz + strlen ("right/");
	else if (g_str_has_prefix (tz, "posix/"))
		tz = tz + strlen ("posix/");

	/* Here start the crazies */
	replaced = FALSE;

	for (i = 0; i < G_N_ELEMENTS (aliases); i++) {
		if (compare_timezones (tz, aliases[i].orig)) {
			replaced = TRUE;
			timezone = aliases[i].dest;
			break;
		}
	}

	/* Try again! */
	if (!replaced) {
		/* Ignore crazy solar times from the '80s */
		if (g_str_has_prefix (tz, "Asia/Riyadh") ||
		    g_str_has_prefix (tz, "Mideast/Riyadh")) {
			timezone = "Asia/Riyadh";
			replaced = TRUE;
		}
	}

	if (!replaced)
		timezone = tz;

	ret = g_hash_table_lookup (tz_db->backward, timezone);
	if (ret == NULL)
		return g_strdup (timezone);
	return g_strdup (ret);
}

/* ----------------- *
 * Private functions *
 * ----------------- */

static gchar *
tz_data_file_get (void)
{
	gchar *file;

	file = g_strdup (TZ_DATA_FILE);

	return file;
}

static float
convert_pos (gchar *pos, int digits)
{
	gchar whole[10];
	gchar *fraction;
	gint i;
	float t1, t2;
	
	if (!pos || strlen(pos) < 4 || digits > 9) return 0.0;
	
	for (i = 0; i < digits + 1; i++) whole[i] = pos[i];
	whole[i] = '\0';
	fraction = pos + digits + 1;

	t1 = g_strtod (whole, NULL);
	t2 = g_strtod (fraction, NULL);

	if (t1 >= 0.0) return t1 + t2/pow (10.0, strlen(fraction));
	else return t1 - t2/pow (10.0, strlen(fraction));
}


#if 0

/* Currently not working */
static void
free_tzdata (TzLocation *tz)
{
	
	if (tz->country)
	  g_free(tz->country);
	if (tz->zone)
	  g_free(tz->zone);
	if (tz->comment)
	  g_free(tz->comment);
	
	g_free(tz);
}
#endif


static int
compare_country_names (const void *a, const void *b)
{
	const TzLocation *tza = * (TzLocation **) a;
	const TzLocation *tzb = * (TzLocation **) b;
	
	return strcmp (tza->zone, tzb->zone);
}


static void
sort_locations_by_country (GPtrArray *locations)
{
	qsort (locations->pdata, locations->len, sizeof (gpointer),
	       compare_country_names);
}

static void
load_backward_tz (TzDB *tz_db)
{
  GError *error = NULL;
  char **lines, *contents;
  guint i;

  tz_db->backward = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

  if (g_file_get_contents (GNOMECC_DATA_DIR "/datetime/backward", &contents, NULL, &error) == FALSE)
    {
      g_warning ("Failed to load 'backward' file: %s", error->message);
      return;
    }
  lines = g_strsplit (contents, "\n", -1);
  g_free (contents);
  for (i = 0; lines[i] != NULL; i++)
    {
      char **items;
      guint j;
      char *real, *alias;

      if (g_ascii_strncasecmp (lines[i], "Link\t", 5) != 0)
        continue;

      items = g_strsplit (lines[i], "\t", -1);
      real = NULL;
      alias = NULL;
      /* Skip the "Link<tab>" part */
      for (j = 1; items[j] != NULL; j++)
        {
          if (items[j][0] == '\0')
            continue;
          if (real == NULL)
            {
              real = items[j];
              continue;
            }
          alias = items[j];
          break;
        }

      if (real == NULL || alias == NULL)
        g_warning ("Could not parse line: %s", lines[i]);

      /* We don't need more than one name for it */
      if (g_str_equal (real, "Etc/UTC") ||
          g_str_equal (real, "Etc/UCT"))
        real = "Etc/GMT";

      g_hash_table_insert (tz_db->backward, g_strdup (alias), g_strdup (real));
      g_strfreev (items);
    }
  g_strfreev (lines);
}

