#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <jpeglib.h>
#include <setjmp.h>
#include "erl_nif.h"
#include "barcode.h"
#include "base64.h"

#define MAX_ATOM_LENGTH 256

// get_string gets an Elixir string (binary)
// the caller is responsible for freeing the returned pointer.
static char *get_string(ErlNifEnv *env, ERL_NIF_TERM term) {
  ErlNifBinary bin;
  if (!enif_inspect_iolist_as_binary(env, term, &bin)) {
    return NULL;
  }
  char *str = strndup((char*) bin.data, bin.size);
  enif_release_binary(&bin);

  return str;
}

// make_string makes an Elixir string (binary)
static ERL_NIF_TERM make_string(ErlNifEnv *env, const char *str) {
  ERL_NIF_TERM result;
  size_t size = strlen(str);
  unsigned char *chars = enif_make_new_binary(env, size, &result);
  strncpy((char *)chars, str, size);
  return result;
}

// has_prefix checks if a string begins with the prefix string
static bool has_prefix(const char *s, const char *prefix) {
  if(strncmp(s, prefix, strlen(prefix)) == 0) {
    return true;
  }
  return false;
}

// Create barcode session with appropriate parameters.
// Caller is responsible for calling STFreeBarCodeSession when done.
static void *_create_session(ErlNifEnv *env, ERL_NIF_TERM options) {
  bool ok;
  if (!enif_is_map(env, options)) {
    return NULL;
  }

  ERL_NIF_TERM atom_true = enif_make_atom(env, "true");

  // %{multiple: true} waits for multiple barcodes instead of just the first one.
  uint16 multiple = 0;
  ERL_NIF_TERM multiple_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "multiple"), &multiple_term);
  if(ok && enif_is_atom(env, multiple_term) && enif_compare(multiple_term, atom_true) == 0) {
    multiple = 1;
  }

  int value = 0;

  // %{max_threads: 2} sets the number of threads, default (0) based on number of cpu cores.
  uint16 max_threads = 0;
  ERL_NIF_TERM max_threads_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "max_threads"), &max_threads_term);
  if (ok && enif_get_int(env, max_threads_term, &value)) {
    max_threads = value;
  }

  // %{timeout: 5000} maximum time in milliseconds, default is 5000.
  uint16 timeout = 5000;
  ERL_NIF_TERM timeout_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "timeout"), &timeout_term);
  if (ok && enif_get_int(env, timeout_term, &value)) {
    timeout = value;
  }

  // %{noise_reduction: 10} increases the chances of finding a barcode in a poor quality image
  // but also increases the time taken to process an image, default is 0 (off).
  uint16 noise_reduction = 0;
  ERL_NIF_TERM noise_reduction_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "noise_reduction"), &noise_reduction_term);
  if (ok && enif_get_int(env, noise_reduction_term, &value)) {
    noise_reduction = value;
  }

  // %{despeckle: 1} increases the chances of finding a barcode in a poor quality image
  // but also increases the time taken to process an image, default is 0 (off).
  uint16 despeckle = 0;
  ERL_NIF_TERM despeckle_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "despeckle"), &despeckle_term);
  if (ok && enif_get_int(env, despeckle_term, &value)) {
    despeckle = value;
  }

  // barcode types: %{type: [:pdf417, :code128]}
  uint16 codabar = 0, code39 = 0, code128 = 0, code25 = 0, ean13 = 0;
  uint16 ean8 = 0, upca = 0, upce = 0, pdf417 = 0, datamatrix = 0;

  ERL_NIF_TERM type_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "type"), &type_term);
  if(ok) {
    if(enif_is_atom(env, type_term)) {
      // TODO: could support a single atom,
      // or support binary strings in addition to atoms
    }

    if(enif_is_list(env, type_term)) {
      ERL_NIF_TERM head, tail, list = type_term;
      char type[MAX_ATOM_LENGTH];
      while(enif_get_list_cell(env, list, &head, &tail)) {
        if(enif_is_atom(env, head)) {
          ok = enif_get_atom(env, head, type, MAX_ATOM_LENGTH, ERL_NIF_LATIN1);
          if(ok) {
            if (strcmp(type, "codabar") == 0) {
              codabar = 1;
            } else if (strcmp(type, "code39") == 0) {
              code39 = 1;
            } else if (strcmp(type, "code128") == 0) {
              code128 = 1;
            } else if (strcmp(type, "code25") == 0) {
              code25 = 1;
            } else if (strcmp(type, "ean13") == 0) {
              ean13 = 1;
            } else if (strcmp(type, "ean8") == 0) {
              ean8 = 1;
            } else if (strcmp(type, "upca") == 0) {
              upca = 1;
            } else if (strcmp(type, "upce") == 0) {
              upce = 1;
            } else if (strcmp(type, "pdf417") == 0) {
              pdf417 = 1;
            } else if (strcmp(type, "datamatrix") == 0) {
              datamatrix = 1;
            } else {
              return NULL; // unrecognized type
            }
          } else {
            return NULL; // atom is too long
          }
        } else {
          return NULL; // not an atom
        }
        list = tail;
      }
    }
  } else {
    // if not specified, default to any type
    codabar = code39 = code128 = code25 = ean13 = ean8 = upca = upce = pdf417 = datamatrix = 1;
  }

  // license key: %{license: "..."}
  char *license = NULL;
  ERL_NIF_TERM license_term;
  ok = enif_get_map_value(env, options, enif_make_atom(env, "license"), &license_term);
  if (ok) {
    license = get_string(env, license_term);
    if (!license) {
      return NULL;
    }
  }

  // create bardecode session
  void *hBarcode = STCreateBarCodeSession();

  STSetParameter(hBarcode, ST_MULTIPLE_READ, &multiple);
  STSetParameter(hBarcode, ST_MAX_THREADS, &max_threads);
  STSetParameter(hBarcode, ST_TIMEOUT, &timeout);
  STSetParameter(hBarcode, ST_NOISEREDUCTION, &noise_reduction);
  STSetParameter(hBarcode, ST_DESPECKLE, &despeckle);

  STSetParameter(hBarcode, ST_READ_CODABAR, &codabar);
  STSetParameter(hBarcode, ST_READ_CODE39, &code39);
  STSetParameter(hBarcode, ST_READ_CODE128, &code128);
  STSetParameter(hBarcode, ST_READ_CODE25, &code25);
  STSetParameter(hBarcode, ST_READ_EAN13, &ean13);
  STSetParameter(hBarcode, ST_READ_EAN8, &ean8);
  STSetParameter(hBarcode, ST_READ_UPCA, &upca);
  STSetParameter(hBarcode, ST_READ_UPCE, &upce);
  STSetParameter(hBarcode, ST_READ_PDF417, &pdf417);
  STSetParameter(hBarcode, ST_READ_DATAMATRIX, &datamatrix);

  if (license) {
    STSetParameter(hBarcode, ST_LICENSE, license);
  }

  free(license);
  return hBarcode;
}

// _translate_error codes from bardecode into an {:error, "message"} tuple.
static ERL_NIF_TERM _translate_error(ErlNifEnv *env, int error) {
  ERL_NIF_TERM message;

  switch(error) {
  case ST_ERROR_FILE_OPEN:
    message = make_string(env, "cannot open image file");
    break;
  default:
    message =  make_string(env, "error reading image file");
  }

  return enif_make_tuple2(env, enif_make_atom(env, "error"), message);
}

// _prepare_result translates barcode results into a list of maps and returns an ok tuple or an error.
static ERL_NIF_TERM _prepare_result(ErlNifEnv *env, int bar_count, char **bar_codes_value, char **bar_codes_type) {
  if (bar_count < 0) {
    return _translate_error(env, bar_count);
  }

  // make an empty list to populate
  ERL_NIF_TERM barcodes = enif_make_list(env, 0);

  // make :kind and :value atoms as keys for the maps
  ERL_NIF_TERM atom_kind = enif_make_atom(env, "kind");
  ERL_NIF_TERM atom_value = enif_make_atom(env, "rawValue");

  // make a map for each barcode and append to the list.
  for (int n = 0; n < bar_count; n++) {
    ERL_NIF_TERM barcode = enif_make_new_map(env);
    enif_make_map_put(env, barcode, atom_kind, make_string(env, bar_codes_type[n]), &barcode);
    enif_make_map_put(env, barcode, atom_value, make_string(env, bar_codes_value[n]), &barcode);

    barcodes = enif_make_list_cell(env, barcode, barcodes);
  }

  return enif_make_tuple2(env, enif_make_atom(env, "ok"), barcodes);
}

// my_error_mgr for JPEG decoding error recovery
struct my_error_mgr {
  struct jpeg_error_mgr pub;  // "public" fields

  jmp_buf setjmp_buffer;  // for return to caller
};

typedef struct my_error_mgr *my_error_ptr;

void my_error_exit(j_common_ptr cinfo)
{
  // cinfo->err really points to a my_error_mgr struct, so coerce pointer
  my_error_ptr myerr = (my_error_ptr) cinfo->err;

  // Return control to the setjmp point
  longjmp(myerr->setjmp_buffer, 1);
}

// _decode_jpeg_from_memory
static ERL_NIF_TERM _decode_jpeg_from_memory(ErlNifEnv *env, ERL_NIF_TERM options, unsigned char *bitmap_data, unsigned bitmap_bytes) {
  unsigned char *bmp_buffer = NULL;

  struct jpeg_decompress_struct cinfo;

  struct my_error_mgr jerr;
  cinfo.err = jpeg_std_error(&jerr.pub);
  jerr.pub.error_exit = my_error_exit;
  // Establish the setjmp return context for my_error_exit to use.
  if (setjmp(jerr.setjmp_buffer)) {
    // If we get here, the JPEG code has signaled an error.
    char message[JMSG_LENGTH_MAX];
    cinfo.err->format_message((j_common_ptr)&cinfo, message);
    jpeg_destroy_decompress(&cinfo);
    if (bmp_buffer != NULL) {
      free(bmp_buffer);
    }
    return enif_make_tuple2(env, enif_make_atom(env, "error"), make_string(env, message));
  }

  jpeg_create_decompress(&cinfo);

  jpeg_mem_src(&cinfo, bitmap_data, bitmap_bytes);

  if (jpeg_read_header(&cinfo, TRUE) != 1) {
    return enif_make_tuple2(env, enif_make_atom(env, "error"), make_string(env, "error reading JPEG header"));
  }
  jpeg_start_decompress(&cinfo);

  uint16_t width = cinfo.output_width;
  uint16_t height = cinfo.output_height;

  int row_stride = width * cinfo.output_components;
  // Make a one-row-high sample array that will go away when done with the image.
  JSAMPARRAY scanline_buffer = (*cinfo.mem->alloc_sarray)
    ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);

  // NOTE: bmp_buffer scan lines in the image must begin on 4 byte boundary
  uint16_t padded_width = (width + 3) / 4 * 4;
  unsigned long bmp_size = padded_width * height;
  bmp_buffer = (unsigned char*) malloc(bmp_size);

  uint16_t row = 0, col = 0;
  uint8_t r, g, b, lum;
  while (cinfo.output_scanline < cinfo.output_height) {
    if (jpeg_read_scanlines(&cinfo, scanline_buffer, 1) == 1) {
      for(col = 0; col < width; col++) {
        r = scanline_buffer[0][3 * col];
        g = scanline_buffer[0][3 * col + 1];
        b = scanline_buffer[0][3 * col + 2];
        lum = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
        bmp_buffer[row * padded_width + col] = lum;
      }
      row++;
    }
  }

  jpeg_finish_decompress(&cinfo);
  jpeg_destroy_decompress(&cinfo);

  BITMAP bm;
  bm.bmType = 1;
  bm.bmWidth = width; // width of image in pixels
  bm.bmHeight = height;
  bm.bmWidthBytes = padded_width; // width of scan line, padded to 4-byte boundary
  bm.bmBitsPixel = 8;
  bm.bmPlanes = 1;
  bm.bmBits = bmp_buffer;

  float dpi = 200; // TODO: does it matter?

  // create bar code session with appropriate parameters.
  void *hBarcode = _create_session(env, options);
  if(!hBarcode) {
    free(bmp_buffer);
    return enif_make_badarg(env);
  }

  // read bar codes
  char **bar_codes_value;
  char **bar_codes_type;
  int bar_count = STReadBarCodeFromBitmap(hBarcode, &bm, dpi, &bar_codes_value, &bar_codes_type, 0);

  ERL_NIF_TERM result = _prepare_result(env, bar_count, bar_codes_value, bar_codes_type);

  // clean up
  STFreeBarCodeSession(hBarcode);

  free(bmp_buffer);

  return result;
}

// read_barcode from a file on disk (JPG/TIFF).
static ERL_NIF_TERM read_barcode(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
  char *file = get_string(env, argv[0]);
  if (!file) {
    return enif_make_badarg(env);
  }

  ERL_NIF_TERM options = argv[1];

  // create bar code session with appropriate parameters.
  void *hBarcode = _create_session(env, options);
  if(!hBarcode) {
    free(file);
    return enif_make_badarg(env);
  }

  // read bar codes
  char **bar_codes_value;
  char **bar_codes_type;
  int bar_count = STReadBarCode(hBarcode, file, 0, &bar_codes_value, &bar_codes_type);

  ERL_NIF_TERM result = _prepare_result(env, bar_count, bar_codes_value, bar_codes_type);

  // clean up
  STFreeBarCodeSession(hBarcode);
  free(file);

  return result;
}

// read_barcode_from_bitmap from memory (JPEG format).
static ERL_NIF_TERM read_barcode_from_bitmap(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
  ErlNifBinary bitmap;
  if (!enif_inspect_iolist_as_binary(env, argv[0], &bitmap)) {
    return enif_make_badarg(env);
  }

  ERL_NIF_TERM options = argv[1];

  ERL_NIF_TERM result = _decode_jpeg_from_memory(env, options, bitmap.data, bitmap.size);

  enif_release_binary(&bitmap);

  return result;
}

#define DATA_URL_PREFIX "data:image/jpeg;base64,"

// read_barcode_from_data_url from memory (base64 JPEG format).
static ERL_NIF_TERM read_barcode_from_data_url(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) {
  char *data_url = get_string(env, argv[0]);
  if (!data_url) {
    return enif_make_badarg(env);
  }
  if (!has_prefix(data_url, DATA_URL_PREFIX)) {
    free(data_url);
    return enif_make_tuple2(env, enif_make_atom(env, "error"), make_string(env, "expected image/jpeg data URL"));
  }

  ERL_NIF_TERM options = argv[1];

  int bitmap_size;
  char *encoded_data = &data_url[strlen(DATA_URL_PREFIX)];
  unsigned char *bitmap_data = unbase64(encoded_data, strlen(encoded_data), &bitmap_size);
  if(bitmap_size == 0) {
    free(data_url);
    return enif_make_tuple2(env, enif_make_atom(env, "error"), make_string(env, "invalid base64 string"));
  }

  ERL_NIF_TERM result = _decode_jpeg_from_memory(env, options, bitmap_data, bitmap_size);

  free(bitmap_data);
  free(data_url);

  return result;
}

// ErlNifFunc array exposes native implemented functions to Elixir with dirty scheduler.
static ErlNifFunc nif_funcs[] = {
  // {erl_function_name, erl_function_arity, c_function}
  {"read_barcode", 2, read_barcode, ERL_NIF_DIRTY_JOB_CPU_BOUND},
  {"read_barcode_from_bitmap", 2, read_barcode_from_bitmap, ERL_NIF_DIRTY_JOB_CPU_BOUND},
  {"read_barcode_from_data_url", 2, read_barcode_from_data_url, ERL_NIF_DIRTY_JOB_CPU_BOUND}
};

ERL_NIF_INIT(Elixir.Bardecode, nif_funcs, NULL, NULL, NULL, NULL)
