/* mdb2rec.c - mdb to rec converter.  */

/* Copyright (C) 2010-2022 Jose E. Marchesi */

/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <config.h>

#include <getopt.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <xalloc.h>
#include <gettext.h>
#define _(str) gettext (str)

#include <glib.h>
#include <mdbtools.h>

#include <rec.h>
#include <recutl.h>

/* Forward declarations. */
static void parse_args (int argc, char **argv);
static rec_db_t process_mdb (void);
static rec_rset_t process_table (MdbCatalogEntry *entry);
static char *get_field_name (MdbHandle *mdb,
                             const char *table_name, const char *col_name);
static void get_relationships (MdbHandle *mdb);

/*
 * Data types
 */

struct relationship_s
{
  char *table;
  char *column;
  char *referenced_table;
  char *referenced_column;
};

/*
 * Global variables
 */

char *mdb2rec_mdb_file = NULL;
char *mdb2rec_mdb_table = NULL;
bool mdb2rec_include_system = false;
bool mdb2rec_keep_empty_fields = false;
bool mdb2rec_list_tables = false;
struct relationship_s *relationships;
size_t num_relationships = 0;

/*
 * Command line options management
 */

enum
  {
    COMMON_ARGS,
    SYSTEM_TABLES_ARG,
    KEEP_EMPTY_FIELDS_ARG,
    LIST_TABLES_ARG
  };

static const struct option GNU_longOptions[] =
  {
    COMMON_LONG_ARGS,
    {"system-tables", no_argument, NULL, SYSTEM_TABLES_ARG},
    {"keep-empty-fields", no_argument, NULL, KEEP_EMPTY_FIELDS_ARG},
    {"list-tables", no_argument, NULL, LIST_TABLES_ARG},
    {NULL, 0, NULL, 0}
  };

/*
 * Functions.
 */

void
recutl_print_help (void)
{
  /* TRANSLATORS: --help output, mdb2rec synopsis.
     no-wrap */
  printf (_("\
Usage: mdb2rec [OPTIONS]... MDB_FILE [TABLE]\n"));

  /* TRANSLATORS: --help output, mdb2rec short description.
     no-wrap */
  fputs (_("\
Convert an mdb file into a rec file.\n"), stdout);

  puts ("");
  /* TRANSLATORS: --help output, mdb2rec options.
     no-wrap */
  fputs (_("\
  -s, --system-tables                 include system tables.\n\
  -e, --keep-empty-fields             don't prune empty fields in the rec\n\
                                        output.\n\
  -l, --list-tables                   dump a list of the table names contained\n\
                                        in the mdb file.\n"),
         stdout);

  recutl_print_help_common ();
  puts ("");
  recutl_print_help_footer ();
}

static void
parse_args (int argc,
            char **argv)
{
  int ret;
  char c;

  while ((ret = getopt_long (argc,
                             argv,
                             "sel",
                             GNU_longOptions,
                             NULL)) != -1)
    {
      c = ret;
      switch (c)
        {
          COMMON_ARGS_CASES
        case SYSTEM_TABLES_ARG:
        case 's':
          mdb2rec_include_system = true;
          break;
        case KEEP_EMPTY_FIELDS_ARG:
        case 'e':
          mdb2rec_keep_empty_fields = true;
          break;
        case LIST_TABLES_ARG:
        case 'l':
          mdb2rec_list_tables = true;
          break;
        default:
          exit (EXIT_FAILURE);
        }
    }

  /* Read the name of the mdb file.  */
  if ((argc - optind) > 2)
    {
      recutl_print_help ();
      exit (EXIT_FAILURE);
    }
  else
    {
      mdb2rec_mdb_file = argv[optind++];

      if ((argc - optind) > 0)
        mdb2rec_mdb_table = argv[optind++];
    }
}

static void
get_relationships (MdbHandle *mdb)
{
  char *bound[4];
  MdbTableDef *table;
  size_t i;

  table = mdb_read_table_by_name (mdb,
                                  "MsysRelationships",
                                  MDB_TABLE);
  if ((!table) || (table->num_rows == 0))
    return;

  mdb_read_columns (table);
  for (i = 0; i < 4; i++)
    bound[i] = xmalloc (MDB_BIND_SIZE);

  mdb_bind_column_by_name (table, "szColumn", bound[0], NULL);
  mdb_bind_column_by_name (table, "szObject", bound[1], NULL);
  mdb_bind_column_by_name (table, "szReferencedColumn", bound[2], NULL);
  mdb_bind_column_by_name (table, "szReferencedObject", bound[3], NULL);
  mdb_rewind_table (table);

  num_relationships = table->num_rows;
  relationships = xmalloc (sizeof (struct relationship_s) * num_relationships);

  i = 0;
  while (mdb_fetch_row (table))
    {
      relationships[i].column = xstrdup (bound[0]);
      relationships[i].table = xstrdup (bound[1]);
      relationships[i].referenced_column = xstrdup (bound[2]);
      relationships[i].referenced_table = xstrdup (bound[3]);
      i++;
    }
}

static char *
get_field_name (MdbHandle *mdb,
                const char *table_name,
                const char *col_name)
{
  char *field_name;
  char *referenced_table;
  char *referenced_column;
  size_t i;

  /* If this field is a relationship to other table, build a compound
     field name.  */

  referenced_table = NULL;
  referenced_column = NULL;
  for (i = 0; i < num_relationships; i++)
    {
      if ((strcmp (relationships[i].table, table_name) == 0)
          && (strcmp (relationships[i].column, col_name) == 0))
        {
          referenced_table =
            rec_field_name_normalise (relationships[i].referenced_table);
          if (!referenced_table)
            recutl_fatal (_("failed to normalise record type name %s\n"),
                          relationships[i].referenced_table);

          referenced_column =
            rec_field_name_normalise (relationships[i].referenced_column);
          if (!referenced_column)
            recutl_fatal (_("failed to normalise field name %s\n"),
                          relationships[i].referenced_column);

          break;
        }
    }

  field_name = rec_field_name_normalise (col_name);
  if (!field_name)
    recutl_fatal (_("failed to normalise field name %s\n"),
                  table_name);

  if (referenced_table && referenced_column)
    {
      /* TODO: handle foreign keys.  */
    }

  return field_name;
}

static rec_rset_t
process_table (MdbCatalogEntry *entry)
{
  rec_rset_t rset;
  MdbTableDef *table;
  MdbHandle *mdb;
  size_t i;
  MdbColumn *col;
  char *table_name;
  char *column_name;
  char *field_name;
  char *field_value;
  char **bound_values;
  char *normalised;
  int *bound_lens;
#define TYPE_VALUE_SIZE 256
  char type_value[TYPE_VALUE_SIZE];
  rec_record_t record;
  rec_field_t field;

  mdb = entry->mdb;
  table_name = entry->object_name;
  table = mdb_read_table (entry);

  /* Create the record set.  */
  rset = rec_rset_new ();
  if (!rset)
    recutl_out_of_memory ();

  /* Create the record descriptor and add the %rec: entry.  */
  field_name = rec_field_name_normalise (table_name);
  if (!field_name)
    recutl_fatal (_("failed to normalise record type name %s\n"),
                  table_name);

  record = rec_record_new ();
  field = rec_field_new ("%rec", field_name);
  rec_mset_append (rec_record_mset (record), MSET_FIELD, (void *) field, MSET_ANY);
  free (field_name);

  /* Get the columns of the table.  */
  mdb_read_columns (table);

  /* Loop on the columns.  We will define the key and the types.  */
  for (i = 0; i < table->num_cols; i++)
    {
      col = g_ptr_array_index (table->columns, i);
      column_name = col->name;
      type_value[0] = 0;
      normalised = rec_field_name_normalise (column_name);
      if (!normalised)
        recutl_fatal (_("failed to normalise the field name %s\n"),
                      column_name);

      /* Emit a field type specification.  */
      switch (col->col_type)
        {
        case MDB_BOOL:
          snprintf (type_value, TYPE_VALUE_SIZE,
                    "%s bool", normalised);
          break;
        case MDB_BYTE:
          snprintf (type_value, TYPE_VALUE_SIZE,
                    "%s range 256", normalised);
          break;
        case MDB_INT:
        case MDB_LONGINT:
        case MDB_NUMERIC:
          snprintf (type_value, TYPE_VALUE_SIZE,
                    "%s int", normalised);
          break;
        case MDB_MONEY:
        case MDB_FLOAT:
        case MDB_DOUBLE:
          snprintf (type_value, TYPE_VALUE_SIZE,
                    "%s real", normalised);
          break;
        case MDB_DATETIME:
          snprintf (type_value, TYPE_VALUE_SIZE,
                    "%s date", normalised);
          break;
        case MDB_TEXT:
          if (col->col_size > 0)
            snprintf (type_value, TYPE_VALUE_SIZE,
                      "%s size %d", normalised, col->col_size);
          break;
        case MDB_REPID:
        case MDB_MEMO:
        case MDB_OLE:
        default:
          break;
        }

      if (type_value[0] != 0)
        {
          /* Insert a type field for this column.  */
          field = rec_field_new ("%type", type_value);
          rec_mset_append (rec_record_mset (record),
                           MSET_FIELD, (void *) field, MSET_ANY);
        }
    }

  rec_rset_set_descriptor (rset, record);

  /* Add the records for this table.  */
  mdb_rewind_table (table);

  bound_values = xmalloc (table->num_cols * sizeof(char *));
  bound_lens = xmalloc(table->num_cols * sizeof(int));
  for (i = 0; i < table->num_cols; i++)
    {
      bound_values[i] = xmalloc (MDB_BIND_SIZE);
      mdb_bind_column (table, i+1, bound_values[i], &bound_lens[i]);
    }

  while (mdb_fetch_row (table))
    {
      record = rec_record_new ();
      if (!record)
        recutl_out_of_memory ();

      for (i = 0; i < table->num_cols; i++)
        {
          col = g_ptr_array_index (table->columns, i);

          if (col->col_type == MDB_OLE)
            continue;

          /* Compute the value of the field.  */
          field_value = xmalloc (bound_lens[i] + 1);
          memcpy (field_value, bound_values[i], bound_lens[i]);
          field_value[bound_lens[i]] = '\0';

          if (mdb2rec_keep_empty_fields || (strlen (field_value) > 0))
            {
              /* Create the field and append it into the record.  */
              field = rec_field_new (get_field_name (mdb, table_name, column_name),
                                     field_value);
              if (!field)
                {
                  recutl_fatal (_("invalid field name %s.\n"), column_name);
                }

              rec_mset_append (rec_record_mset (record), MSET_FIELD,
                               (void *) field, MSET_ANY);
            }

          free (field_value);
        }

      rec_record_set_container (record, rset);
      rec_mset_append (rec_rset_mset (rset), MSET_RECORD,
                       (void *) record, MSET_ANY);
    }

  mdb_free_tabledef (table);

  return rset;
}

static rec_db_t
process_mdb (void)
{
  rec_db_t db;
  MdbHandle *mdb;
  MdbCatalogEntry *entry;
  int i;
  char *table_name;

  /* Create the rec database.  */
  db = rec_db_new ();
  if (!db)
    recutl_out_of_memory ();

  /* Initialize libmdb and open the input file.  */
  mdb_init();
  mdb_set_date_fmt ("%Y-%m-%dT%H:%M:%S%z"); /* ISO 8601 */

  mdb = mdb_open (mdb2rec_mdb_file, MDB_NOFLAGS);
  if (!mdb)
    recutl_fatal (_("could not open file %s\n"),
                  mdb2rec_mdb_file);

  /* Read the catalog.  */
  if (!mdb_read_catalog (mdb, MDB_TABLE))
    recutl_fatal (_("file does not appear to be an Access database\n"));

  /* Read relationships from the database.  Relationships in mdb files
     are stored in the MSysRelationships table.  */
  get_relationships (mdb);

  /* Iterate on the catalogs.  */
  for (i = 0; i < mdb->num_catalog; i++)
    {
      entry = g_ptr_array_index (mdb->catalog, i);

      table_name = rec_field_name_normalise (entry->object_name);

      if ((entry->object_type == MDB_TABLE)
          && (mdb_is_user_table (entry) || mdb2rec_include_system)
          && (!mdb2rec_mdb_table || (strcmp (mdb2rec_mdb_table, table_name) == 0)))
        {
          if (mdb2rec_list_tables)
            fprintf (stdout, "%s\n", table_name);
          else
            rec_db_insert_rset (db,
                                process_table (entry),
                                rec_db_size (db));
        }
    }

  return db;
}

int
main (int argc, char *argv[])
{
  int ret;
  rec_db_t db;
  rec_writer_t writer;

  recutl_init ("mdb2rec");

  parse_args (argc, argv);
  db = process_mdb ();

  if (db)
    {
      writer = rec_writer_new (stdout);
      rec_write_db (writer, db);

      rec_writer_destroy (writer);
      rec_db_destroy (db);

      ret = EXIT_SUCCESS;
    }
  else
    ret = EXIT_FAILURE;

  return ret;
}
