/*
 * 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.
 */

#include <bsonutil/bson-parser.h>

#include <mongoc/utlist.h>

#include <mlib/config.h>

#include <TestSuite.h>
#include <test-conveniences.h>
#include <unified/util.h>

typedef enum {
   BSON_PARSER_UTF8,
   BSON_PARSER_INT,
   BSON_PARSER_BOOL,
   BSON_PARSER_DOC,
   BSON_PARSER_ARRAY,
   BSON_PARSER_ARRAY_OR_DOC,
   BSON_PARSER_ANY,
   BSON_PARSER_WRITE_CONCERN,
   BSON_PARSER_READ_CONCERN,
   BSON_PARSER_READ_PREFS
} bson_parser_type_t;

typedef struct _bson_parser_entry_t {
   bson_parser_type_t ptype;
   bool optional;
   void *out;
   char *key;
   bool set;
   struct _bson_parser_entry_t *next;
} bson_parser_entry_t;

struct _bson_parser_t {
   bson_parser_entry_t *entries;
   bool allow_extra;
   bson_t *extra;
};

static const char *
parser_type_to_string(bson_parser_type_t ptype)
{
   switch (ptype) {
   case BSON_PARSER_UTF8:
      return "UTF8";
   case BSON_PARSER_INT:
      return "INT";
   case BSON_PARSER_BOOL:
      return "BOOL";
   case BSON_PARSER_DOC:
      return "DOC";
   case BSON_PARSER_ARRAY:
      return "ARRAY";
   case BSON_PARSER_ARRAY_OR_DOC:
      return "ARRAY or DOC";
   case BSON_PARSER_ANY:
      return "ANY";
   case BSON_PARSER_WRITE_CONCERN:
      return "WRITE_CONCERN";
   case BSON_PARSER_READ_CONCERN:
      return "READ_CONCERN";
   case BSON_PARSER_READ_PREFS:
      return "READ_PREFS";
   default:
      return "INVALID";
   }
}

static mongoc_write_concern_t *
bson_to_write_concern(bson_t *bson, bson_error_t *error)
{
   bson_parser_t *parser = NULL;
   mongoc_write_concern_t *out = NULL;
   bool *journal = NULL;
   bson_val_t *w_val = NULL;
   int64_t *wtimeoutms;

   parser = bson_parser_new();
   bson_parser_bool_optional(parser, "journal", &journal);
   bson_parser_any_optional(parser, "w", &w_val);
   bson_parser_int_optional(parser, "wTimeoutMS", &wtimeoutms);

   if (!bson_parser_parse(parser, bson, error)) {
      goto done;
   }

   out = mongoc_write_concern_new();
   if (journal) {
      mongoc_write_concern_set_journal(out, *journal);
   }

   if (w_val) {
      if (bson_val_is_numeric(w_val)) {
         mongoc_write_concern_set_w(out, (int32_t)bson_val_convert_int64(w_val));
      } else if (bson_val_type(w_val) == BSON_TYPE_UTF8 && 0 == strcmp(bson_val_to_utf8(w_val), "majority")) {
         mongoc_write_concern_set_w(out, MONGOC_WRITE_CONCERN_W_MAJORITY);
      } else {
         test_set_error(error, "unrecognized value for 'w': %s", bson_val_to_json(w_val));
      }
   }

   if (wtimeoutms) {
      mongoc_write_concern_set_wtimeout_int64(out, *wtimeoutms);
   }

done:
   bson_parser_destroy_with_parsed_fields(parser);
   return out;
}

static mongoc_read_concern_t *
bson_to_read_concern(bson_t *bson, bson_error_t *error)
{
   bson_parser_t *parser = NULL;
   mongoc_read_concern_t *out = NULL;
   char *level = NULL;

   parser = bson_parser_new();
   bson_parser_utf8_optional(parser, "level", &level);

   if (!bson_parser_parse(parser, bson, error)) {
      goto done;
   }

   out = mongoc_read_concern_new();
   if (level) {
      mongoc_read_concern_set_level(out, level);
   }

done:
   bson_parser_destroy_with_parsed_fields(parser);
   return out;
}

/* Returns 0 on error. */
static mongoc_read_mode_t
string_to_read_mode(char *str, bson_error_t *error)
{
   if (0 == bson_strcasecmp("primary", str)) {
      return MONGOC_READ_PRIMARY;
   } else if (0 == bson_strcasecmp("primarypreferred", str)) {
      return MONGOC_READ_PRIMARY_PREFERRED;
   } else if (0 == bson_strcasecmp("secondary", str)) {
      return MONGOC_READ_SECONDARY;
   } else if (0 == bson_strcasecmp("secondarypreferred", str)) {
      return MONGOC_READ_SECONDARY_PREFERRED;
   } else if (0 == bson_strcasecmp("nearest", str)) {
      return MONGOC_READ_NEAREST;
   }

   test_set_error(error, "Invalid read mode: %s", str);
   return 0;
}

static mongoc_read_prefs_t *
bson_to_read_prefs(bson_t *bson, bson_error_t *error)
{
   bson_parser_t *parser = NULL;
   mongoc_read_prefs_t *out = NULL;
   char *mode_string = NULL;
   mongoc_read_mode_t read_mode;
   bson_t *tag_sets = NULL;
   int64_t *max_staleness_seconds;
   bson_t *hedge = NULL;

   parser = bson_parser_new();
   bson_parser_utf8(parser, "mode", &mode_string);
   bson_parser_array_optional(parser, "tagSets", &tag_sets);
   bson_parser_int_optional(parser, "maxStalenessSeconds", &max_staleness_seconds);
   bson_parser_doc_optional(parser, "hedge", &hedge);

   if (!bson_parser_parse(parser, bson, error)) {
      goto done;
   }

   read_mode = string_to_read_mode(mode_string, error);
   if (read_mode == 0) {
      goto done;
   }

   out = mongoc_read_prefs_new(read_mode);
   if (tag_sets) {
      mongoc_read_prefs_set_tags(out, tag_sets);
   }

   if (max_staleness_seconds) {
      mongoc_read_prefs_set_max_staleness_seconds(out, *max_staleness_seconds);
   }

   if (hedge) {
      mlib_diagnostic_push();
      mlib_disable_deprecation_warnings();
      mongoc_read_prefs_set_hedge(out, hedge);
      mlib_diagnostic_pop();
   }

done:
   bson_parser_destroy_with_parsed_fields(parser);
   return out;
}


bson_parser_t *
bson_parser_new(void)
{
   bson_parser_t *parser = NULL;

   parser = bson_malloc0(sizeof(bson_parser_t));
   parser->extra = bson_new();
   return parser;
}

void
bson_parser_allow_extra(bson_parser_t *parser, bool val)
{
   BSON_ASSERT(parser);
   parser->allow_extra = val;
}

const bson_t *
bson_parser_get_extra(const bson_parser_t *parser)
{
   BSON_ASSERT(parser);
   return parser->extra;
}

static void
bson_parser_entry_destroy(bson_parser_entry_t *entry, bool with_parsed_fields)
{
   if (with_parsed_fields) {
      if (entry->ptype == BSON_PARSER_DOC || entry->ptype == BSON_PARSER_ARRAY ||
          entry->ptype == BSON_PARSER_ARRAY_OR_DOC) {
         bson_t **out;

         out = (bson_t **)entry->out;
         bson_destroy(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_BOOL) {
         bool **out;

         out = (bool **)entry->out;
         bson_free(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_UTF8) {
         char **out;

         out = (char **)entry->out;
         bson_free(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_INT) {
         int64_t **out;

         out = (int64_t **)entry->out;
         bson_free(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_ANY) {
         bson_val_t **out;

         out = (bson_val_t **)entry->out;
         bson_val_destroy(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_WRITE_CONCERN) {
         mongoc_write_concern_t **out;

         out = (mongoc_write_concern_t **)entry->out;
         mongoc_write_concern_destroy(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_READ_CONCERN) {
         mongoc_read_concern_t **out;

         out = (mongoc_read_concern_t **)entry->out;
         mongoc_read_concern_destroy(*out);
         *out = NULL;
      } else if (entry->ptype == BSON_PARSER_READ_PREFS) {
         mongoc_read_prefs_t **out;

         out = (mongoc_read_prefs_t **)entry->out;
         mongoc_read_prefs_destroy(*out);
         *out = NULL;
      }
   }
   bson_free(entry->key);
   bson_free(entry);
}

static void
bson_parser_destroy_helper(bson_parser_t *parser, bool with_parsed_fields)
{
   bson_parser_entry_t *entry, *tmp;

   if (!parser) {
      return;
   }

   LL_FOREACH_SAFE(parser->entries, entry, tmp)
   {
      bson_parser_entry_destroy(entry, with_parsed_fields);
   }
   bson_destroy(parser->extra);
   bson_free(parser);
}

void
bson_parser_destroy(bson_parser_t *parser)
{
   bson_parser_destroy_helper(parser, false);
}

/* This additionally destroys the destination of each parsed field. */
void
bson_parser_destroy_with_parsed_fields(bson_parser_t *parser)
{
   bson_parser_destroy_helper(parser, true);
}

static void
bson_parser_add_entry(bson_parser_t *parser, const char *key, void *out, bson_parser_type_t ptype, bool optional)
{
   bson_parser_entry_t *e = NULL;
   bson_parser_entry_t *match = NULL;

   e = bson_malloc0(sizeof(*e));
   e->optional = optional;
   e->ptype = ptype;
   e->out = out;
   e->key = bson_strdup(key);

   /* Check if an entry already exists for this key. */
   LL_FOREACH(parser->entries, match)
   {
      if (0 == strcmp(match->key, key)) {
         break;
      }
   }

   if (match != NULL) {
      test_error("Invalid parser configuration. Attempted to add duplicated "
                 "type for %s.",
                 key);
   }
   LL_PREPEND(parser->entries, e);
}

void
bson_parser_utf8(bson_parser_t *parser, const char *key, char **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_UTF8, false);
}

void
bson_parser_utf8_optional(bson_parser_t *parser, const char *key, char **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_UTF8, true);
}

void
bson_parser_doc(bson_parser_t *parser, const char *key, bson_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_DOC, false);
}

void
bson_parser_doc_optional(bson_parser_t *parser, const char *key, bson_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_DOC, true);
}

void
bson_parser_array(bson_parser_t *parser, const char *key, bson_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_ARRAY, false);
}
void
bson_parser_array_optional(bson_parser_t *parser, const char *key, bson_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_ARRAY, true);
}

void
bson_parser_array_or_doc(bson_parser_t *parser, const char *key, bson_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_ARRAY_OR_DOC, false);
}
void
bson_parser_array_or_doc_optional(bson_parser_t *parser, const char *key, bson_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_ARRAY_OR_DOC, true);
}

void
bson_parser_bool(bson_parser_t *parser, const char *key, bool **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_BOOL, false);
}

void
bson_parser_bool_optional(bson_parser_t *parser, const char *key, bool **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_BOOL, true);
}

void
bson_parser_int(bson_parser_t *parser, const char *key, int64_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_INT, false);
}

void
bson_parser_int_optional(bson_parser_t *parser, const char *key, int64_t **out)
{
   *out = NULL;
   bson_parser_add_entry(parser, key, (void *)out, BSON_PARSER_INT, true);
}

void
bson_parser_any(bson_parser_t *bp, const char *key, bson_val_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, key, (void *)out, BSON_PARSER_ANY, false);
}

void
bson_parser_any_optional(bson_parser_t *bp, const char *key, bson_val_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, key, (void *)out, BSON_PARSER_ANY, true);
}

void
bson_parser_write_concern(bson_parser_t *bp, mongoc_write_concern_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, "writeConcern", (void *)out, BSON_PARSER_WRITE_CONCERN, false);
}

void
bson_parser_write_concern_optional(bson_parser_t *bp, mongoc_write_concern_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, "writeConcern", (void *)out, BSON_PARSER_WRITE_CONCERN, true);
}

void
bson_parser_read_concern(bson_parser_t *bp, mongoc_read_concern_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, "readConcern", (void *)out, BSON_PARSER_READ_CONCERN, false);
}

void
bson_parser_read_concern_optional(bson_parser_t *bp, mongoc_read_concern_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, "readConcern", (void *)out, BSON_PARSER_READ_CONCERN, true);
}

void
bson_parser_read_prefs(bson_parser_t *bp, mongoc_read_prefs_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, "readPreference", (void *)out, BSON_PARSER_READ_PREFS, false);
}

void
bson_parser_read_prefs_optional(bson_parser_t *bp, mongoc_read_prefs_t **out)
{
   *out = NULL;
   bson_parser_add_entry(bp, "readPreference", (void *)out, BSON_PARSER_READ_PREFS, true);
}

void
marshal_error(const char *key, bson_type_t btype, bson_parser_type_t ptype, bson_error_t *error)
{
   test_set_error(
      error, "expecting %s for '%s' but got: %s", parser_type_to_string(ptype), key, bson_type_to_string(btype));
}

bool
entry_marshal(bson_parser_entry_t *entry, bson_iter_t *iter, bson_error_t *error)
{
   bool ret = false;
   const char *key;
   bson_type_t btype;
   bson_parser_type_t ptype;

   ptype = entry->ptype;
   key = bson_iter_key(iter);
   btype = bson_iter_type(iter);

   if (ptype == BSON_PARSER_UTF8) {
      char **out = (char **)entry->out;

      if (btype != BSON_TYPE_UTF8) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      *out = bson_strdup(bson_iter_utf8(iter, NULL));
   }

   else if (ptype == BSON_PARSER_INT) {
      int64_t **out = (int64_t **)entry->out;

      if (btype != BSON_TYPE_INT32 && btype != BSON_TYPE_INT64) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      *out = bson_malloc0(sizeof(int64_t));
      **out = bson_iter_as_int64(iter);
   }

   else if (ptype == BSON_PARSER_BOOL) {
      bool **out = (bool **)entry->out;

      if (btype != BSON_TYPE_BOOL) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      *out = bson_malloc0(sizeof(bool));
      **out = bson_iter_bool(iter);
   }

   else if (ptype == BSON_PARSER_DOC) {
      bson_t tmp;
      bson_t **out = (bson_t **)entry->out;

      if (btype != BSON_TYPE_DOCUMENT) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      bson_iter_bson(iter, &tmp);
      *out = bson_copy(&tmp);
   }

   else if (ptype == BSON_PARSER_ARRAY) {
      bson_t tmp;
      bson_t **out = (bson_t **)entry->out;

      if (btype != BSON_TYPE_ARRAY) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      bson_iter_bson(iter, &tmp);
      *out = bson_copy(&tmp);
   }

   else if (ptype == BSON_PARSER_ARRAY_OR_DOC) {
      bson_t tmp;
      bson_t **out = (bson_t **)entry->out;

      if (btype != BSON_TYPE_ARRAY && btype != BSON_TYPE_DOCUMENT) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      bson_iter_bson(iter, &tmp);
      *out = bson_copy(&tmp);
   }

   else if (ptype == BSON_PARSER_ANY) {
      bson_val_t **out = (bson_val_t **)entry->out;

      *out = bson_val_from_iter(iter);
   } else if (ptype == BSON_PARSER_WRITE_CONCERN) {
      bson_t tmp;
      mongoc_write_concern_t **out = (mongoc_write_concern_t **)entry->out;

      if (btype != BSON_TYPE_DOCUMENT) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      bson_iter_bson(iter, &tmp);
      *out = bson_to_write_concern(&tmp, error);
      if (!*out) {
         goto done;
      }
   }

   else if (ptype == BSON_PARSER_READ_CONCERN) {
      bson_t tmp;
      mongoc_read_concern_t **out = (mongoc_read_concern_t **)entry->out;
      if (btype != BSON_TYPE_DOCUMENT) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      bson_iter_bson(iter, &tmp);
      *out = bson_to_read_concern(&tmp, error);
   }

   else if (ptype == BSON_PARSER_READ_PREFS) {
      bson_t tmp;
      mongoc_read_prefs_t **out = (mongoc_read_prefs_t **)entry->out;
      if (btype != BSON_TYPE_DOCUMENT) {
         marshal_error(key, btype, ptype, error);
         goto done;
      }

      bson_iter_bson(iter, &tmp);
      *out = bson_to_read_prefs(&tmp, error);
   } else {
      test_set_error(error, "unimplemented parser type: %s", parser_type_to_string(ptype));
      goto done;
   }

   ret = true;
done:
   return ret;
}

bool
bson_parser_parse(bson_parser_t *parser, bson_t *in, bson_error_t *error)
{
   bson_iter_t iter;
   bson_parser_entry_t *entry = NULL;

   /* Check that document is not null. */
   if (in) {
      BSON_FOREACH(in, iter)
      {
         const char *key = bson_iter_key(&iter);
         bson_parser_entry_t *matched = NULL;
         /* Check for a corresponding entry. */
         LL_FOREACH(parser->entries, entry)
         {
            if (0 == strcmp(entry->key, key)) {
               matched = entry;
               break;
            }
         }

         if (matched) {
            if (!entry_marshal(entry, &iter, error)) {
               return false;
            }
            entry->set = true;
         } else if (parser->allow_extra) {
            BSON_APPEND_VALUE(parser->extra, key, bson_iter_value(&iter));
         } else {
            test_set_error(error, "Extra field '%s' found parsing: %s", key, tmp_json(in));
            return false;
         }
      }
   }


   /* Check if there are any unparsed required entries. */
   LL_FOREACH(parser->entries, entry)
   {
      if (!entry->optional && !entry->set) {
         test_set_error(error, "Required field '%s' was not found parsing: %s", entry->key, tmp_json(in));
         return false;
      }
   }
   return true;
}

void
bson_parser_parse_or_assert(bson_parser_t *parser, bson_t *in)
{
   bson_error_t error;

   if (!bson_parser_parse(parser, in, &error)) {
      test_error("Unable to parse: %s: %s", error.message, tmp_json(in));
   }
}
