// DISCOVERED A BUG IN SCOPE MANAGEMENT!

// creating and deleting a nested scope is accidentally deleting the parent!

// currently working on a fix.

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

 */

// public interface
#include "flex.h"
#include "symtab.h"

// DATA is also an int.
typedef int StrpoolIDX; // An index into the string pool which can be used in place of a wchar * where appropriate.
#define pooltowstr(S) pooltowstr_inner(S, __FILE__, __LINE__)
extern wchar_t *pooltowstr_inner(StrpoolIDX p, const char *file, const int line);

// See symtab.h for description

// private section
#include "symtab_priv.h"

#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 = 0;

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 int wmatch(wchar_t *given_name, wchar_t *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 wcscmp(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
}

// types TableIDX and table_list declared in our header
DECLARE(anon_tableRA, table_list, 1000);  // array of records, not pointers to records.
#define  anon_table(x)  READ(x, anon_tableRA, table_list)
#define _anon_table(x) WRITE(x, anon_tableRA, table_list)
TableIDX next_free_anon_table = 0;

int anonymous_table(void) {
  int result = next_free_anon_table++;
  if (debug || debug_scope) {fflush(stdout);fprintf(stderr, "\nCreating anonymous_table %d\n", result);}
  // these two are fixed for anonymous tables.
  _anon_table(result).table_name = NULL; // anonymous
  _anon_table(result).next_table = NULL;

  _anon_table(result).table_data = NULL; // generic_table_entry
  
  return result;
}

// This is primarily for record fields but written somewhat generically at the top level as this part of the code is target language independent.
// The procedure name is a bit verbose but the shorter names that I could think of were more easily confused with scoped tables.

int add_entry_to_anonymous_table(TableIDX tableIDX,  wchar_t *entry_name, DATA entry_data) { // Adds OK, TRUE - duplicate, FALSE.
  // Check for presence:
  generic_table_entry *d = anon_table(tableIDX).table_data;

  while (d != NULL) {
    if (debug || debug_scope > 1) {
      fflush(stdout);
      fprintf(stderr,
              "    add_entry_name_to_anonymous_table(%d, %ls, %d):  Comparing against  d->entry_name=%ls\n",
              tableIDX, entry_name, entry_data, d->entry_name);
    }
    if (wmatch(entry_name, d->entry_name)) {
      // User can warn or error if already present...
      if (debug || debug_scope > 1) {
        fflush(stdout);
        fprintf(stderr, "    Entry name \"%ls\" already in anonymous table %d\n", entry_name, tableIDX);
      }
      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 = anon_table(tableIDX).table_data; // list is reversed but we don't really care since we never take advantage of the list format.

  _anon_table(tableIDX).table_data = d;
  
  if (debug || debug_scope > 1) {
    fflush(stdout);
    fprintf(stderr, "    Returning TRUE: Entry name \"%ls\" is not already in table %d\n", entry_name, tableIDX);
  }

  return TRUE;
}

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, wchar_t *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 || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "\nAdd table_name=%s/entry_name=%ls to Scope: s->level=%d\n", table_name, entry_name, s->level);}
  if (s->table != NULL) {
    table_list *t = s->table;
    while (t != NULL) {
      if ((debug || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "  Add table_name=%s. Comparing against t->table_name=%s\n", table_name, 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 || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "    Add entry_name=%ls. Comparing against  d->entry_name=%ls\n", entry_name, d->entry_name);}
            if (wmatch(entry_name, d->entry_name)) {
              // User can warn or error if already present...
              if ((debug || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "    Returning FALSE: Name %ls already in table %s\n", entry_name, table_name);}
              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;
        if ((debug || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "    Returning TRUE: Name %ls is not already in table %s\n", entry_name, table_name);}      return TRUE;
      }
      t = t->next_table;
    }
  }
  // Table does not exist!  Create it!
  if ((debug || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "    Table %s did not exist, so creating it now.\n", table_name);}
  add_new_table(top_level_scope, table_name);
  if ((debug || debug_scope > 1) /*&& top*/) {fflush(stdout);fprintf(stderr, "    Retrying add entry %ls in newly-added table %s\n", entry_name, 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, wchar_t *entry_name, DATA entry_data) {
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nadd_entry(table_name=%s, entry_name=%ls, entry_data=%d)\n", table_name, entry_name, entry_data);}
  return add_entry_internal(table_name, entry_name, entry_data, TRUE);
}

int add_entry_by_strpoolidx(char *table_name, DATA entry_name_strpoolidx, DATA entry_data) {
  wchar_t *entry_name = pooltowstr(entry_name_strpoolidx);
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nadd_entry_by_strpoolidx(table_name=%s, entry_name=%ls, entry_data=%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, wchar_t *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/%ls 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: checking for table %s against %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: checking for entry %ls against %ls\n", entry_name, d->entry_name);}
            if (wmatch(entry_name, d->entry_name)) {
              if (scope_level != NULL) *scope_level = s->level;
              if (debug || (debug_scope > 1)) {fflush(stdout);fprintf(stderr, "    Lookup: found entry %ls in table %s at scope level %d.  Returning data %d\n", entry_name, table_name, s->level, d->entry_data);}
              return d->entry_data;
            }
            d = d->next_entry;
          }
        }
        t = t->next_table;
      }
    }
    s = s->next_scope;
  }
  return NO_DATA;
}

DATA lookup(char *table_name, wchar_t *entry_name) {
  int result;
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nlookup(table_name=%s, entry_name=%ls) -> ", 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;
}

DATA lookup_by_strpoolidx(char *table_name, DATA entry_name_strpoolidx) {
  int result;
  wchar_t *entry_name = pooltowstr(entry_name_strpoolidx);
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nlookup_by_strpoolidx(table_name=%s, entry_name=%ls) -> \n", table_name, entry_name);}
  result = lookup_with_scope(table_name, entry_name, NULL);
  if (debug_scope) {fflush(stdout);fprintf(stderr, "-> lookup_by_strpoolidx result was %d\n", result);}
  return result;
}

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

int scope_level_by_strpoolidx(char *table_name, DATA entry_name_strpoolidx) {
  DATA result;
  int scope_level;
  wchar_t *entry_name = pooltowstr(entry_name_strpoolidx);
  if (debug_scope) {fflush(stdout);fprintf(stderr, "\nscope_level_by_strpoolidx(table_name=%s, entry_name%ls)\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 overridable 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
