#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <jpeglib.h>
#include "erl_nif.h"
#include "barcode.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;
}

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

// 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) {
    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 a file on disk (JPG/TIFF).
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) {
    return enif_make_badarg(env);
  }

  struct jpeg_decompress_struct cinfo;
  struct jpeg_error_mgr jerr;

  cinfo.err = jpeg_std_error(&jerr);
  jpeg_create_decompress(&cinfo);

  jpeg_mem_src(&cinfo, bitmap.data, bitmap.size);

  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;
  uint8_t pixel_size = cinfo.output_components;

  uint32_t scanline_size = width * pixel_size;
  unsigned char *scanline_buffer = (unsigned char*) malloc(scanline_size);

  // 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;
  unsigned char *bmp_buffer = (unsigned char*) malloc(bmp_size);

  uint16_t row = 0, col = 0;
  uint8_t r, g, b, lum;
  while (row < height) {
    if (jpeg_read_scanlines(&cinfo, &scanline_buffer, 1) == 1) {
      for(col = 0; col < width; col++) {
        r = scanline_buffer[3 * col];
        g = scanline_buffer[3 * col + 1];
        b = scanline_buffer[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(scanline_buffer);
  free(bmp_buffer);

  free(license);
  enif_release_binary(&bitmap);

  return result;
}

// Let's define the array of ErlNifFunc beforehand:
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}
};

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