/*
    See also:
              https://www.csie.ntu.edu.tw/~b93501005/slide5.pdf
              https://www.mkssoftware.com/docs/man5/symbol.5.asp
              http://www-acaps.cs.mcgill.ca/~hendren/520/2016/slides/symbol.pdf
              https://opensource.apple.com/source/swig/swig-4/swig/Source/Swig/symbol.c.auto.html

 */
#ifndef _SYMTAB_H_
// public interface
#include "symtab.h"
#endif

// See symtab.h for descriptionm

#ifndef _SYMTAB_PRIV_H_
// private section
#include "symtab_priv.h"
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// #define TEST_MAIN 1   // Generate a main() to test the library - set in Makefile

#ifndef TRUE
#define TRUE (0==0)
#define FALSE (0!=0)
#endif

static int debug = FALSE;
int debug_scope = FALSE;

int current_scope = -1;

scope_list *top_level_scope = NULL;
scope_list *base_level_scope = NULL;

static void *Malloc(int n) {
  void *p = malloc(n);
  if (debug) fprintf(stderr, "\nMalloc: %p\n", p);
  return p;
}

static void Free(void *p) {
  if (debug) fprintf(stderr, "\nFree: %p\n", p);
}

// Using 'match' rather than strcmp in the lookup procedures allows us to look up a
// variable in multiple symbol tables.  We also sneak in a wildcard here though it
// is unlikely to ever be very useful with the current interface.

static int match(char *given_name, char *symbol_name) {
  if (given_name == symbol_name) return TRUE; // fast match if using strings found in a string table.
  if (*given_name == '*' && given_name[1] == '\0') return TRUE; // wildcard match. Only "*" supported.
  return strcmp(given_name, symbol_name) == 0;
}

static void destroy_table(generic_table_entry *entry) {
  generic_table_entry *next_entry;
  while (entry != NULL) {
    next_entry = entry->next_entry;
    // destroy_data(entry->entry_data); // This would require a callback... let's leave it to the user for now.
    Free(entry);
    entry = next_entry;
  }
}

static scope_list *new_scope(scope_list *previous_scopes) {
  scope_list *scope = Malloc(sizeof(scope_list));
  scope->level = ++current_scope;
  scope->table = NULL;
  scope->next_scope = previous_scopes;
  if (debug || debug_scope) {fflush(stdout);fprintf(stderr, "\nCreate %s scope %d\n", (previous_scopes == NULL ? "top-level" : "nested"), current_scope);}
  return scope;
}

static void destroy_tables(table_list *table) {
  table_list *next_table;
  while (table != NULL) {
    next_table = table->next_table;
    destroy_table(table->table_data);
    Free(table);
    table = next_table;
  }
}

static void end_scope(scope_list **parent) {
#define scope (*parent)
  scope_list *next_scope;
  if (parent == NULL || scope == NULL) {
    fprintf(stderr, "\nWarning! Asked to destroy NULL scope\n");
    return;
  }
  if (scope->level != current_scope) {
    fprintf(stderr, "\nWarning! Asked to destroy scope level %d, but current scope is %d\n", scope->level, current_scope);
  }
  if (top_level_scope == NULL) {
    top_level_scope = base_level_scope = new_scope(NULL); // Move to scope level 0
    return;
  }
  if (scope != top_level_scope) {
    fprintf(stderr, "\nWarning! Scope being ended (level %d) is not the top-level scope (level %d)!\n", scope->level, top_level_scope->level);
    return;
  }
  if (debug || debug_scope) {fflush(stdout);fprintf(stderr, "\nDestroying scope level %d\n", current_scope);}
  next_scope = scope->next_scope;
  current_scope -= 1;
  destroy_tables(scope->table); scope->table = NULL;
  Free(scope);
  scope = next_scope;
#undef scope
}

static void add_new_table(scope_list *current_scope, char *table_name) {
  table_list *table = Malloc(sizeof(table_list));
  table->table_name = table_name;
  table->table_data = NULL;
  table->next_table = current_scope->table;
  current_scope->table = table;
}

static int add_entry_internal(char *table_name, char *entry_name, DATA entry_data, int top) {
  // If table_name does not exist at this scope, create it.
  if (top_level_scope == NULL) {
    top_level_scope = base_level_scope = new_scope(NULL); // Move to scope level 0
    return FALSE;
  }
  scope_list *s = top_level_scope;
  if (debug && top) {fflush(stdout);fprintf(stderr, "\nAdd %s/%s to Scope: %d\n", table_name, entry_name, s->level);}
  if (s->table != NULL) {
    table_list *t = s->table;
    while (t != NULL) {
      if (debug && top) {fflush(stdout);fprintf(stderr, "  Add to Table: %s\n", t->table_name);}
      if (match(table_name, t->table_name)) {
        // Table exists!
        // add entry to the table in current scope

        // Check for presence:
        generic_table_entry *d = t->table_data;
        if (top) {
          while (d != NULL) {
            if (debug && top) {fflush(stdout);fprintf(stderr, "    Add to Entry: %s\n", d->entry_name);}
            if (match(entry_name, d->entry_name)) {
              // User can warn or error if already present...
              return FALSE;
            }
            d = d->next_entry;
          }
        }

        // Now add:
        d = Malloc(sizeof(generic_table_entry));
        d->entry_name = entry_name;
        d->entry_data = entry_data;
        d->next_entry = t->table_data;
        t->table_data = d;
        return TRUE;
      }
      t = t->next_table;
    }
  }
  // Table does not exist!  Create it!
  add_new_table(top_level_scope, table_name);
  return add_entry_internal(table_name, entry_name, entry_data, FALSE); // Safe to unconditionally add now...
}

// 1) Should be using unicode strings
// 2) DATA is currently an int.  But it might not be.  Ideally we need a DATA2STR() call
//    that takes a DATA object and converts it to a readable string for debugging output.
int add_entry(char *table_name, char *entry_name, DATA entry_data) {
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nadd_entry(%s, %s, %d)\n", table_name, entry_name, entry_data);}
  return add_entry_internal(table_name, entry_name, entry_data, TRUE);
}

// exported, but not included in header file.
DATA lookup_with_scope(char *table_name, char *entry_name, int *scope_level) {
  if (top_level_scope == NULL) {
    top_level_scope = base_level_scope = new_scope(NULL); // Move to scope level 0
    return NO_DATA;
  }
  scope_list *s = top_level_scope;
  while (s != NULL) {
    if (debug || (debug_scope > 1)) {fflush(stdout);fprintf(stderr, "\nLookup %s/%s in Scope: %d\n", table_name, entry_name, s->level);}
    if (s->table != NULL) {
      table_list *t = s->table;
      if (debug || (debug_scope > 1)) {fflush(stdout);fprintf(stderr, "  Lookup %s in Table: %s\n", table_name, t->table_name);}
      while (t != NULL) {
        if (match(table_name, t->table_name)) {
          generic_table_entry *d = t->table_data;
          while (d != NULL) {
            if (debug || (debug_scope > 1)) {fflush(stdout);fprintf(stderr, "    Lookup %s in Entry: %s\n", entry_name, d->entry_name);}
            if (match(entry_name, d->entry_name)) {
              if (scope_level != NULL) *scope_level = s->level;
              return d->entry_data;
            }
            d = d->next_entry;
          }
        }
        t = t->next_table;
      }
    }
    s = s->next_scope;
  }
  return NO_DATA;
}

DATA lookup(char *table_name, char *entry_name) {
  int result;
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nlookup(%s, %s) -> ", table_name, entry_name);}
  result = lookup_with_scope(table_name, entry_name, NULL);
  if (debug_scope) {fflush(stdout);fprintf(stderr, "%d\n", result);}
  return result;
}

int scope_level(char *table_name, char *entry_name) {
  DATA result;
  int scope_level;
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nscope_level(%s, %s)\n", table_name, entry_name);}
  result = lookup_with_scope(table_name, entry_name, &scope_level);
  if (result != NO_DATA) return scope_level;
  return -1;
}

void push_scope_level(void) {
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\npush_scope_level()\n");}
  if (top_level_scope == NULL) {
    top_level_scope = base_level_scope = new_scope(NULL); // Move to scope level 0
    return;
  }
  top_level_scope = new_scope(top_level_scope); // Move to scope level 1
}

void pop_scope_level(void) {
  if (top_level_scope == NULL) {
    top_level_scope = base_level_scope = new_scope(NULL); // Move to scope level 0
    return;
  }
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\npop_scope_level()\n");}
  end_scope(&top_level_scope);
  if (top_level_scope == NULL) base_level_scope = NULL;
}

#ifdef TEST_MAIN
int main(int argc, char **argv) {
  double pi = 3.141592653589793;
  double pi2 = 3.14;
  double *found;

  if ((argc == 2) && (strcmp(argv[1], "-d") == 0)) debug = TRUE;

  push_scope_level(); // Move to scope level 0
  fprintf(stderr, "push scope\n");
  
  // declare any over-ridable extrinsics here...
  push_scope_level(); // Move to scope level 1 - ready for externals, top-level statics, etc.
  fprintf(stderr, "push scope\n");

  // enter a '{' group
  push_scope_level(); // Move to next scope level - inside a '{ ... }' block
  fprintf(stderr, "push scope\n");

  if (!add_entry("floats", "pi", &pi)) fprintf(stderr, "Error: %s already exists in %s in scope %d!\n", "pi", "floats", current_scope);
  else fprintf(stderr, "Added floats/pi at scope %d\n", current_scope);

  // enter a '{' group
  push_scope_level(); // Move to next scope level - inside a '{ ... }' block
  fprintf(stderr, "push scope\n");

  if (!add_entry("floats", "pi", &pi2)) fprintf(stderr, "Error: %s already exists in %s in scope %d!\n", "pi", "floats", current_scope);
  else fprintf(stderr, "Added floats/pi at scope %d\n", current_scope);

  if (!add_entry("floats", "pi", &pi2)) fprintf(stderr, "Error: %s already exists in %s in scope %d!\n", "pi", "floats", current_scope);
  else fprintf(stderr, "Added floats/pi at scope %d\n", current_scope);

  found = lookup("floats", "pi");
  if (found) fprintf(stdout, "      pi in scope %d is %f\n", current_scope, *found);
  else fprintf(stderr, "      floats/pi not found in scope %d\n", current_scope);
  // return from '}' group
  pop_scope_level();
  fprintf(stderr, "pop scope\n");

  found = lookup("floats", "pi");
  if (found) fprintf(stdout, "      pi in scope %d is %f\n", current_scope, *found);
  else fprintf(stderr, "      floats/pi not found in scope %d\n", current_scope);
  // return from '}' group
  pop_scope_level();
  fprintf(stderr, "pop scope\n");

  found = lookup("floats", "pi");
  if (found) fprintf(stdout, "      pi in scope %d is %f\n", current_scope, *found);
  else fprintf(stderr, "      floats/pi not found in scope %d\n", current_scope);
  pop_scope_level();
  fprintf(stderr, "pop scope\n");

  while (current_scope >= 0) {
    pop_scope_level(); // Clean up...
    fprintf(stderr, "pop scope\n");
  }
  
  return 0;
}
#endif
