#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"

// Create barcode session with appropriate parameters.
// Caller is responsible for calling STFreeBarCodeSession when done.
static void *_create_session(char *license) {
  void *hBarcode = STCreateBarCodeSession();

  uint16 ival = 1;
  STSetParameter(hBarcode, ST_MULTIPLE_READ, &ival);
  STSetParameter(hBarcode, ST_READ_PDF417, &ival);
  STSetParameter(hBarcode, ST_READ_CODE128, &ival);
  STSetParameter(hBarcode, ST_LICENSE, license);

  return hBarcode;
}

// 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;
}

// _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, char *license, 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(license);

  // 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);
  }

  char *license = get_string(env, argv[1]);
  if (!license) {
    free(file);
    return enif_make_badarg(env);
  }

  // create bar code session with appropriate parameters.
  void *hBarcode = _create_session(license);

  // 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);
  free(license);

  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);
  }

  char *license = get_string(env, argv[1]);
  if (!license) {
    enif_release_binary(&bitmap);
    return enif_make_badarg(env);
  }

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

  free(license);
  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"));
  }

  char *license = get_string(env, argv[1]);
  if (!license) {
    free(data_url);
    return enif_make_badarg(env);
  }

  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);
    free(license);
    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, license, bitmap_data, bitmap_size);

  free(bitmap_data);
  free(license);
  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)
