#include "clock.h"

#include <sys/time.h>
#include <time.h>

static Eio_Monitor *clock_te_monitor = NULL;
static Eio_Monitor *clock_tz2_monitor = NULL;
static Eio_Monitor *clock_tzetc_monitor = NULL;
static Eina_List *handlers = NULL;

static Ecore_Timer *update_today = NULL;

#define ZONEINFO_DIR "/usr/share/zoneinfo/posix"
#define ZONEINFO_DIR_LEN sizeof(ZONEINFO_DIR) - 1

#define TE_DECL \
   const char *tzenv; \
   char prevtz[128] = {0}

#define TZSET(inst) \
   tzenv = getenv("TZ"); \
   if (tzenv) \
     strncpy(prevtz, tzenv, sizeof(prevtz) - 1); \
   if (inst->cfg->timezone) \
     e_util_env_set("TZ", inst->cfg->timezone); \
   tzset()

#define TZUNSET() \
   if (prevtz[0]) \
     e_util_env_set("TZ", prevtz); \
   else \
     e_util_env_set("TZ", NULL); \
   tzset()

static void
_zoneinfo_del(void *data, Evas *e EINA_UNUSED, Evas_Object *obj EINA_UNUSED, void *event_info EINA_UNUSED)
{
   eio_file_cancel(data);
}

static void
_zoneinfo_done(void *data, Eio_File *handler EINA_UNUSED)
{
   evas_object_event_callback_del(data, EVAS_CALLBACK_DEL, _zoneinfo_del);
}

static void
_zoneinfo_error(void *data, Eio_File *handler EINA_UNUSED, int error EINA_UNUSED)
{
   evas_object_event_callback_del(data, EVAS_CALLBACK_DEL, _zoneinfo_del);
}

static Eina_Bool
_zoneinfo_filter(void *data EINA_UNUSED, Eio_File *handler EINA_UNUSED, const Eina_File_Direct_Info *info)
{
   return (info->type == EINA_FILE_REG) || (info->type == EINA_FILE_DIR);
}

static void
_zoneinfo_main(void *data, Eio_File *handler EINA_UNUSED, const Eina_File_Direct_Info *info)
{
   if (info->type == EINA_FILE_REG)
     config_timezone_populate(data, &info->path[ZONEINFO_DIR_LEN + 1]);
   else
     {
        Eio_File *ls;

        ls = eio_file_direct_ls(info->path, _zoneinfo_filter, _zoneinfo_main, _zoneinfo_done, _zoneinfo_error, data);
        evas_object_event_callback_add(data, EVAS_CALLBACK_DEL, _zoneinfo_del, ls);
     }
}

EINTERN void
time_zoneinfo_scan(Evas_Object *obj)
{
   Eio_File *ls;

   ls = eio_file_direct_ls(ZONEINFO_DIR, _zoneinfo_filter, _zoneinfo_main, _zoneinfo_done, _zoneinfo_error, obj);
   evas_object_event_callback_add(obj, EVAS_CALLBACK_DEL, _zoneinfo_del, ls);
}

EINTERN void
time_daynames_clear(Instance *inst)
{
   int x;

   for (x = 0; x < 7; x++)
     eina_stringshare_replace(&inst->daynames[x], NULL);
}

EINTERN void
time_datestring_format(Instance *inst, char *buf, int bufsz)
{
   struct timeval timev;
   struct tm *tm;
   time_t tt;
   const char *default_str = "%F";
   TE_DECL;

   buf[0] = 0;
   if (!inst->cfg->show_date) return;
   TZSET(inst);
   gettimeofday(&timev, NULL);
   tt = (time_t)(timev.tv_sec);
   tm = localtime(&tt);
   TZUNSET();
   switch (inst->cfg->show_date)
     {
      case CLOCK_DATE_DISPLAY_FULL:
        strftime(buf, bufsz, _("%a, %e %b, %Y"), (const struct tm *)tm);
        break;
      case CLOCK_DATE_DISPLAY_NUMERIC:
        strftime(buf, bufsz, _("%a, %x"), (const struct tm *)tm);
        break;
      case CLOCK_DATE_DISPLAY_DATE_ONLY:
        strftime(buf, bufsz, "%x", (const struct tm *)tm);
        break;
      case CLOCK_DATE_DISPLAY_ISO8601:
        strftime(buf, bufsz, "%F", (const struct tm *)tm);
        break;
      case CLOCK_DATE_DISPLAY_CUSTOM:
/* disable warning for known-safe code */
DISABLE_WARNING(format-nonliteral, format-nonliteral, format-nonliteral)
        if (!strftime(buf, bufsz, inst->cfg->time_str[1] ?: default_str, (const struct tm *)tm))
          strncpy(buf, "ERROR", bufsz - 1);
ENABLE_WARNING(format-nonliteral, format-nonliteral, format-nonliteral)
        break;
      default: break;
     }
}

EINTERN int
time_string_format(Instance *inst, char *buf, int bufsz)
{
   struct timeval timev;
   struct tm *tm;
   time_t tt;
   const char *default_fmt = "%R";
   TE_DECL;

   buf[0] = 0;
   TZSET(inst);
   gettimeofday(&timev, NULL);
   tt = (time_t)(timev.tv_sec);
   tm = localtime(&tt);
   TZUNSET();
/* disable warning for known-safe code */
DISABLE_WARNING(format-nonliteral, format-nonliteral, format-nonliteral)
   if (!strftime(buf, bufsz, inst->cfg->time_str[0] ?: default_fmt, (const struct tm *)tm))
     strncpy(buf, "ERROR", bufsz - 1);
ENABLE_WARNING(format-nonliteral, format-nonliteral, format-nonliteral)
   return tm->tm_sec;
}

EINTERN void
time_instance_update(Instance *inst)
{
   struct timeval timev;
   struct tm *tm, tms, tmm, tm2;
   time_t tt;
   int started = 0, num, i;
   int day;

   tzset();
   gettimeofday(&timev, NULL);
   tt = (time_t)(timev.tv_sec);
   tm = localtime(&tt);

   time_daynames_clear(inst);
   if (!tm) return;

   // tms == current date time "saved"
   // tm2 == date to look at adjusting for madj
   // tm2 == month baseline @ 1st
   memcpy(&tms, tm, sizeof(struct tm));
   num = 0;
   for (day = (0 - 6); day < (31 + 16); day++)
     {
        memcpy(&tmm, &tms, sizeof(struct tm));
        tmm.tm_sec = 0;
        tmm.tm_min = 0;
        tmm.tm_hour = 10;
        tmm.tm_mon += inst->madj;
        tmm.tm_mday = 1; // start at the 1st of the month
        tmm.tm_wday = 0; // ignored by mktime
        tmm.tm_yday = 0; // ignored by mktime
        tmm.tm_isdst = 0; // ignored by mktime
        tt = mktime(&tmm);
        tm = localtime(&tt);
        memcpy(&tm2, tm, sizeof(struct tm));

        tt = mktime(&tmm);
        tt += (day * 60 * 60 * 24);
        tm = localtime(&tt);
        memcpy(&tmm, tm, sizeof(struct tm));
        if (!started)
          {
             if (tm->tm_wday == inst->cfg->week.start)
               {
                  char buf[32];

                  for (i = 0; i < 7; i++, tm->tm_wday = (tm->tm_wday + 1) % 7)
                    {
                       strftime(buf, sizeof(buf), "%a", tm);
                       inst->daynames[i] = eina_stringshare_add(buf);
                    }
                  started = 1;
               }
          }
        if (started)
          {
             int y = num / 7;
             int x = num % 7;

             if (y < 6)
               {
                  inst->daynums[x][y] = tmm.tm_mday;

                  inst->dayvalids[x][y] = 0;
                  if (tmm.tm_mon == tm2.tm_mon) inst->dayvalids[x][y] = 1;

                  inst->daytoday[x][y] = 0;
                  if ((tmm.tm_mon == tms.tm_mon) &&
                      (tmm.tm_year == tms.tm_year) &&
                      (tmm.tm_mday == tms.tm_mday))
                    inst->daytoday[x][y] = 1;

                  inst->dayweekends[x][y] = 0;
                  for (i = inst->cfg->weekend.start;
                       i < (inst->cfg->weekend.start + inst->cfg->weekend.len);
                       i++)
                    {
                       if (tmm.tm_wday == (i % 7))
                         {
                            inst->dayweekends[x][y] = 1;
                            break;
                         }
                    }
               }
             num++;
          }
     }

   memcpy(&tmm, &tms, sizeof(struct tm));
   tmm.tm_sec = 0;
   tmm.tm_min = 0;
   tmm.tm_hour = 10;
   tmm.tm_mon += inst->madj;
   tmm.tm_mday = 1; // start at the 1st of the month
   tmm.tm_wday = 0; // ignored by mktime
   tmm.tm_yday = 0; // ignored by mktime
   tmm.tm_isdst = 0; // ignored by mktime
   tt = mktime(&tmm);
   tm = localtime(&tt);
   memcpy(&tm2, tm, sizeof(struct tm));
   inst->year[sizeof(inst->year) - 1] = 0;
   strftime(inst->year, sizeof(inst->year) - 1, "%Y", (const struct tm *)&tm2);
   inst->month[sizeof(inst->month) - 1] = 0;
   strftime(inst->month, sizeof(inst->month) - 1, "%B", (const struct tm *)&tm2); // %b for short month
}


static Eina_Bool
_update_today_timer(void *data EINA_UNUSED)
{
   time_t t, t_tomorrow;
   const struct tm *now;
   struct tm today;

   t = time(NULL);
   now = localtime(&t);
   memcpy(&today, now, sizeof(today));
   today.tm_sec = 1;
   today.tm_min = 0;
   today.tm_hour = 0;

   t_tomorrow = mktime(&today) + 24 * 60 * 60;
   if (update_today) ecore_timer_interval_set(update_today, t_tomorrow - t);
   else update_today = ecore_timer_loop_add(t_tomorrow - t, _update_today_timer, NULL);
   clock_date_update();
   return EINA_TRUE;
}

static void
_time_changed(void)
{
   _update_today_timer(NULL);
   clock_instances_redo();
}

static Eina_Bool
_clock_eio_update(void *d EINA_UNUSED, int type EINA_UNUSED, void *event)
{
   Eio_Monitor_Event *ev = event;

   if ((ev->monitor == clock_te_monitor) ||
       (ev->monitor == clock_tz2_monitor) ||
       (ev->monitor == clock_tzetc_monitor))
     {
        if (eina_streq(ev->filename, "/etc/localtime") ||
            eina_streq(ev->filename, "/etc/timezone"))
          {
             _time_changed();
          }
     }
   return ECORE_CALLBACK_PASS_ON;
}

static Eina_Bool
_clock_time_update(void *d EINA_UNUSED, int type EINA_UNUSED, void *event EINA_UNUSED)
{
   _time_changed();
   return ECORE_CALLBACK_PASS_ON;
}

static Eina_Bool
_clock_eio_error(void *d EINA_UNUSED, int type EINA_UNUSED, void *event)
{
   Eio_Monitor_Event *ev = event;

   if ((ev->monitor == clock_te_monitor) ||
       (ev->monitor == clock_tz2_monitor) ||
       (ev->monitor == clock_tzetc_monitor))
     {
        E_FREE_FUNC(clock_te_monitor, eio_monitor_del);
        if (ecore_file_exists("/etc/localtime"))
          clock_te_monitor = eio_monitor_add("/etc/localtime");

        E_FREE_FUNC(clock_tz2_monitor, eio_monitor_del);
        if (ecore_file_exists("/etc/timezone"))
          clock_tz2_monitor = eio_monitor_add("/etc/timezone");

        E_FREE_FUNC(clock_tzetc_monitor, eio_monitor_del);
        if (ecore_file_is_dir("/etc"))
          clock_tzetc_monitor = eio_monitor_add("/etc");
     }

   return ECORE_CALLBACK_PASS_ON;
}

static Eina_Bool
_clock_screensaver_on()
{
   clock_timer_set(0);
   return ECORE_CALLBACK_RENEW;
}

static Eina_Bool
_clock_screensaver_off()
{
   clock_timer_set(1);
   return ECORE_CALLBACK_RENEW;
}

EINTERN void
time_init(void)
{
   if (ecore_file_exists("/etc/localtime"))
     clock_te_monitor = eio_monitor_add("/etc/localtime");
   if (ecore_file_exists("/etc/timezone"))
     clock_tz2_monitor = eio_monitor_add("/etc/timezone");
   if (ecore_file_is_dir("/etc"))
     clock_tzetc_monitor = eio_monitor_add("/etc");

   E_LIST_HANDLER_APPEND(handlers, EIO_MONITOR_ERROR, _clock_eio_error, NULL);
   E_LIST_HANDLER_APPEND(handlers, EIO_MONITOR_FILE_CREATED, _clock_eio_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, EIO_MONITOR_FILE_MODIFIED, _clock_eio_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, EIO_MONITOR_FILE_DELETED, _clock_eio_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, EIO_MONITOR_SELF_DELETED, _clock_eio_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, EIO_MONITOR_SELF_RENAME, _clock_eio_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, E_EVENT_SYS_RESUME, _clock_time_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, ECORE_EVENT_SYSTEM_TIMEDATE_CHANGED, _clock_time_update, NULL);
   E_LIST_HANDLER_APPEND(handlers, E_EVENT_SCREENSAVER_ON, _clock_screensaver_on, NULL);
   E_LIST_HANDLER_APPEND(handlers, E_EVENT_SCREENSAVER_OFF, _clock_screensaver_off, NULL);
   _update_today_timer(NULL);
}

EINTERN void
time_shutdown(void)
{
   E_FREE_FUNC(update_today, ecore_timer_del);
   E_FREE_FUNC(clock_te_monitor, eio_monitor_del);
   E_FREE_FUNC(clock_tz2_monitor, eio_monitor_del);
   E_FREE_FUNC(clock_tzetc_monitor, eio_monitor_del);
}
