/*
 * Copyright 2009-present MongoDB, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * This program will scan each BSON document contained in the provided files
 * and print metrics to STDOUT.
 */

#include <bson/bson.h>

#include <math.h>
#include <stdio.h>

#define MAX_RECURSION 100

static double
dtimeofday(void)
{
   struct timeval timeval;
   bson_gettimeofday(&timeval);
   return (timeval.tv_sec + timeval.tv_usec * 0.000001);
}

typedef struct {
   uint64_t count;
   char *description;
} bson_type_metrics_t;

typedef struct {
   uint64_t doc_count;
   uint64_t element_count;
   uint64_t doc_size_max;
   uint64_t key_size_tally;
   uint64_t utf8_size_tally;
   uint32_t depth;
   bson_type_metrics_t bson_type_metrics[256];
} bson_metrics_state_t;

static bson_metrics_state_t initial_state = {0L,
                                             0L,
                                             0L,
                                             0L,
                                             0L,
                                             0L,
                                             {{/* BSON_TYPE_EOD        = 0x00 */ 0L, "End of document"},
                                              {/* BSON_TYPE_DOUBLE     = 0x01 */ 0L, "Floating point"},
                                              {/* BSON_TYPE_UTF8       = 0x02 */ 0L, "UTF-8 string"},
                                              {/* BSON_TYPE_DOCUMENT   = 0x03 */ 0L, "Embedded document"},
                                              {/* BSON_TYPE_ARRAY      = 0x04 */ 0L, "Array"},
                                              {/* BSON_TYPE_BINARY     = 0x05 */ 0L, "Binary data"},
                                              {/* BSON_TYPE_UNDEFINED  = 0x06 */ 0L, "Undefined - Deprecated"},
                                              {/* BSON_TYPE_OID        = 0x07 */ 0L, "ObjectId"},
                                              {/* BSON_TYPE_BOOL       = 0x08 */ 0L, "Boolean"},
                                              {/* BSON_TYPE_DATE_TIME  = 0x09 */ 0L, "UTC datetime"},
                                              {/* BSON_TYPE_NULL       = 0x0A */ 0L, "Null value"},
                                              {/* BSON_TYPE_REGEX      = 0x0B */ 0L, "Regular expression"},
                                              {/* BSON_TYPE_DBPOINTER  = 0x0C */ 0L, "DBPointer - Deprecated"},
                                              {/* BSON_TYPE_CODE       = 0x0D */ 0L, "JavaScript code"},
                                              {/* BSON_TYPE_SYMBOL     = 0x0E */ 0L, "Symbol - Deprecated"},
                                              {/* BSON_TYPE_CODEWSCOPE = 0x0F */ 0L, "JavaScript code w/ scope"},
                                              {/* BSON_TYPE_INT32      = 0x10 */ 0L, "32-bit Integer"},
                                              {/* BSON_TYPE_TIMESTAMP  = 0x11 */ 0L, "Timestamp"},
                                              {/* BSON_TYPE_INT64      = 0x12 */ 0L, "64-bit Integer"},
                                              {0L, NULL}}};

static bson_metrics_state_t state;

static int
compar_bson_type_metrics(const void *a, const void *b)
{
   const uint64_t a_count = ((bson_type_metrics_t *)a)->count;
   const uint64_t b_count = ((bson_type_metrics_t *)b)->count;
   if (a_count == b_count) {
      return 0;
   }
   return a_count < b_count ? -1 : 1;
}

/*
 * Forward declarations.
 */
static bool
bson_metrics_visit_array(const bson_iter_t *iter, const char *key, const bson_t *v_array, void *data);
static bool
bson_metrics_visit_document(const bson_iter_t *iter, const char *key, const bson_t *v_document, void *data);

static bool
bson_metrics_visit_utf8(const bson_iter_t *iter, const char *key, size_t v_utf8_len, const char *v_utf8, void *data)
{
   bson_metrics_state_t *s = data;

   BSON_UNUSED(iter);
   BSON_UNUSED(key);
   BSON_UNUSED(v_utf8);

   s->utf8_size_tally += v_utf8_len;

   return false;
}

static bool
bson_metrics_visit_before(const bson_iter_t *iter, const char *key, void *data)
{
   bson_metrics_state_t *s = data;
   bson_type_t btype;
   ++s->element_count;
   s->key_size_tally += strlen(key); /* TODO - filter out array keys(?) */
   btype = bson_iter_type(iter);
   ++s->bson_type_metrics[btype].count;

   return false;
}

static const bson_visitor_t bson_metrics_visitors = {
   bson_metrics_visit_before,
   NULL, /* visit_after */
   NULL, /* visit_corrupt */
   NULL, /* visit_double */
   bson_metrics_visit_utf8,
   bson_metrics_visit_document,
   bson_metrics_visit_array,
   NULL, /* visit_binary */
   NULL, /* visit_undefined */
   NULL, /* visit_oid */
   NULL, /* visit_bool */
   NULL, /* visit_date_time */
   NULL, /* visit_null */
   NULL, /* visit_regex */
   NULL, /* visit_dbpointer */
   NULL, /* visit_code */
   NULL, /* visit_symbol */
   NULL, /* visit_codewscope */
   NULL, /* visit_int32 */
   NULL, /* visit_timestamp */
   NULL, /* visit_int64 */
   NULL, /* visit_maxkey */
   NULL, /* visit_minkey */
};

static bool
bson_metrics_visit_document(const bson_iter_t *iter, const char *key, const bson_t *v_document, void *data)
{
   bson_metrics_state_t *s = data;
   bson_iter_t child;

   BSON_UNUSED(iter);
   BSON_UNUSED(key);

   if (s->depth >= MAX_RECURSION) {
      fprintf(stderr, "Invalid document, max recursion reached.\n");
      return true;
   }

   if (bson_iter_init(&child, v_document)) {
      s->depth++;
      bson_iter_visit_all(&child, &bson_metrics_visitors, data);
      s->depth--;
   }

   return false;
}

static bool
bson_metrics_visit_array(const bson_iter_t *iter, const char *key, const bson_t *v_array, void *data)
{
   bson_metrics_state_t *s = data;
   bson_iter_t child;

   BSON_UNUSED(iter);
   BSON_UNUSED(key);

   if (s->depth >= MAX_RECURSION) {
      fprintf(stderr, "Invalid document, max recursion reached.\n");
      return true;
   }

   if (bson_iter_init(&child, v_array)) {
      s->depth++;
      bson_iter_visit_all(&child, &bson_metrics_visitors, data);
      s->depth--;
   }

   return false;
}

static void
bson_metrics(const bson_t *bson, size_t *length, void *data)
{
   bson_iter_t iter;
   bson_metrics_state_t *s = data;

   BSON_UNUSED(length);

   ++s->doc_count;

   if (bson_iter_init(&iter, bson)) {
      bson_iter_visit_all(&iter, &bson_metrics_visitors, data);
   }
}

int
main(int argc, char *argv[])
{
   bson_reader_t *reader;
   const bson_t *b;
   bson_error_t error;
   const char *filename;
   int i, j;
   double dtime_before, dtime_after, dtime_delta;
   uint64_t aggregate_count;
   off_t mark;

   /*
    * Print program usage if no arguments are provided.
    */
   if (argc == 1) {
      fprintf(stderr, "usage: %s FILE...\n", argv[0]);
      return 1;
   }

   /*
    * Process command line arguments expecting each to be a filename.
    */
   printf("[");
   for (i = 1; i < argc; i++) {
      if (i > 1)
         printf(",");
      filename = argv[i];

      /*
       * Initialize a new reader for this file descriptor.
       */
      if (!(reader = bson_reader_new_from_file(filename, &error))) {
         fprintf(stderr, "Failed to open \"%s\": %s\n", filename, error.message);
         continue;
      }

      state = initial_state;
      dtime_before = dtimeofday();
      mark = 0;
      while ((b = bson_reader_read(reader, NULL))) {
         off_t pos = bson_reader_tell(reader);
         state.doc_size_max = BSON_MAX(pos - mark, (off_t)state.doc_size_max);
         mark = pos;
         bson_metrics(b, NULL, &state);
      }
      dtime_after = dtimeofday();
      dtime_delta = BSON_MAX(dtime_after - dtime_before, 0.000001);
      state.bson_type_metrics[BSON_TYPE_MAXKEY].description = "Max key";
      state.bson_type_metrics[BSON_TYPE_MINKEY].description = "Min key";
      aggregate_count =
         state.bson_type_metrics[BSON_TYPE_DOCUMENT].count + state.bson_type_metrics[BSON_TYPE_ARRAY].count;
      qsort(state.bson_type_metrics, 256, sizeof(bson_type_metrics_t), compar_bson_type_metrics);

      printf("\n  {\n");
      printf("    \"file\": \"%s\",\n", filename);
      printf("    \"secs\": %.2f,\n", dtime_delta);
      printf("    \"docs_per_sec\": %" PRIu64 ",\n", (uint64_t)floor(state.doc_count / dtime_delta));
      printf("    \"docs\": %" PRIu64 ",\n", state.doc_count);
      printf("    \"elements\": %" PRIu64 ",\n", state.element_count);
      printf("    \"elements_per_doc\": %" PRIu64 ",\n",
             (uint64_t)floor((double)state.element_count / (double)BSON_MAX(state.doc_count, 1)));
      printf("    \"aggregates\": %" PRIu64 ",\n", aggregate_count);
      printf("    \"aggregates_per_doc\": %" PRIu64 ",\n",
             (uint64_t)floor((double)aggregate_count / (double)BSON_MAX(state.doc_count, 1)));
      printf("    \"degree\": %" PRIu64 ",\n",
             (uint64_t)floor((double)state.element_count / ((double)BSON_MAX(state.doc_count + aggregate_count, 1))));
      printf("    \"doc_size_max\": %" PRIu64 ",\n", state.doc_size_max);
      printf("    \"doc_size_average\": %" PRIu64 ",\n",
             (uint64_t)floor((double)bson_reader_tell(reader) / (double)BSON_MAX(state.doc_count, 1)));
      printf("    \"key_size_average\": %" PRIu64 ",\n",
             (uint64_t)floor((double)state.key_size_tally / (double)BSON_MAX(state.element_count, 1)));
      printf("    \"string_size_average\": %" PRIu64 ",\n",
             (uint64_t)floor((double)state.utf8_size_tally /
                             (double)BSON_MAX(state.bson_type_metrics[BSON_TYPE_UTF8].count, 1)));
      printf("    \"percent_by_type\": {\n");
      for (j = 0; state.bson_type_metrics[j].count > 0; j++) {
         bson_type_metrics_t bson_type_metrics = state.bson_type_metrics[j];
         printf("      \"%s\": %" PRIu64 ",\n",
                bson_type_metrics.description,
                (uint64_t)floor((double)bson_type_metrics.count * 100.0 / (double)BSON_MAX(state.element_count, 1)));
      }
      printf("    }\n");
      printf("  }");

      /*
       * Cleanup after our reader, which closes the file descriptor.
       */
      bson_reader_destroy(reader);
   }
   printf("\n]\n");

   return 0;
}
