#include <string.h>
#include <stdint.h>
#include <time.h>

#include "erl_nif.h"

#include "buffer.h"
#include "document.h"
#include "html.h"

#define OUTPUT_UNIT 128

typedef struct {
  ERL_NIF_TERM atom_autolink;
  ERL_NIF_TERM atom_disable_indented_code;
  ERL_NIF_TERM atom_escape;
  ERL_NIF_TERM atom_fenced_code;
  ERL_NIF_TERM atom_hard_wrap;
  ERL_NIF_TERM atom_highlight;
  ERL_NIF_TERM atom_math;
  ERL_NIF_TERM atom_math_explicit;
  ERL_NIF_TERM atom_no_intra_emphasis;
  ERL_NIF_TERM atom_quote;
  ERL_NIF_TERM atom_skip_html;
  ERL_NIF_TERM atom_space_headers;
  ERL_NIF_TERM atom_strikethrough;
  ERL_NIF_TERM atom_superscript;
  ERL_NIF_TERM atom_tables;
  ERL_NIF_TERM atom_true;
  ERL_NIF_TERM atom_underline;
  ERL_NIF_TERM atom_use_xhtml;
} markdown_priv;

/* semi-arbitrary value that configures the minimum input threshold to schedule
 * the NIF to run in a dirty scheduler */
static unsigned long dirty_scheduling_threshold = 30000;

int
apply_extension(const ERL_NIF_TERM* tuple, ERL_NIF_TERM field, ERL_NIF_TERM atom_true, unsigned int flag, unsigned int* extensions) {
    if (enif_compare(tuple[0], field) == 0) {
      if (enif_compare(tuple[1], atom_true) == 0) {
        *extensions |= flag;
        return 1;
      }
    }

    return 0;
}

int
apply_markdown_extensions(const ERL_NIF_TERM* tuple, markdown_priv* priv, unsigned int* extensions) {
    if (apply_extension(tuple, priv->atom_autolink, priv->atom_true, HOEDOWN_EXT_AUTOLINK, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_disable_indented_code, priv->atom_true, HOEDOWN_EXT_DISABLE_INDENTED_CODE, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_fenced_code, priv->atom_true, HOEDOWN_EXT_FENCED_CODE, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_highlight, priv->atom_true, HOEDOWN_EXT_HIGHLIGHT, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_math, priv->atom_true, HOEDOWN_EXT_MATH, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_math_explicit, priv->atom_true, HOEDOWN_EXT_MATH_EXPLICIT, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_no_intra_emphasis, priv->atom_true, HOEDOWN_EXT_NO_INTRA_EMPHASIS, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_quote, priv->atom_true, HOEDOWN_EXT_QUOTE, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_space_headers, priv->atom_true, HOEDOWN_EXT_SPACE_HEADERS, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_strikethrough, priv->atom_true, HOEDOWN_EXT_STRIKETHROUGH, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_superscript, priv->atom_true, HOEDOWN_EXT_SUPERSCRIPT, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_tables, priv->atom_true, HOEDOWN_EXT_TABLES, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_underline, priv->atom_true, HOEDOWN_EXT_UNDERLINE, extensions))
        return 1;

    return 0;
}

int
apply_html_extensions(const ERL_NIF_TERM* tuple, markdown_priv* priv, unsigned int* extensions) {
    if (apply_extension(tuple, priv->atom_escape, priv->atom_true, HOEDOWN_HTML_ESCAPE, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_hard_wrap, priv->atom_true, HOEDOWN_HTML_HARD_WRAP, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_skip_html, priv->atom_true, HOEDOWN_HTML_SKIP_HTML, extensions))
        return 1;

    if (apply_extension(tuple, priv->atom_use_xhtml, priv->atom_true, HOEDOWN_HTML_USE_XHTML, extensions))
        return 1;

    return 0;
}

static ERL_NIF_TERM
do_to_html(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  ErlNifBinary input;
  ErlNifBinary output;

  markdown_priv* priv;
  ERL_NIF_TERM options;
  ERL_NIF_TERM term;
  const ERL_NIF_TERM* tuple;
  int tuple_size;
  unsigned int markdown_extensions;
  unsigned int html_extensions;

  hoedown_buffer* ob;
  hoedown_document* document;
  hoedown_renderer* renderer;

#ifdef PROFILE
  clock_t prof_start_time, prof_end_time;
  double elapsed_time;
#endif

  if (enif_inspect_binary(env, argv[0], &input) == 0) {
    return enif_make_badarg(env);
  }

  if (input.size < 1) {
    return argv[0];
  }

  priv = enif_priv_data(env);
  options = argv[1];
  markdown_extensions = 0;
  html_extensions = 0;

  while (enif_get_list_cell(env, options, &term, &options) != 0) {
    if (enif_get_tuple(env, term, &tuple_size, &tuple) != 0) {
      if (tuple_size > 1) {
          if(apply_markdown_extensions(tuple, priv, &markdown_extensions))
              continue;

          if(apply_html_extensions(tuple, priv, &html_extensions))
              continue;
      }
    }
  }

  ob = hoedown_buffer_new(OUTPUT_UNIT);
  renderer = hoedown_html_renderer_new(html_extensions, 0);
  document = hoedown_document_new(renderer, markdown_extensions, 16);

#ifdef PROFILE
  prof_start_time = clock();
#endif

  hoedown_document_render(document, ob, (uint8_t*) input.data, input.size);

#ifdef PROFILE
  prof_end_time = clock();

  if(prof_start_time == (clock_t)-1 || prof_end_time == (clock_t)-1) {
      enif_fprintf(stderr, "[markdown.c]: Error profiling\n");
  }

  enif_fprintf(stderr, "[markdown.c]: Input size: %lu\n", input.size);

  elapsed_time = (double)(prof_end_time - prof_start_time) / CLOCKS_PER_SEC;

  if (elapsed_time < 1)
      /* time in millis */
      enif_fprintf(stderr, "[markdown.c]: Parsing time: %.3fms.\n", elapsed_time * 1000);
  else
      /* time in seconds */
      enif_fprintf(stderr, "[markdown.c]: Parsing time: %.5fs.\n", elapsed_time);
#endif

  enif_release_binary(&input);
  hoedown_html_renderer_free(renderer);
  hoedown_document_free(document);

  enif_alloc_binary(ob->size, &output);
  memcpy(output.data, ob->data, ob->size);
  hoedown_buffer_free(ob);

  return enif_make_binary(env, &output);
}

static ERL_NIF_TERM
to_html(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
  ErlNifBinary input;

  if (enif_inspect_binary(env, argv[0], &input) == 0) {
    return enif_make_badarg(env);
  }

  if (input.size < 1) {
    return argv[0];
  }

  if (input.size > dirty_scheduling_threshold) {
#ifdef PROFILE
      enif_fprintf(stderr, "[markdown.c]: Input above arbitrary threshold. Running dirty NIF.\n");
#endif
      return enif_schedule_nif(env, "do_to_html", ERL_NIF_DIRTY_JOB_CPU_BOUND, do_to_html, argc, argv);
  } else {
#ifdef PROFILE
      enif_fprintf(stderr, "[markdown.c]: Input below arbitrary threshold. Running regular NIF.\n");
#endif
      return do_to_html(env, argc, argv);
  }
}

static unsigned long
set_nif_threshold(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int input;

    if (enif_get_int(env, argv[0], &input) == 0) {
        return enif_make_badarg(env);
    }

    if (input <= 0) {
        return enif_make_badarg(env);
    }

    dirty_scheduling_threshold = (unsigned long)input;

    return dirty_scheduling_threshold;
}

static ErlNifFunc funcs[] = {
    { "to_html", 2, to_html },
    { "set_nif_threshold", 1, set_nif_threshold }
};

static int
load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) {
  markdown_priv* data = enif_alloc(sizeof(markdown_priv));
  if (data == NULL) {
    return 1;
  }

  data->atom_autolink = enif_make_atom(env, "autolink");
  data->atom_disable_indented_code = enif_make_atom(env, "disable_indented_code");
  data->atom_escape = enif_make_atom(env, "escape");
  data->atom_fenced_code = enif_make_atom(env, "fenced_code");
  data->atom_hard_wrap = enif_make_atom(env, "hard_wrap");
  data->atom_highlight = enif_make_atom(env, "highlight");
  data->atom_math = enif_make_atom(env, "math");
  data->atom_math_explicit = enif_make_atom(env, "math_explicit");
  data->atom_no_intra_emphasis = enif_make_atom(env, "no_intra_emphasis");
  data->atom_quote = enif_make_atom(env, "quote");
  data->atom_skip_html = enif_make_atom(env, "skip_html");
  data->atom_space_headers = enif_make_atom(env, "space_headers");
  data->atom_strikethrough = enif_make_atom(env, "strikethrough");
  data->atom_superscript = enif_make_atom(env, "superscript");
  data->atom_tables = enif_make_atom(env, "tables");
  data->atom_true = enif_make_atom(env, "true");
  data->atom_underline = enif_make_atom(env, "underline");
  data->atom_use_xhtml = enif_make_atom(env, "use_xhtml");

  *priv = (void*) data;
  return 0;
}

static int
reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) {
  return 0;
}

static int
upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) {
  return load(env, priv, info);
}

static void
unload(ErlNifEnv* env, void* priv) {
  enif_free(priv);
}

ERL_NIF_INIT(Elixir.Markdown, funcs, &load, &reload, &upgrade, &unload)
