/* -*- c-basic-offset: 2 -*- */
/*
  Copyright(C) 2016-2018 Brazil

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License version 2.1 as published by the Free Software Foundation.

  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, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#ifdef GRN_EMBEDDED
#  define GRN_PLUGIN_FUNCTION_TAG functions_time
#endif

#include <groonga/plugin.h>

#include <math.h>

typedef enum {
  GRN_TIME_CLASSIFY_UNIT_SECOND,
  GRN_TIME_CLASSIFY_UNIT_MINUTE,
  GRN_TIME_CLASSIFY_UNIT_HOUR,
  GRN_TIME_CLASSIFY_UNIT_DAY,
  GRN_TIME_CLASSIFY_UNIT_WEEK,
  GRN_TIME_CLASSIFY_UNIT_MONTH,
  GRN_TIME_CLASSIFY_UNIT_YEAR
} grn_time_classify_unit;

static grn_obj *
func_time_classify_raw(grn_ctx *ctx,
                       int n_args,
                       grn_obj **args,
                       grn_user_data *user_data,
                       const char *function_name,
                       grn_time_classify_unit unit)
{
  grn_obj *time;
  uint32_t interval_raw = 1;
  grn_obj *classed_time;
  grn_bool accept_interval = GRN_TRUE;

  switch (unit) {
  case GRN_TIME_CLASSIFY_UNIT_SECOND :
  case GRN_TIME_CLASSIFY_UNIT_MINUTE :
  case GRN_TIME_CLASSIFY_UNIT_HOUR :
    accept_interval = GRN_TRUE;
    break;
  case GRN_TIME_CLASSIFY_UNIT_DAY :
  case GRN_TIME_CLASSIFY_UNIT_WEEK :
    accept_interval = GRN_FALSE;
    break;
  case GRN_TIME_CLASSIFY_UNIT_MONTH :
  case GRN_TIME_CLASSIFY_UNIT_YEAR :
    accept_interval = GRN_TRUE;
    break;
  }

  if (accept_interval) {
    if (!(n_args == 1 || n_args == 2)) {
      GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                       "%s(): "
                       "wrong number of arguments (%d for 1..2)",
                       function_name,
                       n_args);
      return NULL;
    }
  } else {
    if (n_args != 1) {
      GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                       "%s(): "
                       "wrong number of arguments (%d for 1)",
                       function_name,
                       n_args);
      return NULL;
    }
  }

  time = args[0];
  if (!(time->header.type == GRN_BULK &&
        time->header.domain == GRN_DB_TIME)) {
    grn_obj inspected;

    GRN_TEXT_INIT(&inspected, 0);
    grn_inspect(ctx, &inspected, time);
    GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                     "%s(): "
                     "the first argument must be a time: "
                     "<%.*s>",
                     function_name,
                     (int)GRN_TEXT_LEN(&inspected),
                     GRN_TEXT_VALUE(&inspected));
    GRN_OBJ_FIN(ctx, &inspected);
    return NULL;
  }

  if (n_args == 2) {
    grn_obj *interval;
    grn_obj casted_interval;

    interval = args[1];
    if (!(interval->header.type == GRN_BULK &&
          grn_type_id_is_number_family(ctx, interval->header.domain))) {
      grn_obj inspected;

      GRN_TEXT_INIT(&inspected, 0);
      grn_inspect(ctx, &inspected, interval);
      GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                       "%s(): "
                       "the second argument must be a number: "
                       "<%.*s>",
                       function_name,
                       (int)GRN_TEXT_LEN(&inspected),
                       GRN_TEXT_VALUE(&inspected));
      GRN_OBJ_FIN(ctx, &inspected);
      return NULL;
    }

    GRN_VALUE_FIX_SIZE_INIT(&casted_interval, 0, GRN_DB_UINT32);
    grn_obj_cast(ctx, interval, &casted_interval, GRN_FALSE);
    interval_raw = GRN_UINT32_VALUE(&casted_interval);
    GRN_OBJ_FIN(ctx, &casted_interval);

    if (interval_raw == 0) {
      grn_obj inspected;

      GRN_TEXT_INIT(&inspected, 0);
      grn_inspect(ctx, &inspected, interval);
      GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                       "%s(): "
                       "the second argument must not be zero: "
                       "<%.*s>",
                       function_name,
                       (int)GRN_TEXT_LEN(&inspected),
                       GRN_TEXT_VALUE(&inspected));
      GRN_OBJ_FIN(ctx, &inspected);
      return NULL;
    }
  }

  {
    int64_t time_raw;
    struct tm tm;
    int64_t classed_time_raw;

    time_raw = GRN_TIME_VALUE(time);
    if (!grn_time_to_tm(ctx, time_raw, &tm)) {
      return NULL;
    }

    switch (unit) {
    case GRN_TIME_CLASSIFY_UNIT_SECOND :
      tm.tm_sec = (tm.tm_sec / interval_raw) * interval_raw;
      break;
    case GRN_TIME_CLASSIFY_UNIT_MINUTE :
      tm.tm_min = (tm.tm_min / interval_raw) * interval_raw;
      tm.tm_sec = 0;
      break;
    case GRN_TIME_CLASSIFY_UNIT_HOUR :
      tm.tm_hour = (tm.tm_hour / interval_raw) * interval_raw;
      tm.tm_min = 0;
      tm.tm_sec = 0;
      break;
    case GRN_TIME_CLASSIFY_UNIT_DAY :
      tm.tm_hour = 0;
      tm.tm_min = 0;
      tm.tm_sec = 0;
      break;
    case GRN_TIME_CLASSIFY_UNIT_WEEK :
      if ((tm.tm_mday - tm.tm_wday) >= 0) {
        tm.tm_mday -= tm.tm_wday;
      } else {
        int n_underflowed_mday = -(tm.tm_mday - tm.tm_wday);
        int mday;
        int max_mday = 31;

        if (tm.tm_mon == 0) {
          tm.tm_year--;
          tm.tm_mon = 11;
        } else {
          tm.tm_mon--;
        }

        for (mday = max_mday; mday > n_underflowed_mday; mday--) {
          int64_t unused;
          tm.tm_mday = mday;
          if (grn_time_from_tm(ctx, &unused, &tm)) {
            break;
          }
        }
        tm.tm_mday -= n_underflowed_mday;
      }
      tm.tm_hour = 0;
      tm.tm_min = 0;
      tm.tm_sec = 0;
      break;
    case GRN_TIME_CLASSIFY_UNIT_MONTH :
      tm.tm_mon = (tm.tm_mon / interval_raw) * interval_raw;
      tm.tm_mday = 1;
      tm.tm_hour = 0;
      tm.tm_min = 0;
      tm.tm_sec = 0;
      break;
    case GRN_TIME_CLASSIFY_UNIT_YEAR :
      tm.tm_year = (((1900 + tm.tm_year) / interval_raw) * interval_raw) - 1900;
      tm.tm_mon = 0;
      tm.tm_mday = 1;
      tm.tm_hour = 0;
      tm.tm_min = 0;
      tm.tm_sec = 0;
      break;
    }

    if (!grn_time_from_tm(ctx, &classed_time_raw, &tm)) {
      return NULL;
    }

    classed_time = grn_plugin_proc_alloc(ctx,
                                         user_data,
                                         time->header.domain,
                                         0);
    if (!classed_time) {
      return NULL;
    }
    GRN_TIME_SET(ctx, classed_time, classed_time_raw);

    return classed_time;
  }
}

static grn_obj *
func_time_classify_second(grn_ctx *ctx, int n_args, grn_obj **args,
                          grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_second",
                                GRN_TIME_CLASSIFY_UNIT_SECOND);
}

static grn_obj *
func_time_classify_minute(grn_ctx *ctx, int n_args, grn_obj **args,
                          grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_minute",
                                GRN_TIME_CLASSIFY_UNIT_MINUTE);
}

static grn_obj *
func_time_classify_hour(grn_ctx *ctx, int n_args, grn_obj **args,
                        grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_hour",
                                GRN_TIME_CLASSIFY_UNIT_HOUR);
}

static grn_obj *
func_time_classify_day(grn_ctx *ctx, int n_args, grn_obj **args,
                       grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_day",
                                GRN_TIME_CLASSIFY_UNIT_DAY);
}

static grn_obj *
func_time_classify_week(grn_ctx *ctx, int n_args, grn_obj **args,
                        grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_week",
                                GRN_TIME_CLASSIFY_UNIT_WEEK);
}

static grn_obj *
func_time_classify_month(grn_ctx *ctx, int n_args, grn_obj **args,
                         grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_month",
                                GRN_TIME_CLASSIFY_UNIT_MONTH);
}

static grn_obj *
func_time_classify_year(grn_ctx *ctx, int n_args, grn_obj **args,
                        grn_user_data *user_data)
{
  return func_time_classify_raw(ctx,
                                n_args,
                                args,
                                user_data,
                                "time_classify_year",
                                GRN_TIME_CLASSIFY_UNIT_YEAR);
}

static grn_obj *
func_time_format(grn_ctx *ctx, int n_args, grn_obj **args,
                 grn_user_data *user_data)
{
  grn_obj *time;
  grn_obj *format;

  if (n_args != 2) {
    GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                     "time_format(): "
                     "wrong number of arguments (%d for 2)",
                     n_args);
    return NULL;
  }

  time = args[0];
  format = args[1];

  if (!(time->header.type == GRN_BULK &&
        time->header.domain == GRN_DB_TIME)) {
    grn_obj inspected;

    GRN_TEXT_INIT(&inspected, 0);
    grn_inspect(ctx, &inspected, time);
    GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                     "time_format(): "
                     "the first argument must be a time: "
                     "<%.*s>",
                     (int)GRN_TEXT_LEN(&inspected),
                     GRN_TEXT_VALUE(&inspected));
    GRN_OBJ_FIN(ctx, &inspected);
    return NULL;
  }

  if (!grn_obj_is_text_family_bulk(ctx, format)) {
    grn_obj inspected;

    GRN_TEXT_INIT(&inspected, 0);
    grn_inspect(ctx, &inspected, format);
    GRN_PLUGIN_ERROR(ctx, GRN_INVALID_ARGUMENT,
                     "time_format(): "
                     "the second argument must be a string: "
                     "<%.*s>",
                     (int)GRN_TEXT_LEN(&inspected),
                     GRN_TEXT_VALUE(&inspected));
    GRN_OBJ_FIN(ctx, &inspected);
    return NULL;
  }

  {
    int64_t time_raw;
    struct tm tm;
    grn_obj nul_terminated_format;
    grn_obj *formatted_time;
    char formatted_time_buffer[4096];
    size_t formatted_time_size;

    time_raw = GRN_TIME_VALUE(time);
    if (!grn_time_to_tm(ctx, time_raw, &tm)) {
      return NULL;
    }

    GRN_TEXT_INIT(&nul_terminated_format, 0);
    GRN_TEXT_SET(ctx,
                 &nul_terminated_format,
                 GRN_TEXT_VALUE(format),
                 GRN_TEXT_LEN(format));
    GRN_TEXT_PUTC(ctx, &nul_terminated_format, '\0');

    formatted_time_size = strftime(formatted_time_buffer,
                                   sizeof(formatted_time_buffer),
                                   GRN_TEXT_VALUE(&nul_terminated_format),
                                   &tm);
    GRN_OBJ_FIN(ctx, &nul_terminated_format);

    formatted_time = grn_plugin_proc_alloc(ctx, user_data, GRN_DB_TEXT, 0);
    GRN_TEXT_SET(ctx,
                 formatted_time,
                 formatted_time_buffer,
                 formatted_time_size);

    return formatted_time;
  }
}

grn_rc
GRN_PLUGIN_INIT(grn_ctx *ctx)
{
  return ctx->rc;
}

grn_rc
GRN_PLUGIN_REGISTER(grn_ctx *ctx)
{
  grn_rc rc = GRN_SUCCESS;

  grn_proc_create(ctx,
                  "time_classify_second", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_second,
                  NULL, NULL, 0, NULL);
  grn_proc_create(ctx,
                  "time_classify_minute", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_minute,
                  NULL, NULL, 0, NULL);
  grn_proc_create(ctx,
                  "time_classify_hour", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_hour,
                  NULL, NULL, 0, NULL);
  grn_proc_create(ctx,
                  "time_classify_day", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_day,
                  NULL, NULL, 0, NULL);
  grn_proc_create(ctx,
                  "time_classify_week", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_week,
                  NULL, NULL, 0, NULL);
  grn_proc_create(ctx,
                  "time_classify_month", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_month,
                  NULL, NULL, 0, NULL);
  grn_proc_create(ctx,
                  "time_classify_year", -1,
                  GRN_PROC_FUNCTION,
                  func_time_classify_year,
                  NULL, NULL, 0, NULL);

  grn_proc_create(ctx,
                  "time_format", -1,
                  GRN_PROC_FUNCTION,
                  func_time_format,
                  NULL, NULL, 0, NULL);

  return rc;
}

grn_rc
GRN_PLUGIN_FIN(grn_ctx *ctx)
{
  return GRN_SUCCESS;
}
