#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <math.h>
#include <ctype.h>

#include "proto.h"
#include "main.h"
#include "preprocess.h"
#include "evaluate.h"

//#define ALLOW_ARRAYS

#define MAXNESTEDLOOPS        10


typedef struct LOOP {
  U32 startoffset;
  S32 current;
  char name[MAXVARNAMELENGTH+1];
  char *to_expr;
} LOOP;

static LOOP loopinfo[MAXNESTEDLOOPS];
static S32 loopdepth = 0;


typedef struct MACRO {
  char *name, *value;
} MACRO;

static MACRO macros[MAXMACROS];
static S32 macrocount = 0;


static U32 allocated, codesize, ifdefdepth, readoffset;
static char *buffer;


static S32 insert_file(char *filename);
static S32 macro_substitute(char *line, char *out);
static S32 is_macro_start_char(char s);
static S32 is_macro_char(char s);
static S32 insert_line(char *line);
static S32 read_line(char *line);
static void strip_leading_trailing_spaces(char *in, char **out);
static char *get_var_name(char *in, char *name);
static char *get_expression(char *in, char *expr);



S32 preprocessor(char *filename) {

  S32 i;

  buffer = malloc(32*1024);
  if (!buffer) {
    fprintf(stderr, "No room\n");
    return 1;
  }
  allocated = 32*1024;
  codesize = 0;
  ifdefdepth = 0;
  readoffset = 0;

  loopdepth = 0;
  for (i = 0; i < MAXNESTEDLOOPS; i++)
    loopinfo[i].name[0] = '\0';

  return insert_file(filename);
}


S32 end_of_file() {

  if (readoffset >= codesize)    return 1;
  return 0;
}


S32 get_line(char *line) {

  U32 old;

  old = readoffset;
  if (read_line(line))           return 1;

  if (strncmp(line, ":for ", 5) == 0) {
    char name[MAXVARNAMELENGTH], expr1[MAXLINELENGTH], expr2[MAXLINELENGTH], *p;

    // :for var = expr1 to expr2
    // read var
    p = get_var_name(line+5, name);
    if (!p) {
      fprintf(stderr, "Bad ':for' instruction 1\n");
      return 1;
    }
    if (*p != '=') {
      fprintf(stderr, "Bad ':for' instruction 2\n");
      return 1;
    }
    p = get_expression(p+1, expr1);
    if (!p) {
      fprintf(stderr, "Bad ':for' instruction 3\n");
      return 1;
    }
    if (strncmp(p, "to", 2)) {
      fprintf(stderr, "Bad ':for' instruction 4\n");
      return 1;
    }
    p = get_expression(p+2, expr2);
    if (!p) {
      fprintf(stderr, "Bad ':for' instruction 5\n");
      return 1;
    }

    if (create_variable(name))                                  return 1;
    if (evaluate(expr1, &loopinfo[loopdepth].current))          return 1;
    loopinfo[loopdepth].to_expr = malloc(strlen(expr2)+1);
    if (!loopinfo[loopdepth].to_expr) {
      fprintf(stderr, "No room\n");
      return 1;
    }
    strcpy(loopinfo[loopdepth].to_expr, expr2);
    if (set_variable_value(name, loopinfo[loopdepth].current))  return 1;
    loopinfo[loopdepth].startoffset = readoffset;
    strcpy(loopinfo[loopdepth].name, name);
    loopdepth++;
    return get_line(line);

  } else if (strncmp(line, ":next", 5) == 0) {
    S32 to;
    if (loopdepth == 0) {
      fprintf(stderr, "Unmatched ':next' encountered\n");
      return 1;
    }
    loopinfo[loopdepth-1].current++;
    if (set_variable_value(loopinfo[loopdepth-1].name,
                           loopinfo[loopdepth-1].current))  return 1;
    if (evaluate(loopinfo[loopdepth-1].to_expr, &to))       return 1;
    if (loopinfo[loopdepth-1].current <= to) {
      readoffset = loopinfo[loopdepth-1].startoffset;
      return get_line(line);

    } else {
      loopdepth--;
      if (destroy_variable(loopinfo[loopdepth].name))   return 1;
      free(loopinfo[loopdepth].to_expr);
      loopinfo[loopdepth].name[0] = '\0';
      return get_line(line);
    }

  } else if (strncmp(line, ":print ", 7) == 0) {
    char *p;

    strip_leading_trailing_spaces(line+7, &p);
    if (*p == '"') {
      char *end;

      end = p + strlen(p) - 1;
      if (*end != '"') {
        fprintf(stderr, "Bad ':print'\n");
        return 1;
      }
      *end = '\0';
      printf("%s\n", p+1);

    } else {
      S32 v;
      if (evaluate(p, &v))              return 1;
      printf("%d\n", v);
    }
    return get_line(line);

  } else if (strncmp(line, ":endif", 6) == 0) {
    return get_line(line);

  } else if (strncmp(line, ":var ", 5) == 0) {
    char *p, name[MAXVARNAMELENGTH+1];
    S32 n;

    p = line+5;
    n = 0;
    do {
      p = get_var_name(p, name);
      if (!p) {
        fprintf(stderr, "Syntax error in ':var'\n");
        return 1;
      }
      if (create_variable(name))  return 1;
      n++;
      if ((*p) && (*p != ',')) {
        fprintf(stderr, "Syntax error in ':var'\n");
        return 1;
      }
      if (*p == ',') {
        p++;
        while (*p == ' ')  p++;
        if (!*p) {
          fprintf(stderr, "Syntax error in ':var'\n");
          return 1;
        }
      }
    } while (*p);

    if (!n) {
      fprintf(stderr, "Syntax error in ':var'\n");
      return 1;
    }

    return get_line(line);

  } else if (strncmp(line, ":if ", 4) == 0) {
    S32 value;
    if (evaluate(line+4, &value)) {
      fprintf(stderr, "Failed to evaluate '%s'\n", line+4);
      return 1;
    }
    if (!value) {
      S32 ifdepth;
      ifdepth = 1;
      do {
        if (read_line(line))   return 1;
        if (strncmp(line, ":if ", 4) == 0)
          ifdepth++;
        if (strncmp(line, ":endif", 6) == 0)
          ifdepth--;
      } while (ifdepth);
    }
    return get_line(line);

  } else if (line[0] == ':') {
    char name[MAXVARNAMELENGTH+1], *p;
    S32 value, varvalue;
    S32 error;

    p = get_var_name(line+1, name);
    if (!p) {
      fprintf(stderr, "Syntax error: '%s'\n", line);
      return 1;
    }
    if (read_variable_value(name, &varvalue))  return 1;
    while (*p == ' ')  p++;
    error = 0;
    if ((p[0] == '+') && (p[1] == '=')) {
      error = evaluate(p+2, &value);
      varvalue += value;
    } else if ((p[0] == '-') && (p[1] == '=')) {
      error = evaluate(p+2, &value);
      varvalue -= value;
    } else if ((p[0] == '*') && (p[1] == '=')) {
      error = evaluate(p+2, &value);
      varvalue *= value;
    } else if ((p[0] == '/') && (p[1] == '=')) {
      error = evaluate(p+2, &value);
      if (value == 0) {
        fprintf(stderr, "Division by zero: %s equals 0\n", p+2);
        return 1;
      }
      varvalue /= value;
    } else if (p[0] == '=') {
      error = evaluate(p+1, &varvalue);
    }
    if (error) {
      fprintf(stderr, "Syntax error: '%s'\n", line);
      return 1;
    }
    if (set_variable_value(name, varvalue))  return 1;
    return get_line(line);

  } else if (line[0] == '.') {
    fprintf(stderr, "Lines are not allowed to start with a '.'\n");
    return 1;
  }

  return 0;
}


S32 find_macro(char *name) {

  S32 i;
  char *p;

  while (isspace(name[0]))     name++;
  p = name;
  while (is_macro_char(*p))  p++;
  *p = '\0';

  for (i = 0; i < macrocount; i++)
     if (strcmp(macros[i].name, name) == 0)  return i;

  return -1;
}

// --------------------------------------------------------------------


S32 is_macro_start_char(char s) {

  if (isalpha(s) || (s == '_'))     return 1;
  return 0;
}


S32 is_macro_char(char s) {

  if (isalpha(s) || isdigit(s) || (s == '_'))     return 1;
  return 0;
}


S32 insert_file(char *filename) {

  FILE *fh;
  char linep[MAXLINELENGTH];

  fh = fopen(filename, "r");
  if (!fh) {
    fprintf(stderr, "Failed to open file '%s'\n", filename);
    return 1;
  }

  do {
    if (fgets(linep, MAXLINELENGTH, fh)) {
      S32 i;
      char *line;

      // convert TAB, linefeed etc to normal spaces
      line = linep;
      while ((*line) && (*line < ' '))  line++;
      for(i = 0; i < strlen(line); i++)
        if (line[i] < ' ')  line[i] = ' ';

      if (line[0] == '#') {
        if (strncmp(line, "#define ", 8) == 0) {
          if (!ifdefdepth) {
            char temp[MAXLINELENGTH], *value, *name;

            name = line+8;
            while (*name == ' ')        name++;     // skip to macro name
            value = name;
            while (is_macro_char(*value))  value++; // skip to end of macro name
            *value++ = '\0';                        // terminate name
            while (*value == ' ')        value++;   // skip to macro name
            if (macro_substitute(value, temp))         return 1;
            if (define_macro(name, temp))              return 1;
          }

        } else if (strncmp(line, "#undef ", 7) == 0) {
          if (!ifdefdepth) {
            S32 mi;
            mi = find_macro(line+7);
            if (mi >= 0) {
              if (mi != macrocount-1) {
                macros[mi].name  = macros[macrocount-1].name;
                macros[mi].value = macros[macrocount-1].value;
              }
              macrocount--;
            }
          }

        } else if (strncmp(line, "#ifdef ", 7) == 0) {
          if (ifdefdepth)
            ifdefdepth++;
          else {
            if (find_macro(line+7) < 0)  ifdefdepth++;
          }

        } else if (strncmp(line, "#else", 5) == 0) {
          if (ifdefdepth == 1)
            ifdefdepth = 0;
          else if (ifdefdepth == 0)
            ifdefdepth = 1;

        } else if (strncmp(line, "#ifndef ", 8) == 0) {
          if (ifdefdepth)
            ifdefdepth++;
          else {
            if (find_macro(line+8) >= 0)  ifdefdepth++;
          }

        } else if (strncmp(line, "#endif", 6) == 0) {
          if (ifdefdepth)  ifdefdepth--;

        } else if (strncmp(line, "#include ", 9) == 0) {
          if (!ifdefdepth) {
            char *filename;

            strip_leading_trailing_spaces(line+9, &filename);
            if (insert_file(filename)) {
              fclose(fh);
              return 1;
            }
          }

        } else if (!ifdefdepth) {
          if (insert_line(line)) {
            fclose(fh);
            return 1;
          }
        }

      } else if (!ifdefdepth) {
        char output[MAXLINELENGTH];

        if (macro_substitute(line, output)) {
          fclose(fh);
          return 1;
        }
        if (insert_line(output)) {
          fclose(fh);
          return 1;
        }
      }

    } else {
      fclose(fh);
      return 0;
    }
  } while (fh);

  return 1;
}


S32 insert_line(char *line) {

  S32 len;

  len = strlen(line)+1;
  if (codesize + len >= allocated)  {
    char *newbuffer;
    newbuffer = realloc(buffer, allocated + 32*1024);
    if (!newbuffer)  return 1;
    allocated += 32*1024;
    buffer = newbuffer;
  }
  memcpy(buffer+codesize, line, len);
  codesize += len;

  return 0;
}


S32 define_macro(char *name, char *value) {

  S32 mi;
  char *namep, *valuep;

  if (macrocount == MAXMACROS) {
    fprintf(stderr, "Too many macros\n");
    return 1;
  }

  namep = malloc(strlen(name)+1);
  if (!namep) {
    fprintf(stderr, "No room for macro\n");
    return 1;
  }
  valuep = malloc(strlen(value)+1);
  if (!valuep) {
    free(namep);
    fprintf(stderr, "No room for macro\n");
    return 1;
  }

  strcpy(namep, name);
  strcpy(valuep, value);
  if (*valuep) {
    char *end;

    end = valuep + strlen(valuep) - 1;
    while (*end == ' ')   *end-- = '\0';
  }

  mi = find_macro(namep);
  if (mi != -1) {
    // macro already defined
    if (strcmp(valuep, macros[mi].value) == 0)  return 0;
    fprintf(stderr, "Differing redefinition of macro '%s'\n", namep);
    return 1;
  }

  macros[macrocount].name = namep;
  macros[macrocount].value = valuep;

  macrocount++;
  return 0;
}


S32 macro_substitute(char *line, char *out) {

  S32 rd, wr, len, checkmacro;

  len = strlen(line);
  checkmacro = 1;
  wr = 0;
  for (rd = 0; rd < len; rd++) {
    // don't do macro-substitution inside ""
    if (line[rd] == '"') {
      S32 quotes;
      out[wr++] = line[rd++];
      quotes = 1;
      while (quotes && (line[rd])) {
        if (line[rd] == '"')  quotes = 0;
        out[wr++] = line[rd++];
      }
    }
    if ((checkmacro) && (is_macro_start_char(line[rd]))) {
      S32 m;
      for (m = 0; m < macrocount; m++) {
        char *macro, *value;
        S32 len;
        macro = macros[m].name;
        value = macros[m].value;
        len = strlen(macro);
        if ((strncmp(line+rd, macro, len) == 0) && (!is_macro_char(line[rd+len]))) {
          if (wr + strlen(value) > MAXLINELENGTH-1) {
            fprintf(stderr, "Macro-substituted line is too long\n");
            return 1;
          }
          strcpy(out+wr, value);
          wr += strlen(value);
          rd += strlen(macro);
          m = macrocount+1;         // abort
          rd--;
        }
      }
      if (m == macrocount)  out[wr++] = line[rd];
      checkmacro = 0;

    } else {
      if (!is_macro_char(line[rd]))
        checkmacro = 1;
      else
        checkmacro = 0;
      out[wr++] = line[rd];
    }
  }
  out[wr] = '\0';

  return 0;
}


S32 read_line(char *line) {


  if (readoffset >= codesize)    return 1;
  strcpy(line, buffer+readoffset);
  readoffset += strlen(line)+1;

/*  if (*line) {      // strip trailing white-spaces
    S32 p;

    p = strlen(line)-1;
    while ((p > 0) && (line[p] == ' '))  p--;
    line[p+1] = '\0';
  } */
  return 0;
}


void strip_leading_trailing_spaces(char *in, char **out) {

  while (*in == ' ')  in++;
  *out = in;
  if (!*in)                   return;

  in += strlen(in)-1;
  while (*in == ' ')  *in = '\0', in--;
}


char *get_var_name(char *in, char *name) {

  S32 len;

  while (*in == ' ')   in++;
  if (!is_macro_start_char(*in))  return NULL;

  len = 0;
  while ((len < MAXVARNAMELENGTH) && (is_macro_char(*in)))
    name[len++] = *in++;
  if (len > MAXVARNAMELENGTH)  return NULL;
  name[len] = '\0';

  while (*in == ' ')  in++;

  return in;
}


char *get_expression(char *in, char *expr) {

  S32 bm, len, done;

  bm = 0;
  len = 0;
  done = 0;
  do {
    if (*in == ' ') {
      in++;
      if ((len) && (!bm))     done = 1;
    } else if (*in == '(') {
      expr[len++] = *in++;
      bm++;
    } else if (*in == ')') {
      expr[len++] = *in++;
      bm--;
    } else
      expr[len++] = *in++;

    if (!*in)                 done = 1;
    if ((bm) && (len))        done = 1;
  } while (!done);

  if (bm) {
    fprintf(stderr, "Bad expression, or unmatched brackets\n");
    return NULL;
  }
  expr[len] = '\0';
  while (*in == ' ')  in++;

  return in;
}
