/*
  Copyright(C) 2009-2018  Brazil
  Copyright(C) 2018-2020  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 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
*/

#include "../grn_proc.h"
#include "../grn_ctx_impl.h"
#include "../grn_db.h"
#include "../grn_str.h"

#include <groonga/plugin.h>

static const size_t DUMP_FLUSH_THRESHOLD_SIZE = 256 * 1024;

typedef struct {
  grn_obj *output;
  bool is_close_opened_object_mode;
  bool have_reference_column;
  bool have_index_column;
  bool is_sort_hash_table;
  bool is_dump_paths;
  grn_obj column_name_buffer;
} grn_dumper;

static void
dumper_collect_statistics_table(grn_ctx *ctx,
                                grn_dumper *dumper,
                                grn_obj *table)
{
  grn_hash *columns;

  columns = grn_hash_create(ctx, NULL, sizeof(grn_id), 0,
                            GRN_OBJ_TABLE_HASH_KEY|GRN_HASH_TINY);
  if (!columns) {
    return;
  }

  grn_table_columns(ctx, table, NULL, 0, (grn_obj *)columns);
  GRN_HASH_EACH_BEGIN(ctx, columns, cursor, id) {
    void *key;
    grn_id column_id;
    grn_obj *column;

    grn_hash_cursor_get_key(ctx, cursor, &key);
    column_id = *((grn_id *)key);

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    column = grn_ctx_at(ctx, column_id);
    if (!column) {
      GRN_PLUGIN_CLEAR_ERROR(ctx);
      goto next_loop;
    }

    if (grn_obj_is_index_column(ctx, column)) {
      dumper->have_index_column = GRN_TRUE;
    } else if (grn_obj_is_reference_column(ctx, column)) {
      dumper->have_reference_column = GRN_TRUE;
    }

  next_loop :
    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  } GRN_HASH_EACH_END(ctx, cursor);
  grn_hash_close(ctx, columns);
}

static void
dumper_collect_statistics(grn_ctx *ctx, grn_dumper *dumper)
{
  GRN_DB_EACH_BEGIN_BY_ID(ctx, cursor, id) {
    void *name;
    int name_size;
    grn_obj *object;

    if (grn_id_is_builtin(ctx, id)) {
      continue;
    }

    name_size = grn_table_cursor_get_key(ctx, cursor, &name);
    if (grn_obj_name_is_column(ctx, name, name_size)) {
      continue;
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    object = grn_ctx_at(ctx, id);
    if (!object) {
      /* XXX: this clause is executed when MeCab tokenizer is enabled in
         database but the groonga isn't supported MeCab.
         We should return error mesage about it and error exit status
         but it's too difficult for this architecture. :< */
      GRN_PLUGIN_CLEAR_ERROR(ctx);
      goto next_loop;
    }

    if (!grn_obj_is_table(ctx, object)) {
      goto next_loop;
    }

    dumper_collect_statistics_table(ctx, dumper, object);

next_loop :
    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  } GRN_DB_EACH_END(ctx, cursor);
}

static void
dump_value_raw(grn_ctx *ctx, grn_obj *output, const char *value, int value_len)
{
  grn_obj escaped_value;
  GRN_TEXT_INIT(&escaped_value, 0);
  grn_text_esc(ctx, &escaped_value, value, value_len);
  /* is no character escaped? */
  /* TODO false positive with spaces inside values */
  if (GRN_TEXT_LEN(&escaped_value) == value_len + 2) {
    GRN_TEXT_PUT(ctx, output, value, value_len);
  } else {
    GRN_TEXT_PUT(ctx, output,
                 GRN_TEXT_VALUE(&escaped_value), GRN_TEXT_LEN(&escaped_value));
  }
  grn_obj_close(ctx, &escaped_value);
}

static void
dump_value(grn_ctx *ctx, grn_dumper *dumper, const char *value, int value_len)
{
  dump_value_raw(ctx, dumper->output, value, value_len);
}

static void
dump_configs(grn_ctx *ctx, grn_dumper *dumper)
{
  grn_obj *config_cursor;

  config_cursor = grn_config_cursor_open(ctx);
  if (!config_cursor)
    return;

  while (grn_config_cursor_next(ctx, config_cursor)) {
    const char *key;
    uint32_t key_size;
    const char *value;
    uint32_t value_size;

    key_size = grn_config_cursor_get_key(ctx, config_cursor, &key);
    value_size = grn_config_cursor_get_value(ctx, config_cursor, &value);

    GRN_TEXT_PUTS(ctx, dumper->output, "config_set ");
    dump_value(ctx, dumper, key, key_size);
    GRN_TEXT_PUTS(ctx, dumper->output, " ");
    dump_value(ctx, dumper, value, value_size);
    GRN_TEXT_PUTC(ctx, dumper->output, '\n');
  }
  grn_obj_close(ctx, config_cursor);
}

static void
dump_plugins(grn_ctx *ctx, grn_dumper *dumper)
{
  grn_obj plugin_names;
  unsigned int i, n;

  GRN_TEXT_INIT(&plugin_names, GRN_OBJ_VECTOR);

  grn_plugin_get_names(ctx, &plugin_names);

  n = grn_vector_size(ctx, &plugin_names);
  if (n == 0) {
    GRN_OBJ_FIN(ctx, &plugin_names);
    return;
  }

  if (GRN_TEXT_LEN(dumper->output) > 0) {
    GRN_TEXT_PUTC(ctx, dumper->output, '\n');
    grn_ctx_output_flush(ctx, 0);
  }
  for (i = 0; i < n; i++) {
    const char *name;
    unsigned int name_size;

    name_size = grn_vector_get_element(ctx, &plugin_names, i, &name, NULL, NULL);
    grn_text_printf(ctx, dumper->output, "plugin_register %.*s\n",
                    (int)name_size, name);
  }

  GRN_OBJ_FIN(ctx, &plugin_names);
}

static void
dump_obj_name_raw(grn_ctx *ctx, grn_obj *output, grn_obj *obj)
{
  char name[GRN_TABLE_MAX_KEY_SIZE];
  int name_len;
  name_len = grn_obj_name(ctx, obj, name, GRN_TABLE_MAX_KEY_SIZE);
  dump_value_raw(ctx, output, name, name_len);
}

static void
dump_obj_name(grn_ctx *ctx, grn_dumper *dumper, grn_obj *obj)
{
  dump_obj_name_raw(ctx, dumper->output, obj);
}

static void
dump_column_name(grn_ctx *ctx, grn_dumper *dumper, grn_obj *column)
{
  char name[GRN_TABLE_MAX_KEY_SIZE];
  int name_len;
  name_len = grn_column_name(ctx, column, name, GRN_TABLE_MAX_KEY_SIZE);
  dump_value(ctx, dumper, name, name_len);
}

static void
dump_column_sources(grn_ctx *ctx, grn_dumper *dumper, grn_obj *column)
{
  grn_obj sources;
  grn_id *source_ids;
  int i, n;

  GRN_OBJ_INIT(&sources, GRN_BULK, 0, GRN_ID_NIL);
  grn_obj_get_info(ctx, column, GRN_INFO_SOURCE, &sources);

  n = GRN_BULK_VSIZE(&sources) / sizeof(grn_id);
  source_ids = (grn_id *)GRN_BULK_HEAD(&sources);
  if (n > 0) {
    GRN_TEXT_PUTC(ctx, dumper->output, ' ');
  }
  for (i = 0; i < n; i++) {
    grn_id source_id;
    grn_obj *source;

    source_id = *source_ids;
    source_ids++;

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    source = grn_ctx_at(ctx, source_id);
    if (!source) {
      goto next_loop;
    }

    if (i) { GRN_TEXT_PUTC(ctx, dumper->output, ','); }
    switch (source->header.type) {
    case GRN_TABLE_PAT_KEY:
    case GRN_TABLE_DAT_KEY:
    case GRN_TABLE_HASH_KEY:
      GRN_TEXT_PUT(ctx,
                   dumper->output,
                   GRN_COLUMN_NAME_KEY,
                   GRN_COLUMN_NAME_KEY_LEN);
      break;
    default:
      dump_column_name(ctx, dumper, source);
      break;
    }

  next_loop :
    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  }
  grn_obj_close(ctx, &sources);
}

static void
dump_string(grn_ctx *ctx,
            grn_dumper *dumper,
            grn_obj *string)
{
  const char *value = GRN_TEXT_VALUE(string);
  size_t length = GRN_TEXT_LEN(string);

  if (grn_proc_text_include_special_character(ctx, value, length)) {
    grn_text_otoj(ctx, dumper->output, string, NULL);
  } else {
    GRN_TEXT_PUT(ctx, dumper->output, value, length);
  }
}

static void
dump_column(grn_ctx *ctx, grn_dumper *dumper, grn_obj *table, grn_obj *column)
{
  grn_id type_id;
  grn_obj *type;
  grn_column_flags flags;
  grn_column_flags default_flags = GRN_OBJ_PERSISTENT;

  type_id = grn_obj_get_range(ctx, column);
  if (dumper->is_close_opened_object_mode) {
    grn_ctx_push_temporary_open_space(ctx);
  }
  type = grn_ctx_at(ctx, type_id);
  if (!type) {
    /* ERR(GRN_RANGE_ERROR, "couldn't get column's type object"); */
    goto exit;
  }

  GRN_TEXT_PUTS(ctx, dumper->output, "column_create ");
  dump_obj_name(ctx, dumper, table);
  GRN_TEXT_PUTC(ctx, dumper->output, ' ');
  dump_column_name(ctx, dumper, column);
  GRN_TEXT_PUTC(ctx, dumper->output, ' ');
  if (type->header.type == GRN_TYPE) {
    default_flags |= type->header.flags;
  }
  flags = grn_column_get_flags(ctx, column);
  grn_dump_column_create_flags(ctx,
                               flags & ~default_flags,
                               dumper->output);
  GRN_TEXT_PUTC(ctx, dumper->output, ' ');
  dump_obj_name(ctx, dumper, type);
  switch (column->header.type) {
  case GRN_COLUMN_VAR_SIZE :
  case GRN_COLUMN_INDEX :
    dump_column_sources(ctx, dumper, column);
    break;
  }
  if ((flags & GRN_OBJ_CUSTOM_NAME) && dumper->is_dump_paths) {
    grn_obj sub_output;
    GRN_TEXT_PUTS(ctx, dumper->output, " --path ");
    GRN_TEXT_INIT(&sub_output, GRN_OBJ_DO_SHALLOW_COPY);
    const char *path = grn_obj_path(ctx, column);
    GRN_TEXT_SETS(ctx, &sub_output, path);
    dump_string(ctx, dumper, &sub_output);
    GRN_OBJ_FIN(ctx, &sub_output);
  }
  GRN_TEXT_PUTC(ctx, dumper->output, '\n');

exit :
  if (dumper->is_close_opened_object_mode) {
    grn_ctx_pop_temporary_open_space(ctx);
  }
}

static void
dump_columns(grn_ctx *ctx, grn_dumper *dumper, grn_obj *table,
             grn_bool dump_data_column,
             grn_bool dump_reference_column,
             grn_bool dump_index_column)
{
  grn_hash *columns;
  columns = grn_hash_create(ctx, NULL, sizeof(grn_id), 0,
                            GRN_OBJ_TABLE_HASH_KEY|GRN_HASH_TINY);
  if (!columns) {
    GRN_PLUGIN_ERROR(ctx,
                     GRN_NO_MEMORY_AVAILABLE,
                     "couldn't create a hash to hold columns");
    return;
  }

  if (grn_table_columns(ctx, table, NULL, 0, (grn_obj *)columns) >= 0) {
    GRN_HASH_EACH_BEGIN(ctx, columns, cursor, id) {
      void *key;
      grn_id column_id;
      grn_obj *column;

      grn_hash_cursor_get_key(ctx, cursor, &key);
      column_id = *((grn_id *)key);

      if (dumper->is_close_opened_object_mode) {
        grn_ctx_push_temporary_open_space(ctx);
      }

      column = grn_ctx_at(ctx, column_id);
      if (!column) {
        GRN_PLUGIN_CLEAR_ERROR(ctx);
        goto next_loop;
      }

      if (grn_obj_is_index_column(ctx, column)) {
        if (dump_index_column) {
          dump_column(ctx, dumper, table, column);
          GRN_PLUGIN_CLEAR_ERROR(ctx);
        }
      } else if (grn_obj_is_reference_column(ctx, column)) {
        if (dump_reference_column) {
          dump_column(ctx, dumper, table, column);
          GRN_PLUGIN_CLEAR_ERROR(ctx);
        }
      } else {
        if (dump_data_column) {
          dump_column(ctx, dumper, table, column);
          GRN_PLUGIN_CLEAR_ERROR(ctx);
        }
      }

    next_loop :
      if (dumper->is_close_opened_object_mode) {
        grn_ctx_pop_temporary_open_space(ctx);
      }
    } GRN_HASH_EACH_END(ctx, cursor);
  }
  grn_hash_close(ctx, columns);
}

static void
dump_record_column_vector(grn_ctx *ctx, grn_dumper *dumper, grn_id id,
                          grn_obj *column, grn_id range_id, grn_obj *buf)
{
  grn_obj *range;
  grn_obj_format *format_argument = NULL;
  grn_obj_format format;

  range = grn_ctx_at(ctx, range_id);
  grn_column_flags column_flags = grn_column_get_flags(ctx, column);
  if (column_flags & GRN_OBJ_WITH_WEIGHT) {
    format.flags = GRN_OBJ_FORMAT_WITH_WEIGHT;
    if (column_flags & GRN_OBJ_WEIGHT_FLOAT32) {
      format.flags |= GRN_OBJ_FORMAT_WEIGHT_FLOAT32;
    }
    format_argument = &format;
  }

  if (grn_obj_is_table(ctx, range) ||
      (range->header.flags & GRN_OBJ_KEY_VAR_SIZE) == 0) {
    GRN_OBJ_INIT(buf, GRN_UVECTOR, 0, range_id);
    grn_obj_get_value(ctx, column, id, buf);
    grn_text_otoj(ctx, dumper->output, buf, format_argument);
  } else {
    GRN_OBJ_INIT(buf, GRN_VECTOR, 0, range_id);
    grn_obj_get_value(ctx, column, id, buf);
    grn_text_otoj(ctx, dumper->output, buf, format_argument);
  }

  grn_obj_unlink(ctx, range);
  grn_obj_unlink(ctx, buf);
}

static void
dump_record(grn_ctx *ctx, grn_dumper *dumper,
            grn_obj *table,
            grn_id id,
            grn_obj *columns, int n_columns)
{
  int j;
  grn_obj buf;
  grn_obj *column_name = &(dumper->column_name_buffer);

  GRN_TEXT_PUTC(ctx, dumper->output, '[');
  for (j = 0; j < n_columns; j++) {
    grn_bool is_value_column;
    grn_id range;
    grn_obj *column;
    column = GRN_PTR_VALUE_AT(columns, j);
    /* TODO: use grn_obj_is_value_accessor() */
    GRN_BULK_REWIND(column_name);
    grn_column_name_(ctx, column, column_name);
    if (GRN_TEXT_LEN(column_name) == GRN_COLUMN_NAME_VALUE_LEN &&
        !memcmp(GRN_TEXT_VALUE(column_name),
                GRN_COLUMN_NAME_VALUE,
                GRN_COLUMN_NAME_VALUE_LEN)) {
      is_value_column = GRN_TRUE;
    } else {
      is_value_column = GRN_FALSE;
    }
    range = grn_obj_get_range(ctx, column);

    if (j) { GRN_TEXT_PUTC(ctx, dumper->output, ','); }
    switch (column->header.type) {
    case GRN_COLUMN_VAR_SIZE:
    case GRN_COLUMN_FIX_SIZE:
      switch (column->header.flags & GRN_OBJ_COLUMN_TYPE_MASK) {
      case GRN_OBJ_COLUMN_VECTOR:
        dump_record_column_vector(ctx, dumper, id, column, range, &buf);
        break;
      case GRN_OBJ_COLUMN_SCALAR:
        {
          GRN_OBJ_INIT(&buf, GRN_BULK, 0, range);
          grn_obj_get_value(ctx, column, id, &buf);
          grn_text_otoj(ctx, dumper->output, &buf, NULL);
          grn_obj_unlink(ctx, &buf);
        }
        break;
      default:
        GRN_PLUGIN_ERROR(ctx,
                         GRN_OPERATION_NOT_SUPPORTED,
                         "unsupported column type: %#x",
                         column->header.type);
        break;
      }
      break;
    case GRN_COLUMN_INDEX:
      break;
    case GRN_ACCESSOR:
      {
        GRN_OBJ_INIT(&buf, GRN_BULK, 0, range);
        grn_obj_get_value(ctx, column, id, &buf);
        /* XXX maybe, grn_obj_get_range() should not unconditionally return
           GRN_DB_INT32 when column is GRN_ACCESSOR and
           GRN_ACCESSOR_GET_VALUE */
        if (is_value_column) {
          buf.header.domain = grn_obj_get_range(ctx, table);
        }
        grn_text_otoj(ctx, dumper->output, &buf, NULL);
        grn_obj_unlink(ctx, &buf);
      }
      break;
    default:
      GRN_PLUGIN_ERROR(ctx,
                       GRN_OPERATION_NOT_SUPPORTED,
                       "unsupported header type %#x",
                       column->header.type);
      break;
    }
  }
  GRN_TEXT_PUTC(ctx, dumper->output, ']');
  if (GRN_TEXT_LEN(dumper->output) >= DUMP_FLUSH_THRESHOLD_SIZE) {
    grn_ctx_output_flush(ctx, 0);
  }
}

static void
dump_records(grn_ctx *ctx, grn_dumper *dumper, grn_obj *table)
{
  grn_table_cursor *cursor;
  int i, n_columns;
  grn_obj columns;
  bool have_auto_generated_value_column = false;
  bool have_data_column = false;

  if (grn_table_size(ctx, table) == 0) {
    return;
  }

  if (dumper->is_close_opened_object_mode) {
    grn_ctx_push_temporary_open_space(ctx);
  }

  GRN_PTR_INIT(&columns, GRN_OBJ_VECTOR, GRN_ID_NIL);

  if (table->header.type == GRN_TABLE_NO_KEY) {
    grn_obj *id_accessor;
    id_accessor = grn_obj_column(ctx,
                                 table,
                                 GRN_COLUMN_NAME_ID,
                                 GRN_COLUMN_NAME_ID_LEN);
    GRN_PTR_PUT(ctx, &columns, id_accessor);
  } else if (table->header.domain != GRN_ID_NIL) {
    grn_obj *key_accessor;
    key_accessor = grn_obj_column(ctx,
                                  table,
                                  GRN_COLUMN_NAME_KEY,
                                  GRN_COLUMN_NAME_KEY_LEN);
    GRN_PTR_PUT(ctx, &columns, key_accessor);
  }

  if (grn_obj_get_range(ctx, table) != GRN_ID_NIL) {
    grn_obj *value_accessor;
    value_accessor = grn_obj_column(ctx,
                                    table,
                                    GRN_COLUMN_NAME_VALUE,
                                    GRN_COLUMN_NAME_VALUE_LEN);
    GRN_PTR_PUT(ctx, &columns, value_accessor);
  }

  {
    grn_hash *real_columns;

    real_columns = grn_hash_create(ctx, NULL, sizeof(grn_id), 0,
                                   GRN_OBJ_TABLE_HASH_KEY|GRN_HASH_TINY);
    grn_table_columns(ctx, table, NULL, 0, (grn_obj *)real_columns);
    GRN_HASH_EACH_BEGIN(ctx, real_columns, cursor, id) {
      void *key;
      grn_id column_id;
      grn_obj *column;

      if (dumper->is_close_opened_object_mode) {
        grn_ctx_push_temporary_open_space(ctx);
      }

      grn_hash_cursor_get_key(ctx, cursor, &key);
      column_id = *((grn_id *)key);

      column = grn_ctx_at(ctx, column_id);
      if (column) {
        if (grn_obj_is_index_column(ctx, column) ||
            grn_obj_is_token_column(ctx, column)) {
          have_auto_generated_value_column = true;
          if (dumper->is_close_opened_object_mode) {
            grn_ctx_pop_temporary_open_space(ctx);
          }
        } else {
          have_data_column = true;
          GRN_PTR_PUT(ctx, &columns, column);
          if (dumper->is_close_opened_object_mode) {
            grn_ctx_merge_temporary_open_space(ctx);
          }
        }
      } else {
        GRN_PLUGIN_CLEAR_ERROR(ctx);
        if (dumper->is_close_opened_object_mode) {
          grn_ctx_pop_temporary_open_space(ctx);
        }
      }
    } GRN_HASH_EACH_END(ctx, cursor);
    grn_hash_close(ctx, real_columns);
  }

  n_columns = GRN_BULK_VSIZE(&columns) / sizeof(grn_obj *);

  if (have_auto_generated_value_column && !have_data_column) {
    goto exit;
  }

  if (GRN_TEXT_LEN(dumper->output) > 0) {
    GRN_TEXT_PUTC(ctx, dumper->output, '\n');
  }

  GRN_TEXT_PUTS(ctx, dumper->output, "load --table ");
  dump_obj_name(ctx, dumper, table);
  GRN_TEXT_PUTS(ctx, dumper->output, "\n[\n");

  GRN_TEXT_PUTC(ctx, dumper->output, '[');
  for (i = 0; i < n_columns; i++) {
    grn_obj *column;
    grn_obj *column_name = &(dumper->column_name_buffer);

    column = GRN_PTR_VALUE_AT(&columns, i);
    if (i) { GRN_TEXT_PUTC(ctx, dumper->output, ','); }
    GRN_BULK_REWIND(column_name);
    grn_column_name_(ctx, column, column_name);
    grn_text_otoj(ctx, dumper->output, column_name, NULL);
  }
  GRN_TEXT_PUTS(ctx, dumper->output, "],\n");

  if (table->header.type == GRN_TABLE_HASH_KEY && dumper->is_sort_hash_table) {
    grn_obj *sorted;
    grn_table_sort_key sort_keys[1];
    uint32_t n_sort_keys = 1;
    grn_bool is_first_record = GRN_TRUE;

    sort_keys[0].key = grn_obj_column(ctx, table,
                                      GRN_COLUMN_NAME_KEY,
                                      GRN_COLUMN_NAME_KEY_LEN);
    sort_keys[0].flags = GRN_TABLE_SORT_ASC;
    sort_keys[0].offset = 0;
    sorted = grn_table_create(ctx,
                              NULL, 0, NULL,
                              GRN_TABLE_NO_KEY,
                              NULL,
                              table);
    grn_table_sort(ctx,
                   table, 0, -1,
                   sorted,
                   sort_keys, n_sort_keys);
    cursor = grn_table_cursor_open(ctx,
                                   sorted,
                                   NULL, 0, NULL, 0,
                                   0, -1,
                                   0);
    while (grn_table_cursor_next(ctx, cursor) != GRN_ID_NIL) {
      void *value_raw;
      grn_id id;

      grn_table_cursor_get_value(ctx, cursor, &value_raw);
      id = *((grn_id *)value_raw);

      if (is_first_record) {
        is_first_record = GRN_FALSE;
      } else {
        GRN_TEXT_PUTS(ctx, dumper->output, ",\n");
      }
      dump_record(ctx, dumper, table, id, &columns, n_columns);
    }
    grn_table_cursor_close(ctx, cursor);
    GRN_TEXT_PUTS(ctx, dumper->output, "\n]\n");
    grn_obj_close(ctx, sorted);
    grn_obj_unlink(ctx, sort_keys[0].key);
  } else {
    grn_obj delete_commands;
    grn_id old_id = GRN_ID_NIL;
    grn_id id;

    GRN_TEXT_INIT(&delete_commands, 0);
    cursor = grn_table_cursor_open(ctx, table, NULL, 0, NULL, 0, 0, -1,
                                   GRN_CURSOR_BY_KEY);
    while ((id = grn_table_cursor_next(ctx, cursor)) != GRN_ID_NIL) {
      if (old_id != GRN_ID_NIL) { GRN_TEXT_PUTS(ctx, dumper->output, ",\n"); }
      if (table->header.type == GRN_TABLE_NO_KEY && old_id + 1 < id) {
        grn_id current_id;
        for (current_id = old_id + 1; current_id < id; current_id++) {
          GRN_TEXT_PUTS(ctx, dumper->output, "[],\n");
          GRN_TEXT_PUTS(ctx, &delete_commands, "delete --table ");
          dump_obj_name_raw(ctx, &delete_commands, table);
          GRN_TEXT_PUTS(ctx, &delete_commands, " --id ");
          grn_text_lltoa(ctx, &delete_commands, current_id);
          GRN_TEXT_PUTC(ctx, &delete_commands, '\n');
        }
      }
      dump_record(ctx, dumper, table, id, &columns, n_columns);

      old_id = id;
    }
    grn_table_cursor_close(ctx, cursor);
    GRN_TEXT_PUTS(ctx, dumper->output, "\n]\n");
    GRN_TEXT_PUT(ctx, dumper->output,
                 GRN_TEXT_VALUE(&delete_commands),
                 GRN_TEXT_LEN(&delete_commands));
    GRN_OBJ_FIN(ctx, &delete_commands);
  }
exit :
  for (i = 0; i < n_columns; i++) {
    grn_obj *column;

    column = GRN_PTR_VALUE_AT(&columns, i);
    if (column->header.type == GRN_ACCESSOR) {
      grn_obj_close(ctx, column);
    }
  }
  GRN_OBJ_FIN(ctx, &columns);

  if (dumper->is_close_opened_object_mode) {
    grn_ctx_pop_temporary_open_space(ctx);
  }
}

static void
dump_table(grn_ctx *ctx, grn_dumper *dumper, grn_obj *table)
{
  grn_obj *domain = NULL;
  grn_id range_id;
  grn_obj *range = NULL;
  grn_table_flags flags;
  grn_table_flags default_flags = GRN_OBJ_PERSISTENT;
  grn_obj *default_tokenizer;
  grn_obj *normalizer;
  grn_obj *token_filters;

  switch (table->header.type) {
  case GRN_TABLE_HASH_KEY:
  case GRN_TABLE_PAT_KEY:
  case GRN_TABLE_DAT_KEY:
    domain = grn_ctx_at(ctx, table->header.domain);
    break;
  default:
    break;
  }

  if (GRN_TEXT_LEN(dumper->output) > 0) {
    GRN_TEXT_PUTC(ctx, dumper->output, '\n');
    grn_ctx_output_flush(ctx, 0);
  }

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

  GRN_TEXT_PUTS(ctx, dumper->output, "table_create ");
  dump_obj_name(ctx, dumper, table);
  GRN_TEXT_PUTC(ctx, dumper->output, ' ');
  grn_dump_table_create_flags(ctx,
                              flags & ~default_flags,
                              dumper->output);
  if (domain) {
    GRN_TEXT_PUTC(ctx, dumper->output, ' ');
    dump_obj_name(ctx, dumper, domain);
  }
  range_id = grn_obj_get_range(ctx, table);
  if (range_id != GRN_ID_NIL) {
    range = grn_ctx_at(ctx, range_id);
    if (!range) {
      // ERR(GRN_RANGE_ERROR, "couldn't get table's value_type object");
      return;
    }
    if (table->header.type != GRN_TABLE_NO_KEY) {
      GRN_TEXT_PUTC(ctx, dumper->output, ' ');
    } else {
      GRN_TEXT_PUTS(ctx, dumper->output, " --value_type ");
    }
    dump_obj_name(ctx, dumper, range);
    grn_obj_unlink(ctx, range);
  }
  if (default_tokenizer) {
    grn_obj sub_output;
    GRN_TEXT_PUTS(ctx, dumper->output, " --default_tokenizer ");
    GRN_TEXT_INIT(&sub_output, 0);
    grn_table_get_default_tokenizer_string(ctx, table, &sub_output);
    dump_string(ctx, dumper, &sub_output);
    GRN_OBJ_FIN(ctx, &sub_output);
  }
  if (normalizer) {
    grn_obj sub_output;
    GRN_TEXT_PUTS(ctx, dumper->output, " --normalizer ");
    GRN_TEXT_INIT(&sub_output, 0);
    grn_table_get_normalizer_string(ctx, table, &sub_output);
    dump_string(ctx, dumper, &sub_output);
    GRN_OBJ_FIN(ctx, &sub_output);
  }
  if (token_filters && GRN_BULK_VSIZE(token_filters) > 0) {
    grn_obj sub_output;
    GRN_TEXT_PUTS(ctx, dumper->output, " --token_filters ");
    GRN_TEXT_INIT(&sub_output, 0);
    grn_table_get_token_filters_string(ctx, table, &sub_output);
    dump_string(ctx, dumper, &sub_output);
    GRN_OBJ_FIN(ctx, &sub_output);
  }
  if ((flags & GRN_OBJ_CUSTOM_NAME) && dumper->is_dump_paths) {
    grn_obj sub_output;
    GRN_TEXT_PUTS(ctx, dumper->output, " --path ");
    GRN_TEXT_INIT(&sub_output, GRN_OBJ_DO_SHALLOW_COPY);
    const char *path = grn_obj_path(ctx, table);
    GRN_TEXT_SETS(ctx, &sub_output, path);
    dump_string(ctx, dumper, &sub_output);
    GRN_OBJ_FIN(ctx, &sub_output);
  }

  GRN_TEXT_PUTC(ctx, dumper->output, '\n');

  dump_columns(ctx, dumper, table, GRN_TRUE, GRN_FALSE, GRN_FALSE);
}

static void
dump_schema(grn_ctx *ctx, grn_dumper *dumper)
{
  GRN_DB_EACH_BEGIN_BY_KEY(ctx, cursor, id) {
    void *name;
    int name_size;
    grn_obj *object;

    if (grn_id_is_builtin(ctx, id)) {
      continue;
    }

    name_size = grn_table_cursor_get_key(ctx, cursor, &name);
    if (grn_obj_name_is_column(ctx, name, name_size)) {
      continue;
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    if ((object = grn_ctx_at(ctx, id))) {
      switch (object->header.type) {
      case GRN_TABLE_HASH_KEY:
      case GRN_TABLE_PAT_KEY:
      case GRN_TABLE_DAT_KEY:
      case GRN_TABLE_NO_KEY:
        dump_table(ctx, dumper, object);
        break;
      default:
        break;
      }
    } else {
      /* XXX: this clause is executed when MeCab tokenizer is enabled in
         database but the groonga isn't supported MeCab.
         We should return error mesage about it and error exit status
         but it's too difficult for this architecture. :< */
      GRN_PLUGIN_CLEAR_ERROR(ctx);
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  } GRN_DB_EACH_END(ctx, cursor);

  if (!dumper->have_reference_column) {
    return;
  }

  GRN_TEXT_PUTC(ctx, dumper->output, '\n');
  grn_ctx_output_flush(ctx, 0);

  GRN_DB_EACH_BEGIN_BY_KEY(ctx, cursor, id) {
    void *name;
    int name_size;
    grn_obj *object;

    if (grn_id_is_builtin(ctx, id)) {
      continue;
    }

    name_size = grn_table_cursor_get_key(ctx, cursor, &name);
    if (grn_obj_name_is_column(ctx, name, name_size)) {
      continue;
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    if ((object = grn_ctx_at(ctx, id))) {
      switch (object->header.type) {
      case GRN_TABLE_HASH_KEY:
      case GRN_TABLE_PAT_KEY:
      case GRN_TABLE_DAT_KEY:
      case GRN_TABLE_NO_KEY:
        dump_columns(ctx, dumper, object, GRN_FALSE, GRN_TRUE, GRN_FALSE);
        break;
      default:
        break;
      }
    } else {
      /* XXX: this clause is executed when MeCab tokenizer is enabled in
         database but the groonga isn't supported MeCab.
         We should return error mesage about it and error exit status
         but it's too difficult for this architecture. :< */
      GRN_PLUGIN_CLEAR_ERROR(ctx);
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  } GRN_DB_EACH_END(ctx, cursor);
}

static void
dump_selected_tables_records(grn_ctx *ctx, grn_dumper *dumper, grn_obj *tables)
{
  const char *p, *e;

  p = GRN_TEXT_VALUE(tables);
  e = p + GRN_TEXT_LEN(tables);
  while (p < e) {
    int len;
    grn_obj *table;
    const char *token, *token_e;

    if ((len = grn_isspace(p, ctx->encoding))) {
      p += len;
      continue;
    }

    token = p;
    if (!(('a' <= *p && *p <= 'z') ||
          ('A' <= *p && *p <= 'Z') ||
          (*p == '_'))) {
      while (p < e && !grn_isspace(p, ctx->encoding)) {
        p++;
      }
      GRN_LOG(ctx, GRN_LOG_WARNING, "invalid table name is ignored: <%.*s>\n",
              (int)(p - token), token);
      continue;
    }
    while (p < e &&
           (('a' <= *p && *p <= 'z') ||
            ('A' <= *p && *p <= 'Z') ||
            ('0' <= *p && *p <= '9') ||
            (*p == '_'))) {
      p++;
    }
    token_e = p;
    while (p < e && (len = grn_isspace(p, ctx->encoding))) {
      p += len;
      continue;
    }
    if (p < e && *p == ',') {
      p++;
    }

    table = grn_ctx_get(ctx, token, token_e - token);
    if (!table) {
      GRN_LOG(ctx, GRN_LOG_WARNING,
              "nonexistent table name is ignored: <%.*s>\n",
              (int)(token_e - token), token);
      continue;
    }

    if (grn_obj_is_table(ctx, table)) {
      dump_records(ctx, dumper, table);
    }
    grn_obj_unlink(ctx, table);
  }
}

static void
dump_all_records(grn_ctx *ctx, grn_dumper *dumper)
{
  GRN_DB_EACH_BEGIN_BY_KEY(ctx, cursor, id) {
    void *name;
    int name_size;
    grn_obj *table;

    if (grn_id_is_builtin(ctx, id)) {
      continue;
    }

    name_size = grn_table_cursor_get_key(ctx, cursor, &name);
    if (grn_obj_name_is_column(ctx, name, name_size)) {
      continue;
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    table = grn_ctx_at(ctx, id);
    if (!table) {
      /* XXX: this clause is executed when MeCab tokenizer is enabled in
         database but the groonga isn't supported MeCab.
         We should return error mesage about it and error exit status
         but it's too difficult for this architecture. :< */
      GRN_PLUGIN_CLEAR_ERROR(ctx);
      goto next_loop;
    }

    if (grn_obj_is_table(ctx, table)) {
      dump_records(ctx, dumper, table);
    }

  next_loop :
    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  } GRN_DB_EACH_END(ctx, cursor);
}

static void
dump_indexes(grn_ctx *ctx, grn_dumper *dumper)
{
  if (!dumper->have_index_column) {
    return;
  }

  if (GRN_TEXT_LEN(dumper->output) > 0) {
    GRN_TEXT_PUTC(ctx, dumper->output, '\n');
  }

  GRN_DB_EACH_BEGIN_BY_KEY(ctx, cursor, id) {
    void *name;
    int name_size;
    grn_obj *object;

    if (grn_id_is_builtin(ctx, id)) {
      continue;
    }

    name_size = grn_table_cursor_get_key(ctx, cursor, &name);
    if (grn_obj_name_is_column(ctx, name, name_size)) {
      continue;
    }

    if (dumper->is_close_opened_object_mode) {
      grn_ctx_push_temporary_open_space(ctx);
    }

    object = grn_ctx_at(ctx, id);
    if (!object) {
      /* XXX: this clause is executed when MeCab tokenizer is enabled in
         database but the groonga isn't supported MeCab.
         We should return error mesage about it and error exit status
         but it's too difficult for this architecture. :< */
      GRN_PLUGIN_CLEAR_ERROR(ctx);
      goto next_loop;
    }

    if (grn_obj_is_table(ctx, object)) {
      dump_columns(ctx, dumper, object, GRN_FALSE, GRN_FALSE, GRN_TRUE);
    }

  next_loop :
    if (dumper->is_close_opened_object_mode) {
      grn_ctx_pop_temporary_open_space(ctx);
    }
  } GRN_DB_EACH_END(ctx, cursor);
}

static grn_obj *
command_dump(grn_ctx *ctx, int nargs, grn_obj **args, grn_user_data *user_data)
{
  grn_dumper dumper;
  grn_obj *tables;
  bool is_dump_plugins;
  bool is_dump_schema;
  bool is_dump_records;
  bool is_dump_indexes;
  bool is_dump_configs;

  dumper.output = ctx->impl->output.buf;
  if (grn_thread_get_limit() == 1) {
    dumper.is_close_opened_object_mode = GRN_TRUE;
  } else {
    dumper.is_close_opened_object_mode = GRN_FALSE;
  }
  dumper.have_reference_column = GRN_FALSE;
  dumper.have_index_column = GRN_FALSE;

  tables = grn_plugin_proc_get_var(ctx, user_data, "tables", -1);
  is_dump_plugins = grn_plugin_proc_get_var_bool(ctx, user_data,
                                                 "dump_plugins", -1,
                                                 true);
  is_dump_schema = grn_plugin_proc_get_var_bool(ctx, user_data,
                                                "dump_schema", -1,
                                                true);
  is_dump_records = grn_plugin_proc_get_var_bool(ctx, user_data,
                                                 "dump_records", -1,
                                                 true);
  is_dump_indexes = grn_plugin_proc_get_var_bool(ctx, user_data,
                                                 "dump_indexes", -1,
                                                 true);
  is_dump_configs = grn_plugin_proc_get_var_bool(ctx, user_data,
                                                 "dump_configs", -1,
                                                 true);
  dumper.is_sort_hash_table =
    grn_plugin_proc_get_var_bool(ctx, user_data,
                                 "sort_hash_table", -1,
                                 GRN_FALSE);
  dumper.is_dump_paths = grn_plugin_proc_get_var_bool(ctx, user_data,
                                                      "dump_paths", -1,
                                                      true);
  GRN_TEXT_INIT(&(dumper.column_name_buffer), 0);

  grn_ctx_set_output_type(ctx, GRN_CONTENT_GROONGA_COMMAND_LIST);

  dumper_collect_statistics(ctx, &dumper);

  if (is_dump_configs) {
    dump_configs(ctx, &dumper);
  }
  if (is_dump_plugins) {
    dump_plugins(ctx, &dumper);
  }
  if (is_dump_schema) {
    dump_schema(ctx, &dumper);
  }
  if (is_dump_records) {
    /* To update index columns correctly, we first create the whole schema, then
       load non-derivative records, while skipping records of index columns. That
       way, Groonga will silently do the job of updating index columns for us. */
    if (GRN_TEXT_LEN(tables) > 0) {
      dump_selected_tables_records(ctx, &dumper, tables);
    } else {
      dump_all_records(ctx, &dumper);
    }
  }
  if (is_dump_indexes) {
    dump_indexes(ctx, &dumper);
  }
  /* remove the last newline because another one will be added by the caller.
     maybe, the caller of proc functions currently doesn't consider the
     possibility of multiple-line output from proc functions. */
  if (GRN_BULK_VSIZE(dumper.output) > 0) {
    grn_bulk_truncate(ctx, dumper.output, GRN_BULK_VSIZE(dumper.output) - 1);
  }

  GRN_OBJ_FIN(ctx, &(dumper.column_name_buffer));

  return NULL;
}

void
grn_proc_init_dump(grn_ctx *ctx)
{
  grn_expr_var vars[8];

  grn_plugin_expr_var_init(ctx, &(vars[0]), "tables", -1);
  grn_plugin_expr_var_init(ctx, &(vars[1]), "dump_plugins", -1);
  grn_plugin_expr_var_init(ctx, &(vars[2]), "dump_schema", -1);
  grn_plugin_expr_var_init(ctx, &(vars[3]), "dump_records", -1);
  grn_plugin_expr_var_init(ctx, &(vars[4]), "dump_indexes", -1);
  grn_plugin_expr_var_init(ctx, &(vars[5]), "dump_configs", -1);
  grn_plugin_expr_var_init(ctx, &(vars[6]), "sort_hash_table", -1);
  grn_plugin_expr_var_init(ctx, &(vars[7]), "dump_paths", -1);
  grn_plugin_command_create(ctx,
                            "dump", -1,
                            command_dump,
                            sizeof(vars) / sizeof(vars[0]),
                            vars);
}
