#include "json.h"
#include <assert.h>
#include <math.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const char* jsonErrorString(jsonResult_t res) {
  switch (res) {
  case jsonSuccess:
    return "jsonSuccess";
  case jsonFileError:
    return "jsonFileError";
  case jsonUnknownStateError:
    return "jsonUnknownStateError";
  case jsonEmptyStateError:
    return "jsonEmptyStateError";
  case jsonExpectedNonNoneStateError:
    return "jsonExpectedNonNoneStateError";
  case jsonMemoryError:
    return "jsonMemoryError";
  case jsonStringOverflowError:
    return "jsonStringOverflowError";
  case jsonStringBadChar:
    return "jsonStringBadChar";
  case jsonLockError:
    return "jsonLockError";
  default:
    return "unknown json error";
  }
}

// We use these statics to mantain a stack of states where we are writing.
typedef struct jsonFileOutput {
  jsonState_t* states;
  size_t state_cap; // Allocated stack capacity
  size_t state_n;   // # of items in the stack.
  FILE* fp;
  pthread_mutex_t mutex;
} jsonFileOutput;

jsonResult_t jsonInitFileOutput(jsonFileOutput** jfo, const char* outfile) {
  jsonFileOutput* new_jfo = (jsonFileOutput*)malloc(sizeof(jsonFileOutput));
  if (new_jfo == NULL) {
    return jsonMemoryError;
  }
  if (pthread_mutex_init(&new_jfo->mutex, NULL) != 0) {
    free(new_jfo);
    *jfo = 0;
    return jsonLockError;
  }
  new_jfo->states = NULL;
  new_jfo->state_cap = 0;
  new_jfo->state_n = 0;
  new_jfo->fp = fopen(outfile, "w");
  if (new_jfo->fp == NULL) {
    free(new_jfo);
    *jfo = 0;
    return jsonFileError;
  }
  *jfo = new_jfo;
  return jsonSuccess;
}

jsonResult_t jsonNewline(jsonFileOutput* jfo) {
  fprintf(jfo->fp, "\n");
  return jsonSuccess;
}

jsonResult_t jsonFlushOutput(jsonFileOutput* jfo) {
  fflush(jfo->fp);
  return jsonSuccess;
}

jsonResult_t jsonLockOutput(jsonFileOutput* jfo) {
  if (pthread_mutex_lock(&jfo->mutex) != 0) {
    return jsonLockError;
  }
  return jsonSuccess;
}

jsonResult_t jsonUnlockOutput(jsonFileOutput* jfo) {
  if (pthread_mutex_unlock(&jfo->mutex) != 0) {
    return jsonLockError;
  }
  return jsonSuccess;
}

jsonResult_t jsonFinalizeFileOutput(jsonFileOutput* jfo) {
  // Really should probably complain if we aren't in a valid state

  if (pthread_mutex_destroy(&jfo->mutex) != 0) {
    free(jfo);
    return jsonLockError;
  }
  if (jfo->states != NULL) {
    free(jfo->states);
  }
  jfo->states = NULL;
  jfo->state_cap = 0;
  jfo->state_n = 0;
  if (jfo->fp) {
    fclose(jfo->fp);
    jfo->fp = 0;
  }

  free(jfo);
  return jsonSuccess;
}

static int utf8copy(unsigned char* out, int out_lim, const unsigned char* in) {
  int copy_len;
  if ((in[0] & 0xE0) == 0xC0) {
    // 2-byte sequence
    if ((in[1] & 0xC0) != 0x80 || out_lim < 2) {
      return 0;
    }
    copy_len = 2;
  } else if ((in[0] & 0xF0) == 0xE0) {
    // 3-byte sequence
    if ((in[1] & 0xC0) != 0x80 || (in[2] & 0xC0) != 0x80 || out_lim < 3) {
      return 0;
    }
    copy_len = 3;
  } else if ((in[0] & 0xF8) == 0xF0) {
    // 4-byte sequence
    if ((in[1] & 0xC0) != 0x80 || (in[2] & 0xC0) != 0x80 || (in[3] & 0xC0) != 0x80 || out_lim < 4) {
      return 0;
    }
    copy_len = 4;
  } else {
    // Invalid start byte
    return 0;
  }

  for (int i = 0; i < copy_len; ++i) {
    out[i] = in[i];
  }

  return copy_len;
}

// This tries to sanitize/quote a string from 'in' into 'out',
// assuming 'out' has length 'lim'.  We mainly quote ",/,\,\t,\n, and
// bail if we encounter non-printable stuff or non-ASCII stuff.
// 'in' should be null-terminated, of course.
//
// We return false if we were not able to copy all of 'in', either for
// length reasons or for unhandled characters.
static jsonResult_t sanitizeJson(unsigned char out[], int lim, const unsigned char* in) {
  int c = 0;
  while (*in) {
    if (c + 1 >= lim) {
      out[c] = 0;
      return jsonStringOverflowError;
    }
    switch (*in) {
    case '"':
    case '\\':
    case '/':
    case '\t':
    case '\n':
      if (c + 2 > lim) {
        out[c] = 0;
        return jsonStringOverflowError;
      }

      out[c++] = '\\';
      if (*in == '\n') {
        out[c++] = 'n';
      } else if (*in == '\t') {
        out[c++] = 't';
      } else {
        out[c++] = *in;
      }
      ++in;
      break;
    default:
      if (*in <= 0x1F) {
        out[c] = 0;
        return jsonStringBadChar;
      } else if (*in <= 0x7F) {
        out[c++] = *in;
        ++in;
      } else {
        const int utf8len = utf8copy(out + c, lim - c - 1, in);
        if (utf8len == 0) {
          out[c] = 0;
          return jsonStringBadChar;
        }
        c += utf8len;
        in += utf8len;
      }
      break;
    }
  }
  out[c] = 0;
  return jsonSuccess;
}

static size_t max(size_t a, size_t b) {
  if (a < b) {
    return b;
  }
  return a;
}

// Push state onto the state stack. Reallocate for extra storage if needed.
// Because JSON_NONE is a pseudo-state, don't allow it to be pushed.
static jsonResult_t jsonPushState(jsonFileOutput* jfo, jsonState_t state) {
  if (state == JSON_NONE) {
    return jsonExpectedNonNoneStateError;
  }
  if (jfo->state_cap <= (jfo->state_n + 1)) {
    jfo->state_cap = max((size_t)16, jfo->state_cap * 2);
    jfo->states = (jsonState_t*)realloc(jfo->states, sizeof(jsonState_t) * jfo->state_cap);
    if (jfo->states == 0) {
      return jsonMemoryError;
    }
  }
  jfo->states[jfo->state_n++] = state;
  return jsonSuccess;
}

// Return the current state at the top of the stack
static jsonState_t jsonCurrState(const jsonFileOutput* jfo) {
  if (jfo->state_n == 0) {
    return JSON_NONE;
  }
  return jfo->states[jfo->state_n - 1];
}

// Replace the stack with state (equivalent to a pop & push if stack is not empty)
static jsonResult_t jsonReplaceState(jsonFileOutput* jfo, jsonState_t state) {
  if (state == JSON_NONE) {
    return jsonExpectedNonNoneStateError;
  }
  if (jfo->state_n == 0) {
    return jsonEmptyStateError;
  }
  jfo->states[jfo->state_n - 1] = state;
  return jsonSuccess;
}

// Pop the top state off the stack, or return that the state is empty
static jsonState_t jsonPopState(jsonFileOutput* jfo) {
  if (jfo->state_n == 0) {
    return JSON_NONE;
  }
  return jfo->states[--jfo->state_n];
}

// Emit a key and separator. Santize the key.
// This is only acceptable if the top state is an object
// Emit a ',' separator of we aren't the first item.
jsonResult_t jsonKey(jsonFileOutput* jfo, const char* name) {
  switch (jsonCurrState(jfo)) {
  case JSON_OBJECT_EMPTY:
    jsonReplaceState(jfo, JSON_OBJECT_SOME);
    break;
  case JSON_OBJECT_SOME:
    fprintf(jfo->fp, ",");
    break;
  default:
    return jsonUnknownStateError;
  }
  unsigned char tmp[2048];
  const jsonResult_t res = sanitizeJson(tmp, sizeof(tmp), (const unsigned char*)name);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "\"%s\":", tmp);
  jsonPushState(jfo, JSON_KEY);
  return jsonSuccess;
}

// Helper function for inserting values.
// Only acceptable after keys, top-level, or in lists.
// Emit preceeding ',' if in a list and not first item.
static jsonResult_t jsonValHelper(jsonFileOutput* jfo) {
  switch (jsonCurrState(jfo)) {
  case JSON_LIST_EMPTY:
    jsonReplaceState(jfo, JSON_LIST_SOME);
    break;
  case JSON_LIST_SOME:
    fprintf(jfo->fp, ",");
    break;
  case JSON_KEY:
    jsonPopState(jfo);
    break;
  case JSON_NONE:
    break;
  default:
    return jsonUnknownStateError;
  }
  return jsonSuccess;
}

// Start an object
jsonResult_t jsonStartObject(jsonFileOutput* jfo) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "{");
  return jsonPushState(jfo, JSON_OBJECT_EMPTY);
}

// Close an object
jsonResult_t jsonFinishObject(jsonFileOutput* jfo) {
  switch (jsonPopState(jfo)) {
  case JSON_OBJECT_EMPTY:
  case JSON_OBJECT_SOME:
    break;
  default:
    return jsonUnknownStateError;
  }
  fprintf(jfo->fp, "}");
  return jsonSuccess;
}

// Start a list
jsonResult_t jsonStartList(jsonFileOutput* jfo) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "[");
  return jsonPushState(jfo, JSON_LIST_EMPTY);
}

// Close a list
jsonResult_t jsonFinishList(jsonFileOutput* jfo) {
  switch (jsonPopState(jfo)) {
  case JSON_LIST_EMPTY:
  case JSON_LIST_SOME:
    break;
  default:
    return jsonUnknownStateError;
  }
  fprintf(jfo->fp, "]");
  return jsonSuccess;
}

// Write a null value
jsonResult_t jsonNull(jsonFileOutput* jfo) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "null");
  return jsonSuccess;
}

// Write a (sanititzed) string
jsonResult_t jsonStr(jsonFileOutput* jfo, const char* str) {
  if (str == NULL) {
    jsonNull(jfo);
    return jsonSuccess;
  }
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  unsigned char tmp[2048];
  const jsonResult_t san_res = sanitizeJson(tmp, sizeof(tmp), (const unsigned char*)str);
  if (san_res != jsonSuccess) {
    return san_res;
  }
  fprintf(jfo->fp, "\"%s\"", tmp);
  return jsonSuccess;
}

// Write a bool as "true" or "false" strings.
jsonResult_t jsonBool(jsonFileOutput* jfo, bool val) {
  return jsonStr(jfo, val ? "true" : "false");
}

// Write an integer value
jsonResult_t jsonInt(jsonFileOutput* jfo, const int val) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "%d", val);
  return jsonSuccess;
}

// Write an integer value
jsonResult_t jsonUint32(jsonFileOutput* jfo, const uint32_t val) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "%u", val);
  return jsonSuccess;
}


// Write an integer value
jsonResult_t jsonUint64(jsonFileOutput* jfo, const uint64_t val) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "%lu", val);
  return jsonSuccess;
}

// Write a size_t value
jsonResult_t jsonSize_t(jsonFileOutput* jfo, const size_t val) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  fprintf(jfo->fp, "%zu", val);
  return jsonSuccess;
}

// Write a double value
jsonResult_t jsonDouble(jsonFileOutput* jfo, const double val) {
  const jsonResult_t res = jsonValHelper(jfo);
  if (res != jsonSuccess) {
    return res;
  }
  if (val != val) {
    fprintf(jfo->fp, "\"nan\"");
  } else {
    fprintf(jfo->fp, "%lf", val);
  }
  return jsonSuccess;
}

#ifdef DO_JSON_TEST
// compile with
// gcc json.cc -Iinclude/ -DDO_JSON_TEST -o json_test
// run with:
// ./json_test
// if something fails, it will print out the error
// if it all works, print out "output matches reference"
#define JSONCHECK(expr)                                                                            \
  do {                                                                                             \
    const jsonResult_t res = (expr);                                                               \
    if (res != jsonSuccess) {                                                                      \
      fprintf(stderr, "jsonError: %s\n", jsonErrorString(res));                                    \
      exit(1);                                                                                     \
    }                                                                                              \
  } while (0)

int main() {

  const char refstr[] =
      "{\"number\":123,\"utfstring\":\"∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ "
      "¬β = ¬(¬α ∨ β),\",\"list\":[\"true\",null,9423812381231,3123111,0.694234]}";

  jsonFileOutput* jfo;
  JSONCHECK(jsonInitFileOutput(&jfo, "test.json"));
  JSONCHECK(jsonStartObject(jfo));
  JSONCHECK(jsonKey(jfo, "number"));
  JSONCHECK(jsonInt(jfo, 123));
  JSONCHECK(jsonKey(jfo, "utfstring"));
  JSONCHECK(
      jsonStr(jfo, "∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i), ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β),"));
  JSONCHECK(jsonKey(jfo, "list"));
  JSONCHECK(jsonStartList(jfo));
  JSONCHECK(jsonBool(jfo, true));
  JSONCHECK(jsonNull(jfo));
  JSONCHECK(jsonUint64(jfo, 9423812381231ULL));
  JSONCHECK(jsonSize_t(jfo, 3123111));
  JSONCHECK(jsonDouble(jfo, 0.69423413));
  JSONCHECK(jsonFinishList(jfo));
  JSONCHECK(jsonFinishObject(jfo));
  JSONCHECK(jsonFinalizeFileOutput(jfo));

  FILE* fp = fopen("test.json", "r");

  const size_t reflen = sizeof(refstr) / sizeof(char);

  char buffer[reflen];

  fread(buffer, sizeof(char), reflen, fp);

  fclose(fp);

  if (memcmp(buffer, refstr, reflen) == 0) {
    printf("output matches reference\n");
  } else {
    printf("output    %s\nreference %s\n", buffer, refstr);
    return 1;
  }

  return 0;
}

#endif
