#include <iostream>

#define DUMP_DATA     1
#define DUMP_PREVIEWS (DUMP_DATA && 0)
#define DUMP_IMAGES   (DUMP_DATA && 1)

extern "C"
{
#include "json.h"
}

struct filereader_t
{
  filereader_t(const char *filename)
    : m_filename(filename)
  {
  }

  ~filereader_t()
  {
    if ( m_buf != nullptr )
      delete [] m_buf;
  }

  bool open()
  {
    FILE *fp = fopen(m_filename, "rb");
    if ( fp == nullptr )
      return false;
    fseek(fp, 0, SEEK_END);
    m_buflen = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    m_buf = new uint8_t[m_buflen];
    fread(m_buf, m_buflen, 1, fp);
    fclose(fp);

    reset();

    return true;
  }

  void reset()
  {
    m_ptr = m_buf;
  }

  void skip_crlf()
  {
    m_ptr += 2;
  }

  uint8_t get_u8()
  {
    return *m_ptr++;
  }

  uint16_t get_be16()
  {
    uint16_t v0 = get_u8();
    uint16_t v1 = get_u8();
    return (v0 << 8) | v1;
  }

  uint32_t get_be32()
  {
    uint32_t v0 = get_be16();
    uint32_t v1 = get_be16();
    return (v0 << 16) | v1;
  }

  uint8_t *get_buf(size_t buflen)
  {
    uint8_t *buf = m_ptr;
    m_ptr += buflen;
    return buf;
  }

  uint8_t *get_ptr()
  {
    return m_ptr;
  }

  size_t get_offset()
  {
    return m_ptr - m_buf;
  }

  size_t get_bytesleft()
  {
    return m_buflen - get_offset();
  }

private:
  const char *m_filename;
  uint8_t *m_buf = nullptr;
  size_t m_buflen;
  uint8_t *m_ptr;
};

struct json_ctx_cpp_t
{
  json_ctx_cpp_t()
  {
    json_ctx_start(&m_ctx, 1);
  }

  ~json_ctx_cpp_t()
  {
    json_ctx_free(&m_ctx);
  }

  /* object */
  size_t object_length(json_t *jso)
  {
    return json_object_length(jso);
  }

  json_t *object_new()
  {
    return json_object_new(&m_ctx);
  }

  int object_del(json_t *jso, const char *key)
  {
    return json_object_del(jso, key);
  }

  int object_add(json_t *jso, const char *key, json_t *jval)
  {
    return json_object_add(jso, key, jval);
  }

  json_t *object_get(json_t *jso, const char *key)
  {
    return json_object_get(jso, key);
  }

  int object_done(json_t *jso)
  {
    return json_object_done(&m_ctx, jso);
  }

  /* array */
  json_t *array_new(size_t len)
  {
    return json_array_new(&m_ctx, len);
  }

  int array_set(json_t *jso, size_t idx, json_t *jval)
  {
    return json_array_set(jso, idx, jval);
  }

  /* u8_array */
  json_t *buffer_new(const uint8_t *buf, size_t len)
  {
    json_t *jso = json_u8_array_new(&m_ctx, len);
    json_set_pflags(jso, JSON_PFLAGS_NO_LF);
    memcpy(jso->u8_array, buf, len);
    return jso;
  }

  /* string */
  json_t *string_new(const uint8_t *str)
  {
    return json_string_new(&m_ctx, (const char *) str);
  }

  /* int */
  json_t *int_new(int64_t val)
  {
    return json_int_new(&m_ctx, val);
  }

  /* float */
  json_t *float_new(uint32_t val)
  {
    union {
        uint32_t u32;
        float flt;
    } u;
    u.u32 = val;
    return json_float_new(&m_ctx, u.flt);
  }

  /* u8_2darray */
  json_t *u8_2darray_new(const uint8_t *buf, int16_t width, int16_t height)
  {
    return json_u8_2darray_new(&m_ctx, width, height, buf);
  }

  /* u16_2darray */
  json_t *u16_2darray_new(const uint8_t *buf, int16_t width, int16_t height)
  {
    return json_u16_2darray_new(&m_ctx, width, height, (const uint16_t *) buf);
  }

private:
  json_ctx_t m_ctx;
};

static bool writePPM(const char *fname, uint8_t *buf, size_t buflen, uint32_t ResolutionX, uint32_t ResolutionY, const char *filetype)
{
  // open output file
  FILE *fp = fopen(fname, "wb");
  if ( fp == nullptr )
    return false;

  // write header
  fprintf(fp, "%s\n", filetype);
  fprintf(fp, "%u %u\n", ResolutionX, ResolutionY);
  fprintf(fp, "255\n");

  // write data
  fwrite(buf, buflen, 1, fp);

  // close output file
  fclose(fp);
  return true;
}

static void decode_slice(const uint8_t *ptr, uint32_t data_size, uint32_t width, uint32_t height, uint32_t frameNum)
{
  const int total_size = width * height;
  uint8_t *dst = (uint8_t *) malloc(total_size);
  uint8_t *dst_ptr = dst;

  // vv
  // 00 black
  // 01 gray
  // 10 diff
  // 11 white

  //     0000
  //     3210
  // vv00llll
  //     0000 11000000
  //     3210 10987654
  // vv01llll llllllll
  // vv11llll llllllll llllllll

  const uint8_t *ptr_end = ptr + data_size;
  uint8_t prev_pixel = 0;

  // uint32_t pixelCount = 0;
  while ( ptr < ptr_end )
  {
    uint32_t runLength;
    uint8_t byte0 = *ptr++;
    uint8_t pixel = 0;
    switch ( (byte0 >> 6) )
    {
      case 0b00: pixel =   0x00; break;
      case 0b11: pixel =   0xff; break;
      case 0b01: pixel = *ptr++; break;
      case 0b10:
        pixel = byte0 & 0x0f;
        pixel = prev_pixel + ((byte0 & 0b00100000) ? -pixel : pixel);
        runLength = (byte0 & 0b00010000) ? *ptr++ : 1;
        goto do_runlength;
    }
    runLength = byte0 & 0b00001111;
    switch ( (byte0 >> 4) & 0b00000011 )
    {
      case 0b11:
        runLength |= (*ptr++) << 20;
        /* fall-through */
      case 0b10:
        runLength |= (*ptr++) << 12;
        /* fall-through */
      case 0b01:
        runLength |= (*ptr++) << 4;
        /* fall-through */
      case 0b00:
        break;
    }
do_runlength:
    while ( runLength-- )
      *dst_ptr++ = pixel;
    prev_pixel = pixel;
  }

  if ( DUMP_IMAGES )
  {
    char fname[0x100];
    sprintf(fname, "out/frame%04d.ppm", frameNum);
    writePPM(fname, dst, total_size, width, height, "P5");
  }

  free(dst);
}

static json_t *parse_layer(json_ctx_cpp_t &ctx, filereader_t &file, uint32_t width, uint32_t height, uint32_t idx)
{
  json_t *jlayer = ctx.object_new();

  json_t *jdef = ctx.object_new();
  ctx.object_add(jdef, "PauseFlag",                 ctx.int_new  (file.get_be16()));
  ctx.object_add(jdef, "PausePositonZ",             ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "layerPositionZ",            ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "layerExposureTime",         ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "layerOffTime",              ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Before_lift_static_time",   ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "After_lift_static_time",    ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "After_retract_static_time", ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "LiftDist",                  ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "LiftSpeed",                 ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Lift_Second_Dist",          ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Lift_Second_Speed",         ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Retract_Dist",              ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Retract_Speed",             ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Retract_Second_Dist",       ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "Retract_Second_Speed",      ctx.float_new(file.get_be32()));
  ctx.object_add(jdef, "LigthPwm",                  ctx.int_new  (file.get_be16()));
  ctx.object_done(jdef);

  ctx.object_add(jlayer, "LayerDefinition", jdef);
  ctx.object_add(jlayer, "CR_LF1",          ctx.buffer_new(file.get_buf(2), 2));
  ctx.object_add(jlayer, "LayerPointNum",   ctx.int_new   (file.get_be32()));

  json_t *jsep = ctx.object_new();
  ctx.object_add(jlayer, "sep", jsep);

  uint32_t LayerPointNum = json_int_val(ctx.object_get(jlayer, "LayerPointNum"));
#if 0
  for ( uint32_t i = 0; i < LayerPointNum; i++ )
  {
    uint8_t byte0 = file.get_u8();
  }
#else
  ctx.object_add(jsep, "layer_header_flag",   ctx.buffer_new(file.get_buf(1), 1));
  uint32_t image_data_size = LayerPointNum - 2;
  uint8_t *image_data = file.get_ptr();
  uint8_t *ptr = image_data;
  ctx.object_add(jsep, "layer_encode_data",   ctx.buffer_new(file.get_buf(image_data_size), image_data_size));
  uint8_t *ptr_end = file.get_ptr();
  uint8_t checksum = 0;
  while ( ptr < ptr_end )
    checksum += *ptr++;
  // printf("checksums (from file) %02X (calculated) %02X\n", *y, (uint8_t) ~checksum);
  ctx.object_add(jsep, "layer_data_checksum", ctx.buffer_new(file.get_buf(1), 1));
#endif

  if ( DUMP_IMAGES )
    decode_slice(image_data, image_data_size, width, height, idx);

  ctx.object_add(jlayer, "CR_LF2",          ctx.buffer_new(file.get_buf(2), 2));

  ctx.object_done(jlayer);

  return jlayer;
}

static void parse_prz(filereader_t &file)
{
  json_ctx_cpp_t ctx;
  json_t *jroot;

  jroot = ctx.object_new();

  ctx.object_add(jroot, "version",                          ctx.buffer_new     (file.get_buf( 4), 4));
  ctx.object_add(jroot, "Tag",                              ctx.buffer_new     (file.get_buf( 8), 8));
  ctx.object_add(jroot, "software",                         ctx.string_new     (file.get_buf(32)));
  ctx.object_add(jroot, "softwareVersion",                  ctx.string_new     (file.get_buf(24)));
  ctx.object_add(jroot, "time",                             ctx.string_new     (file.get_buf(24)));
  ctx.object_add(jroot, "printerName",                      ctx.string_new     (file.get_buf(32)));
  ctx.object_add(jroot, "printerType",                      ctx.string_new     (file.get_buf(32)));
  ctx.object_add(jroot, "profileName",                      ctx.string_new     (file.get_buf(32)));
  ctx.object_add(jroot, "AntiAliasingLevel",                ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "GreyLevel",                        ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "BlurLevel",                        ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "PreviewImage_116",                 ctx.u16_2darray_new(file.get_buf(2 * 116 * 116), 116, 116));
  ctx.object_add(jroot, "CR_LF1",                           ctx.buffer_new     (file.get_buf(2), 2));
  ctx.object_add(jroot, "PreviewImage_290",                 ctx.u16_2darray_new(file.get_buf(2 * 290 * 290), 290, 290));
  ctx.object_add(jroot, "CR_LF2",                           ctx.buffer_new     (file.get_buf(2), 2));
  ctx.object_add(jroot, "TotalLayers",                      ctx.int_new        (file.get_be32()));
  ctx.object_add(jroot, "XResolution",                      ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "YResolution",                      ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "Xmirror",                          ctx.int_new        (file.get_u8()));
  ctx.object_add(jroot, "Ymirror",                          ctx.int_new        (file.get_u8()));
  ctx.object_add(jroot, "PlatformXLength",                  ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "PlatformYLength",                  ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "PlatformZLength",                  ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "LayerThickness",                   ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "ExposureTime",                     ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Exposure_delay_mode",              ctx.int_new        (file.get_u8()));
  ctx.object_add(jroot, "TurnOffTime",                      ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Bottom_Before_lift_static_time",   ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Bottom_After_lift_static_time",    ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Bottom_After_retract_static_time", ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Before_lift_static_time",          ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "After_lift_static_time",           ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "After_retract_static_time",        ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomExposureTime",               ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomLayers",                     ctx.int_new        (file.get_be32()));
  ctx.object_add(jroot, "BottomLiftDist",                   ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomLiftSpeed",                  ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "LiftDist",                         ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "LiftSpeed",                        ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomRetractDist",                ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomRetractSpeed",               ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "RetractDist",                      ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "RetractSpeed",                     ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomLift_second_Dist",           ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomLift_second_Speed",          ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Lift_second_Dist",                 ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Lift_second_Speed",                ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomRetract_second_Dist",        ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomRetract_second_Speed",       ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Retract_second_Dist",              ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "Retract_second_Speed",             ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "BottomLigthPwm",                   ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "LigthPwm",                         ctx.int_new        (file.get_be16()));
  ctx.object_add(jroot, "Advance_Mode",                     ctx.int_new        (file.get_u8()));
  ctx.object_add(jroot, "PrintTimes",                       ctx.int_new        (file.get_be32()));
  ctx.object_add(jroot, "TotalVolume",                      ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "TotalWeight",                      ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "TotalPrice",                       ctx.float_new      (file.get_be32()));
  ctx.object_add(jroot, "PriceUnit",                        ctx.string_new     (file.get_buf(8)));
  ctx.object_add(jroot, "LayerContent_position_offset",     ctx.int_new        (file.get_be32()));
  ctx.object_add(jroot, "Grayscale_level",                  ctx.int_new        (file.get_u8()));
  ctx.object_add(jroot, "transitionLayers",                 ctx.int_new        (file.get_be16()));

  // TODO
  // printf("offset %zu\n", file.get_offset()); // == LayerContent_position_offset

  uint32_t TotalLayers = json_int_val(ctx.object_get(jroot, "TotalLayers"));
  json_t *jlayers = ctx.array_new(TotalLayers);
  ctx.object_add(jroot, "layers", jlayers);

  uint32_t width = json_int_val(ctx.object_get(jroot, "XResolution"));
  uint32_t height = json_int_val(ctx.object_get(jroot, "YResolution"));

  /* parse layers */
  for ( uint32_t i = 0; i < TotalLayers; i++ )
  {
    json_t *jlayer = parse_layer(ctx, file, width, height, i);
    ctx.array_set(jlayers, i, jlayer);
  }

  ctx.object_add(jroot, "ClosingTag", ctx.buffer_new(file.get_buf(11), 11));

  /* close root object */
  ctx.object_done(jroot);

  /* dump json */
  json_fputs(stdout, jroot);
}

int main(int argc, char *argv[])
{
  if ( argc < 2 )
  {
    std::cerr << "usage: %s <file.prz>" << argv[0] << std::endl;
    return -1;
  }

  /* open input file */
  filereader_t file(argv[1]);
  if ( !file.open() )
  {
    std::cerr << "Could not open input file \"" << argv[1] << "\"" << std::endl;
    return -1;
  }

  parse_prz(file);

  size_t bytesleft = file.get_bytesleft();
  if ( bytesleft != 0 )
    printf("bytes left at end of file: %zu\n", bytesleft);

  return 0;
}
