/*
  Copyright (C) 2009-2018  Brazil
  Copyright (C) 2018-2022  Sutou Kouhei <kou@clear-code.com>

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

#include "../grn_proc.h"

#include "../grn_ctx.h"
#include "../grn_str.h"
#include "../grn_db.h"

#include <groonga/plugin.h>

static grn_table_flags
command_table_create_parse_flags(grn_ctx *ctx,
                                 const char *nptr,
                                 const char *end)
{
  grn_table_flags flags = 0;
  while (nptr < end) {
    size_t name_size;

    if (*nptr == '|' || *nptr == ' ') {
      nptr += 1;
      continue;
    }

#define CHECK_FLAG(name)                                                \
    name_size = strlen(#name);                                          \
    if ((size_t)(end - nptr) >= name_size &&                            \
        memcmp(nptr, #name, name_size) == 0) {                          \
      flags |= GRN_OBJ_ ## name;                                        \
      nptr += name_size;                                                \
      continue;                                                         \
    }

    CHECK_FLAG(TABLE_HASH_KEY);
    CHECK_FLAG(TABLE_PAT_KEY);
    CHECK_FLAG(TABLE_DAT_KEY);
    CHECK_FLAG(TABLE_NO_KEY);
    CHECK_FLAG(KEY_NORMALIZE);
    CHECK_FLAG(KEY_WITH_SIS);
    CHECK_FLAG(KEY_LARGE);

#undef CHECK_FLAG

    GRN_PLUGIN_ERROR(ctx,
                     GRN_INVALID_ARGUMENT,
                     "[table][create][flags] unknown flag: <%.*s>",
                     (int)(end - nptr), nptr);
    return 0;
  }
  return flags;
}

bool
grn_proc_table_set_token_filters(grn_ctx *ctx,
                                 grn_obj *table,
                                 grn_raw_string *token_filters_raw)
{
  grn_obj token_filters;

  if (token_filters_raw->length == 0) {
    return true;
  }

  GRN_TEXT_INIT(&token_filters, GRN_OBJ_DO_SHALLOW_COPY);
  GRN_TEXT_SET(ctx,
               &token_filters,
               token_filters_raw->value,
               token_filters_raw->length);
  grn_obj_set_info(ctx, table, GRN_INFO_TOKEN_FILTERS, &token_filters);
  GRN_OBJ_FIN(ctx, &token_filters);

  return false;
}

static grn_obj *
command_table_create(grn_ctx *ctx,
                     int nargs,
                     grn_obj **args,
                     grn_user_data *user_data)
{
  grn_raw_string name_raw;
  grn_raw_string flags_raw;
  grn_raw_string key_type_raw;
  grn_raw_string value_type_raw;
  grn_obj *default_tokenizer_raw;
  grn_obj *normalizer_raw;
  grn_raw_string token_filters_raw;
  grn_obj *path_raw;
  grn_obj *normalizers_raw;
  grn_obj *key_type = NULL;
  grn_obj *value_type = NULL;
  grn_obj *table = NULL;
  const char *rest;
  grn_table_flags flags;

#define GET_VALUE(name)                                         \
  name ## _raw.value =                                          \
    grn_plugin_proc_get_var_string(ctx,                         \
                                   user_data,                   \
                                   #name,                       \
                                   strlen(#name),               \
                                   &(name ## _raw.length))

  GET_VALUE(name);
  GET_VALUE(flags);
  GET_VALUE(key_type);
  GET_VALUE(value_type);
  default_tokenizer_raw = grn_plugin_proc_get_var(ctx, user_data,
                                                  "default_tokenizer", -1);
  normalizer_raw = grn_plugin_proc_get_var(ctx, user_data, "normalizer", -1);
  GET_VALUE(token_filters);
  path_raw = grn_plugin_proc_get_var(ctx, user_data, "path", -1);
  normalizers_raw = grn_plugin_proc_get_var(ctx, user_data, "normalizers", -1);

#undef GET_VALUE

  flags = (grn_table_flags)grn_atoi(flags_raw.value,
                                    flags_raw.value + flags_raw.length,
                                    &rest);
  if (flags_raw.value == rest) {
    flags = command_table_create_parse_flags(ctx,
                                             flags_raw.value,
                                             flags_raw.value + flags_raw.length);
    if (ctx->rc) { goto exit; }
  }

  if (name_raw.length == 0) {
    GRN_PLUGIN_ERROR(ctx,
                     GRN_INVALID_ARGUMENT,
                     "[table][create] should not create anonymous table");
    goto exit;
  }

  {
    if (key_type_raw.length > 0) {
      key_type = grn_ctx_get(ctx,
                             key_type_raw.value,
                             (int)(key_type_raw.length));
      if (!key_type) {
        GRN_PLUGIN_ERROR(ctx,
                         GRN_INVALID_ARGUMENT,
                         "[table][create] "
                         "key type doesn't exist: <%.*s> (%.*s)",
                         (int)name_raw.length,
                         name_raw.value,
                         (int)key_type_raw.length,
                         key_type_raw.value);
        goto exit;
      }
    }

    if (value_type_raw.length > 0) {
      value_type = grn_ctx_get(ctx,
                               value_type_raw.value,
                               (int)(value_type_raw.length));
      if (!value_type) {
        GRN_PLUGIN_ERROR(ctx,
                         GRN_INVALID_ARGUMENT,
                         "[table][create] "
                         "value type doesn't exist: <%.*s> (%.*s)",
                         (int)name_raw.length,
                         name_raw.value,
                         (int)value_type_raw.length,
                         value_type_raw.value);
        goto exit;
      }
    }

    const char *path = NULL;
    if (GRN_TEXT_LEN(path_raw) > 0) {
      GRN_TEXT_PUTC(ctx, path_raw, '\0');
      path = GRN_TEXT_VALUE(path_raw);
    }
    flags |= GRN_OBJ_PERSISTENT;
    table = grn_table_create(ctx,
                             name_raw.value,
                             (unsigned int)(name_raw.length),
                             path,
                             flags,
                             key_type,
                             value_type);
    if (!table) {
      goto exit;
    }

    if (GRN_TEXT_LEN(default_tokenizer_raw) > 0) {
      grn_obj_set_info(ctx, table,
                       GRN_INFO_DEFAULT_TOKENIZER,
                       default_tokenizer_raw);
      if (ctx->rc != GRN_SUCCESS) {
        GRN_PLUGIN_ERROR(ctx,
                         GRN_INVALID_ARGUMENT,
                         "[table][create][%.*s] "
                         "failed to set default tokenizer: <%.*s>: %s",
                         (int)name_raw.length,
                         name_raw.value,
                         (int)GRN_TEXT_LEN(default_tokenizer_raw),
                         GRN_TEXT_VALUE(default_tokenizer_raw),
                         ctx->errbuf);
        goto exit;
      }
    }

    if (GRN_TEXT_LEN(normalizers_raw) == 0 &&
        GRN_TEXT_LEN(normalizer_raw) > 0) {
      normalizers_raw = normalizer_raw;
    }
    if (GRN_TEXT_LEN(normalizers_raw) > 0) {
      grn_obj_set_info(ctx, table, GRN_INFO_NORMALIZERS, normalizers_raw);
      if (ctx->rc != GRN_SUCCESS) {
        GRN_PLUGIN_ERROR(ctx,
                         GRN_INVALID_ARGUMENT,
                         "[table][create][%.*s] "
                         "failed to set normalizers: <%.*s>: %s",
                         (int)name_raw.length,
                         name_raw.value,
                         (int)GRN_TEXT_LEN(normalizers_raw),
                         GRN_TEXT_VALUE(normalizers_raw),
                         ctx->errbuf);
        goto exit;
      }
    }

    if (!grn_proc_table_set_token_filters(ctx, table, &token_filters_raw)) {
      goto exit;
    }
  }

exit :
  {
    bool success = (ctx->rc == GRN_SUCCESS);
    if (key_type) {
      grn_obj_unref(ctx, key_type);
    }
    if (value_type) {
      grn_obj_unref(ctx, value_type);
    }
    if (success) {
      if (table) {
        grn_obj_unlink(ctx, table);
      }
    } else {
      if (table) {
        grn_obj_remove(ctx, table);
      }
    }
    grn_ctx_output_bool(ctx, success);
  }
  return NULL;
}

void
grn_proc_init_table_create(grn_ctx *ctx)
{
  grn_expr_var vars[9];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "name", -1);
  grn_plugin_expr_var_init(ctx, &(vars[1]), "flags", -1);
  grn_plugin_expr_var_init(ctx, &(vars[2]), "key_type", -1);
  grn_plugin_expr_var_init(ctx, &(vars[3]), "value_type", -1);
  grn_plugin_expr_var_init(ctx, &(vars[4]), "default_tokenizer", -1);
  grn_plugin_expr_var_init(ctx, &(vars[5]), "normalizer", -1);
  grn_plugin_expr_var_init(ctx, &(vars[6]), "token_filters", -1);
  grn_plugin_expr_var_init(ctx, &(vars[7]), "path", -1);
  grn_plugin_expr_var_init(ctx, &(vars[8]), "normalizers", -1);
  grn_plugin_command_create(ctx,
                            "table_create", -1,
                            command_table_create,
                            9,
                            vars);
}

static void
output_table_info(grn_ctx *ctx, grn_obj *table)
{
  grn_obj buffer;
  grn_id id;
  const char *path;
  grn_table_flags flags;
  grn_obj *default_tokenizer;
  grn_obj *normalizer;
  grn_obj *token_filters;

  GRN_TEXT_INIT(&buffer, 0);

  grn_ctx_output_array_open(ctx, "TABLE", 8);

  id = grn_obj_id(ctx, table);
  grn_ctx_output_int64(ctx, id);
  grn_proc_output_object_id_name(ctx, id);
  path = grn_obj_path(ctx, table);
  grn_ctx_output_cstr(ctx, path);

  grn_table_get_info(ctx, table,
                     &flags,
                     NULL,
                     &default_tokenizer,
                     &normalizer,
                     &token_filters);

  grn_dump_table_create_flags(ctx, flags, &buffer);
  grn_ctx_output_obj(ctx, &buffer, NULL);

  grn_proc_output_object_id_name(ctx, table->header.domain);
  grn_proc_output_object_id_name(ctx, grn_obj_get_range(ctx, table));

  if (default_tokenizer) {
    GRN_BULK_REWIND(&buffer);
    grn_table_get_default_tokenizer_string(ctx, table, &buffer);
    grn_ctx_output_obj(ctx, &buffer, NULL);
  } else {
    grn_ctx_output_null(ctx);
  }

  if (normalizer) {
    GRN_BULK_REWIND(&buffer);
    grn_table_get_normalizers_string(ctx, table, &buffer);
    grn_ctx_output_obj(ctx, &buffer, NULL);
  } else {
    grn_ctx_output_null(ctx);
  }

  grn_ctx_output_array_close(ctx);

  GRN_OBJ_FIN(ctx, &buffer);
}

static grn_obj *
command_table_list(grn_ctx *ctx, int nargs, grn_obj **args,
                   grn_user_data *user_data)
{
  grn_obj *db;
  grn_obj tables;
  int n_elements_for_header = 1;

  db = grn_ctx_db(ctx);

  {
    grn_table_cursor *cursor;
    grn_id id;
    grn_obj *prefix;
    const void *min = NULL;
    unsigned int min_size = 0;
    int flags = 0;

    prefix = grn_plugin_proc_get_var(ctx, user_data, "prefix", -1);
    if (GRN_TEXT_LEN(prefix) > 0) {
      min = GRN_TEXT_VALUE(prefix);
      min_size = (unsigned int)GRN_TEXT_LEN(prefix);
      flags |= GRN_CURSOR_PREFIX;
    }
    cursor = grn_table_cursor_open(ctx, db,
                                   min, min_size,
                                   NULL, 0,
                                   0, -1, flags);
    if (!cursor) {
      return NULL;
    }

    GRN_PTR_INIT(&tables, GRN_OBJ_VECTOR, GRN_ID_NIL);
    while ((id = grn_table_cursor_next(ctx, cursor)) != GRN_ID_NIL) {
      grn_obj *object;
      const char *name;
      void *key;
      int i, key_size;
      bool have_period = false;

      key_size = grn_table_cursor_get_key(ctx, cursor, &key);
      name = key;
      for (i = 0; i < key_size; i++) {
        if (name[i] == '.') {
          have_period = true;
          break;
        }
      }
      if (have_period) {
        continue;
      }

      object = grn_ctx_at(ctx, id);
      if (object) {
        if (grn_obj_is_table(ctx, object)) {
          GRN_PTR_PUT(ctx, &tables, object);
        } else {
          grn_obj_unlink(ctx, object);
        }
      } else {
        if (ctx->rc != GRN_SUCCESS) {
          ERRCLR(ctx);
        }
      }
    }
    grn_table_cursor_close(ctx, cursor);
  }
  size_t n_tables = GRN_BULK_VSIZE(&tables) / sizeof(grn_obj *);
  int n_top_level_elements = n_elements_for_header + (int)n_tables;
  grn_ctx_output_array_open(ctx, "TABLE_LIST", n_top_level_elements);

  grn_ctx_output_array_open(ctx, "HEADER", 8);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "id");
  grn_ctx_output_cstr(ctx, "UInt32");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "name");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "path");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "flags");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "domain");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "range");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "default_tokenizer");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_open(ctx, "PROPERTY", 2);
  grn_ctx_output_cstr(ctx, "normalizer");
  grn_ctx_output_cstr(ctx, "ShortText");
  grn_ctx_output_array_close(ctx);
  grn_ctx_output_array_close(ctx);

  size_t i;
  for (i = 0; i < n_tables; i++) {
    grn_obj *table = GRN_PTR_VALUE_AT(&tables, i);
    output_table_info(ctx, table);
    grn_obj_unlink(ctx, table);
  }
  GRN_OBJ_FIN(ctx, &tables);

  grn_ctx_output_array_close(ctx);

  return NULL;
}

void
grn_proc_init_table_list(grn_ctx *ctx)
{
  grn_expr_var vars[1];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "prefix", -1);
  grn_plugin_command_create(ctx,
                            "table_list", -1,
                            command_table_list,
                            1,
                            vars);
}

static grn_obj *
command_table_remove(grn_ctx *ctx,
                     int nargs,
                     grn_obj **args,
                     grn_user_data *user_data)
{
  grn_obj *name;
  grn_obj *table;
  uint32_t flags = 0;

  name = grn_plugin_proc_get_var(ctx, user_data, "name", -1);
  if (grn_plugin_proc_get_var_bool(ctx, user_data, "dependent", -1, false)) {
    flags |= GRN_OBJ_REMOVE_DEPENDENT;
  }
  if (grn_plugin_proc_get_var_bool(ctx, user_data, "ensure", -1, false)) {
    flags |= GRN_OBJ_REMOVE_ENSURE;
  }
  table = grn_ctx_get(ctx,
                      GRN_TEXT_VALUE(name),
                      (int)GRN_TEXT_LEN(name));
  if (!table) {
    GRN_PLUGIN_ERROR(ctx,
                     GRN_INVALID_ARGUMENT,
                     "[table][remove] table isn't found: <%.*s>",
                     (int)GRN_TEXT_LEN(name),
                     GRN_TEXT_VALUE(name));
    grn_ctx_output_bool(ctx, false);
    return NULL;
  }

  if (!grn_obj_is_table(ctx, table)) {
    const char *type_name;
    type_name = grn_obj_type_to_string(table->header.type);
    grn_obj_unlink(ctx, table);
    GRN_PLUGIN_ERROR(ctx,
                     GRN_INVALID_ARGUMENT,
                     "[table][remove] not table: <%.*s>: <%s>",
                     (int)GRN_TEXT_LEN(name),
                     GRN_TEXT_VALUE(name),
                     type_name);
    grn_ctx_output_bool(ctx, false);
    return NULL;
  }

  grn_obj_remove_flags(ctx, table, flags);
  grn_ctx_output_bool(ctx, ctx->rc == GRN_SUCCESS);
  return NULL;
}

void
grn_proc_init_table_remove(grn_ctx *ctx)
{
#define N_VARS 3
  grn_expr_var vars[N_VARS];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "name", -1);
  grn_plugin_expr_var_init(ctx, &(vars[1]), "dependent", -1);
  grn_plugin_expr_var_init(ctx, &(vars[2]), "ensure", -1);
  grn_plugin_command_create(ctx,
                            "table_remove",
                            -1,
                            command_table_remove,
                            N_VARS,
                            vars);
#undef N_VARS
}

static grn_obj *
command_table_rename(grn_ctx *ctx,
                     int nargs,
                     grn_obj **args,
                     grn_user_data *user_data)
{
  grn_rc rc = GRN_SUCCESS;
  grn_obj *name;
  grn_obj *new_name;
  grn_obj *table = NULL;

  name = grn_plugin_proc_get_var(ctx, user_data, "name", -1);
  new_name = grn_plugin_proc_get_var(ctx, user_data, "new_name", -1);
  if (GRN_TEXT_LEN(name) == 0) {
    rc = GRN_INVALID_ARGUMENT;
    GRN_PLUGIN_ERROR(ctx, rc, "[table][rename] table name isn't specified");
    goto exit;
  }
  table = grn_ctx_get(ctx, GRN_TEXT_VALUE(name), (int)GRN_TEXT_LEN(name));
  if (!table) {
    rc = GRN_INVALID_ARGUMENT;
    GRN_PLUGIN_ERROR(ctx,
                     rc,
                     "[table][rename] table isn't found: <%.*s>",
                     (int)GRN_TEXT_LEN(name),
                     GRN_TEXT_VALUE(name));
    goto exit;
  }
  if (GRN_TEXT_LEN(new_name) == 0) {
    rc = GRN_INVALID_ARGUMENT;
    GRN_PLUGIN_ERROR(ctx,
                     rc,
                     "[table][rename] new table name isn't specified: <%.*s>",
                     (int)GRN_TEXT_LEN(name),
                     GRN_TEXT_VALUE(name));
    goto exit;
  }
  rc = grn_table_rename(ctx, table,
                        GRN_TEXT_VALUE(new_name),
                        (unsigned int)GRN_TEXT_LEN(new_name));
  if (rc != GRN_SUCCESS && ctx->rc == GRN_SUCCESS) {
    GRN_PLUGIN_ERROR(ctx,
                     rc,
                     "[table][rename] failed to rename: <%.*s> -> <%.*s>",
                     (int)GRN_TEXT_LEN(name),
                     GRN_TEXT_VALUE(name),
                     (int)GRN_TEXT_LEN(new_name),
                     GRN_TEXT_VALUE(new_name));
  }
exit :
  grn_ctx_output_bool(ctx, !rc);
  if (table) { grn_obj_unlink(ctx, table); }
  return NULL;
}

void
grn_proc_init_table_rename(grn_ctx *ctx)
{
  grn_expr_var vars[2];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "name", -1);
  grn_plugin_expr_var_init(ctx, &(vars[1]), "new_name", -1);
  grn_plugin_command_create(ctx,
                            "table_rename", -1,
                            command_table_rename,
                            2,
                            vars);
}

static grn_obj *
command_table_copy(grn_ctx *ctx,
                   int nargs,
                   grn_obj **args,
                   grn_user_data *user_data)
{
  grn_obj *from_table = NULL;
  grn_obj *to_table = NULL;
  grn_obj *from_name;
  grn_obj *to_name;

  from_name = grn_plugin_proc_get_var(ctx, user_data, "from_name", -1);
  to_name   = grn_plugin_proc_get_var(ctx, user_data, "to_name", -1);

  from_table = grn_proc_get_value_object(ctx, from_name, "[table][copy][from]");
  if (!from_table) {
    goto exit;
  }
  to_table = grn_proc_get_value_object(ctx, to_name, "[table][copy][to]");
  if (!to_table) {
    goto exit;
  }

  grn_table_copy(ctx, from_table, to_table);

exit :
  grn_ctx_output_bool(ctx, ctx->rc == GRN_SUCCESS);

  if (to_table) {
    grn_obj_unlink(ctx, to_table);
  }
  if (from_table) {
    grn_obj_unlink(ctx, from_table);
  }

  return NULL;
}

void
grn_proc_init_table_copy(grn_ctx *ctx)
{
  grn_expr_var vars[2];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "from_name", -1);
  grn_plugin_expr_var_init(ctx, &(vars[1]), "to_name", -1);
  grn_plugin_command_create(ctx,
                            "table_copy", -1,
                            command_table_copy,
                            2,
                            vars);
}

static grn_obj *
command_table_create_similar(grn_ctx *ctx,
                             int nargs,
                             grn_obj **args,
                             grn_user_data *user_data)
{
  grn_obj *name;
  grn_obj *base_table_name;
  grn_obj *base_table = NULL;
  grn_obj *table = NULL;

  name = grn_plugin_proc_get_var(ctx, user_data, "name", -1);
  base_table_name = grn_plugin_proc_get_var(ctx, user_data, "base_table", -1);

  base_table = grn_proc_get_value_object(ctx,
                                         base_table_name,
                                         "[table][create][similar][base_table]");
  if (!base_table) {
    goto exit;
  }

  table = grn_table_create_similar(ctx,
                                   GRN_TEXT_VALUE(name),
                                   (uint32_t)GRN_TEXT_LEN(name),
                                   NULL,
                                   base_table);

exit :
  grn_ctx_output_bool(ctx, ctx->rc == GRN_SUCCESS);

  if (base_table) {
    grn_obj_unref(ctx, base_table);
  }
  if (table) {
    grn_obj_unref(ctx, table);
  }

  return NULL;
}

void
grn_proc_init_table_create_similar(grn_ctx *ctx)
{
  grn_expr_var vars[2];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "name", -1);
  grn_plugin_expr_var_init(ctx, &(vars[1]), "base_table", -1);
  grn_plugin_command_create(ctx,
                            "table_create_similar", -1,
                            command_table_create_similar,
                            2,
                            vars);
}
