#include <assert.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <db.h>
#include "tokudb_common.h"

typedef struct {
   bool     leadingspace;
   bool     plaintext;
   bool     header;
   bool     footer;
   bool     is_private;
   char*    progname;
   char*    homedir;
   char*    database;
   char*    subdatabase;
   int      exitcode;
   int      recover_flags;
   DBTYPE   dbtype;
   DBTYPE   opened_dbtype;
   DB*      db;
   DB_ENV*  dbenv;
} dump_globals;

dump_globals g;
#include "tokudb_common_funcs.h"

int   usage          ();
int   create_init_env();
int   dump_database  ();
int   open_database  ();
int   dump_pairs     ();
int   dump_footer    ();
int   dump_header    ();
int   close_database ();

int main(int argc, char *argv[]) {
   int ch;
   int retval;

   /* Set up the globals. */
   memset(&g, 0, sizeof(g));
   g.leadingspace   = true;
   //TODO: Uncomment when DB_UNKNOWN + db->get_type are implemented.
   //g.dbtype         = DB_UNKNOWN;
   g.dbtype         = DB_BTREE;
   g.progname       = argv[0];
   g.header         = true;
   g.footer         = true;

   if (verify_library_version() != 0) goto error;

   while ((ch = getopt(argc, argv, "klNpRrVd:f:h:P:s:")) != EOF) {
      switch (ch) {
         case ('k'): {
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('l'): {
            //TODO: Implement (Requires master database support)
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('N'): {
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('T'): {
            g.plaintext    = true;
            g.leadingspace = false;
            g.header       = false;
            g.footer       = false;
            break;
         }
         case ('p'): {
            g.plaintext = true;
            break;
         }
         case ('R'): {
            //TODO: Uncomment when DB_SALVAGE,DB_AGGRESSIVE are implemented.
            /*g.recover_flags |= DB_SALVAGE | DB_AGGRESSIVE;*/

            //TODO: Implement aggressive recovery (requires db->verify())
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('r'): {
            //TODO: Uncomment when DB_SALVAGE,DB_AGGRESSIVE are implemented.
            /*g.recover_flags |= DB_SALVAGE;*/

            //TODO: Implement recovery (requires db->verify())
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('V'): {
            printf("%s\n", db_version(NULL, NULL, NULL));
            goto cleanup;
         }
         case ('d'): {
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('f'): {
            if (freopen(optarg, "w", stdout) == NULL) {
               fprintf(stderr,
                       "%s: %s: reopen: %s\n",
                       g.progname, optarg, strerror(errno));
               goto error;
            }
            break;
         }
         case ('h'): {
            g.homedir = optarg;
            break;
         }
         case ('P'): {
            /* Clear password. */
            memset(optarg, 0, strlen(optarg));
            ERRORX("-%c option not supported.\n", ch);
            goto error;
         }
         case ('s'): {
            g.subdatabase = optarg;
            goto error;
         }
         case ('?'):
         default: {
            g.exitcode = usage();
            goto cleanup;
         }
      }
   }
   argc -= optind;
   argv += optind;

   //TODO: Uncomment when DB_SALVAGE,DB_AGGRESSIVE,DB_PRINTABLE,db->verify are implemented.
   /*
   if (g.plaintext) g.recover_flags |= DB_PRINTABLE;

   if (g.subdatabase != NULL && IS_SET_ALL(g.recover_flags, DB_SALVAGE)) {
      if (IS_SET_ALL(g.recover_flags, DB_AGGRESSIVE)) {
         ERRORX("The -s and -R options may not both be specified.\n");
         goto error;
      }
      ERRORX("The -s and -r options may not both be specified.\n");
      goto error;
      
   }
   */

   if (argc != 1) {
      g.exitcode = usage();
      goto cleanup;
   }
   //TODO:  /* Handle possible interruptions/signals. */

   g.database = argv[0];
   if (create_init_env() != 0) goto error;
   if (dump_database() != 0) goto error;
   if (false) {
error:
      g.exitcode = EXIT_FAILURE;
      fprintf(stderr, "%s: Quitting out due to errors.\n", g.progname);
   }
cleanup:
   if (g.dbenv && (retval = g.dbenv->close(g.dbenv, 0)) != 0) {
      g.exitcode = EXIT_FAILURE;
      fprintf(stderr, "%s: %s: dbenv->close\n", g.progname, db_strerror(retval));
   }
   //TODO:  /* Resend any caught signal. */
   if (g.subdatabase)      free(g.subdatabase);

   return g.exitcode;
}

int dump_database()
{
   int retval;

   /* Create a database handle. */
   retval = db_create(&g.db, g.dbenv, 0);
   if (retval != 0) {
      ERROR(retval, "db_create");
      return EXIT_FAILURE;
   }

   /*
   TODO: If/when supporting encryption
   if (g.password && (retval = db->set_flags(db, DB_ENCRYPT))) {
      ERROR(ret, "DB->set_flags: DB_ENCRYPT");
      goto error;
   }
   */
   if (open_database() != 0) goto error;
   if (g.header && dump_header() != 0) goto error;
   if (dump_pairs() != 0) goto error;
   if (g.footer && dump_footer() != 0) goto error;

   if (false) {
error:
      g.exitcode = EXIT_FAILURE;
   }
cleanup:

   if (close_database() != 0) g.exitcode = EXIT_FAILURE;

   return g.exitcode;
}

int usage()
{
   fprintf(stderr,
           "usage: %s [-klNprRV] [-f output_file] [-h home]\n"
           "          [-s subdatabase] db_file\n",
           g.progname);
   return EXIT_FAILURE;
}

int create_init_env()
{
   int retval;
   DB_ENV* dbenv;
   int flags;
   //TODO: Experiments to determine right cache size for tokudb, or maybe command line argument.
   int cache = 1 << 20; /* 1 megabyte */

   retval = db_env_create(&dbenv, 0);
   if (retval) {
      fprintf(stderr, "%s: db_dbenv_create: %s\n", g.progname, db_strerror(retval));
      goto error;
   }
   ///TODO: UNCOMMENT/IMPLEMENT dbenv->set_errfile(dbenv, stderr);
   dbenv->set_errpfx(dbenv, g.progname);
   /*
   TODO: Anything for encryption?
   */

   /* Open the dbenvironment. */
   g.is_private = false;
   //flags = DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_MPOOL | DB_USE_ENVIRON;
   flags = DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_MPOOL; ///TODO: UNCOMMENT/IMPLEMENT | DB_USE_ENVIRON;
   //TODO: Transactions.. SET_BITS(flags, DB_INIT_TXN);
   
   /*
   ///TODO: UNCOMMENT/IMPLEMENT  Notes:  We require DB_PRIVATE
   if (!dbenv->open(dbenv, g.homedir, flags, 0)) goto success;
   */

   /*
   ///TODO: UNCOMMENT/IMPLEMENT 
   retval = dbenv->set_cachesize(dbenv, 0, cache, 1);
   if (retval) {
      ERROR(retval, "DB_ENV->set_cachesize");
      goto error;
   }
   */
   g.is_private = true;
   //TODO: Do we want to support transactions even in single-process mode?
   //Logging is not necessary.. this is read-only.
   //However, do we need to use DB_INIT_LOG to join a logging environment?
   REMOVE_BITS(flags, DB_INIT_LOCK | DB_INIT_LOG | DB_INIT_TXN);
   SET_BITS(flags, DB_CREATE | DB_PRIVATE);

   retval = dbenv->open(dbenv, g.homedir, flags, 0);
   if (retval) {
      ERROR(retval, "DB_ENV->open");
      goto error;
   }
success:
   g.dbenv = dbenv;
   return EXIT_SUCCESS;

error:
   return EXIT_FAILURE;
}

#define DUMP_FLAG(bit, dump)    if (IS_SET_ALL(flags, bit)) printf(dump);

#define DUMP_IGNORED_FLAG(bit, dump)


int dump_header()
{
   u_int32_t flags;
   int retval;
   DB* db = g.db;
   
   assert(g.header);
   printf("VERSION=3\n");
   printf("format=%s\n", g.plaintext ? "print" : "bytevalue");
   //TODO: Uncomment when DB_UNKNOWN + db->get_type are implemented.
   /*assert(g.dbtype == DB_BTREE || (g.dbtype == DB_UNKNOWN && g.opened_dbtype == DB_BTREE));*/
   printf("type=btree\n");
   //TODO: Get page size from db.  Currently tokudb does not support db->get_pagesize.
   printf("db_pagesize=4096\n");
   if (g.subdatabase) {
      printf("subdatabase=");
      outputplaintextstring(g.subdatabase);
      printf("\n");      
   }
   //TODO: Uncomment when db->get_flags is implemented
   /*
   if ((retval = db->get_flags(db, &flags)) != 0) {
      ERROR(retval, "DB->get_flags");
      goto error;
   }
   DUMP_IGNORED_FLAG(DB_CHKSUM,    "chksum=1");
   DUMP_FLAG(        DB_DUP,       "duplicates=1");
   DUMP_IGNORED_FLAG(DB_DUPSORT,   "dupsort=1");
   DUMP_IGNORED_FLAG(DB_RECNUM,    "recnum=1");*/
   printf("HEADER=END\n");
   
   if (ferror(stdout)) goto error;
   return EXIT_SUCCESS;

error:
   return EXIT_FAILURE;
}

int dump_footer()
{
   printf("DATA=END\n");
   if (ferror(stdout)) goto error;

   return EXIT_SUCCESS;
error:
   return EXIT_FAILURE;
}

int open_database()
{
   DB* db = g.db;
   int retval;

   int open_flags = DB_RDONLY;
   //TODO: Transaction auto commit stuff
   //if (TXN_ON(dbenv)) SET_BITS(open_flags, DB_AUTO_COMMIT);

   retval = db->open(db, NULL, g.database, g.subdatabase, g.dbtype, open_flags, 0666);
   if (retval != 0) {
      ERROR(retval, "DB->open: %s", g.database);
      goto error;
   }
   //TODO: Uncomment when DB_UNKNOWN + db->get_type are implemented.
   /*
   retval = db->get_type(db, &g.opened_dbtype);
   if (retval != 0) {
      ERROR(retval, "DB->get_type");
      goto error;
   }
   if (g.opened_dbtype != DB_BTREE) {
      ERRORX("Unsupported db type %d\n", g.opened_dbtype);
      goto error;
   }
   if (g.dbtype != DB_UNKNOWN && g.opened_dbtype != g.dbtype) {
      ERRORX("DBTYPE %d does not match opened DBTYPE %d.\n", g.dbtype, g.opened_dbtype);
      goto error;
   }*/
   return EXIT_SUCCESS;
error:
   fprintf(stderr, "Quitting out due to errors.\n");
   return EXIT_FAILURE;
}

int dump_dbt(DBT* dbt)
{
   char* str;
   uint32_t index;
   
   assert(dbt);
   str = (char*)dbt->data;
   if (g.leadingspace) printf(" ");
   if (dbt->size > 0) {
      assert(dbt->data);
      for (index = 0; index < dbt->size; index++) {
         outputbyte(str[index]);
         if (ferror(stdout)) {
            perror("stdout");
            goto error;
         }
      }
   }
   printf("\n");
   if (false) {
error:
      g.exitcode = EXIT_FAILURE;
   }
   return g.exitcode;
}

int dump_pairs()
{
   int retval;
   size_t length;
   DBT key;
   DBT data;
   int spacech;
   DB* db = g.db;
   DBC* dbc = NULL;

   memset(&key, 0, sizeof(key));
   memset(&data, 0, sizeof(data));

   if ((retval = db->cursor(db, (DB_TXN*)NULL, &dbc, 0)) != 0) {
      ERROR(retval, "DB->cursor");
      goto error;
   }
   while ((retval = dbc->c_get(dbc, &key, &data, DB_NEXT)) == 0) {
      if (dump_dbt(&key) != 0) goto error;
      if (dump_dbt(&data) != 0) goto error;
   }
   if (retval != DB_NOTFOUND) {
      ERROR(retval, "DBC->c_get");
      goto error;
   }
   
   
   if (false) {
error:
      g.exitcode = EXIT_FAILURE;
   }
cleanup:
   if (dbc && (retval = dbc->c_close(dbc)) != 0) {
      ERROR(retval, "DBC->c_close");
      g.exitcode = EXIT_FAILURE;
   }
success:
   return g.exitcode;
}

int close_database()
{
   DB* db = g.db;
   int retval;

   assert(db);
   if ((retval = db->close(db, 0)) != 0) {
      ERROR(retval, "DB->close");
      goto error;
   }
   return EXIT_SUCCESS;
error:
   return EXIT_FAILURE;
}