local table = require("table")
local string = require("string")
local luabit = require("bit")
local tostr = string.char

local double_decode_count = 0
local double_encode_count = 0

-- cache bitops
local band, rshift = luabit.band, luabit.brshift
if not rshift then -- luajit differ from luabit
  rshift = luabit.rshift
end

local function byte_mod(x, v)
  if x < 0 then
    x = x + 256
  end
  return (x % v)
end

-- buffer
local strbuf = "" -- for unpacking
local strary = {} -- for packing

local function strary_append_int16(n, h)
  if n < 0 then
    n = n + 65536
  end
  table.insert(strary, tostr(h, math.floor(n / 256), n % 256))
end

local function strary_append_int32(n, h)
  if n < 0 then
    n = n + 4294967296
  end
  table.insert(
    strary,
    tostr(h, math.floor(n / 16777216), math.floor(n / 65536) % 256, math.floor(n / 256) % 256, n % 256)
  )
end

local doubleto8bytes
local strary_append_double = function(n)
  -- assume double
  double_encode_count = double_encode_count + 1
  local b = doubleto8bytes(n)
  table.insert(strary, tostr(0xcb))
  table.insert(strary, string.reverse(b)) -- reverse: make big endian double precision
end

--- IEEE 754

-- out little endian
doubleto8bytes = function(x)
  local function grab_byte(v)
    return math.floor(v / 256), tostr(math.fmod(math.floor(v), 256))
  end
  local sign = 0
  if x < 0 then
    sign = 1
    x = -x
  end
  local mantissa, exponent = math.frexp(x)
  if x == 0 then -- zero
    mantissa, exponent = 0, 0
  elseif x == 1 / 0 then
    mantissa, exponent = 0, 2047
  else
    mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, 53)
    exponent = exponent + 1022
  end

  local v, byte = "" -- convert to bytes
  x = mantissa
  for _ = 1, 6 do
    _, byte = grab_byte(x)
    v = v .. byte -- 47:0
  end
  x, byte = grab_byte(exponent * 16 + x)
  v = v .. byte -- 55:48
  x, byte = grab_byte(sign * 128 + x)
  v = v .. byte -- 63:56
  return v, x
end

local function bitstofrac(ary)
  local x = 0
  local cur = 0.5
  for _, v in ipairs(ary) do
    x = x + cur * v
    cur = cur / 2
  end
  return x
end

local function bytestobits(ary)
  local out = {}
  for _, v in ipairs(ary) do
    for j = 0, 7, 1 do
      table.insert(out, band(rshift(v, 7 - j), 1))
    end
  end
  return out
end

-- get little endian
local function bytestodouble(v)
  -- sign:1bit
  -- exp: 11bit (2048, bias=1023)
  local sign = math.floor(v:byte(8) / 128)
  local exp = band(v:byte(8), 127) * 16 + rshift(v:byte(7), 4) - 1023 -- bias
  -- frac: 52 bit
  local fracbytes = {
    band(v:byte(7), 15),
    v:byte(6),
    v:byte(5),
    v:byte(4),
    v:byte(3),
    v:byte(2),
    v:byte(1), -- big endian
  }
  local bits = bytestobits(fracbytes)

  for _ = 1, 4 do
    table.remove(bits, 1)
  end

  if sign == 1 then
    sign = -1
  else
    sign = 1
  end

  local frac = bitstofrac(bits)
  if exp == -1023 and frac == 0 then
    return 0
  end
  if exp == 1024 and frac == 0 then
    return 1 / 0 * sign
  end

  local real = math.ldexp(1 + frac, exp)

  return real * sign
end

--- packers

local packers = {}

packers.dynamic = function(data)
  local t = type(data)
  return packers[t](data)
end

packers["nil"] = function()
  table.insert(strary, tostr(0xc0))
end

packers.boolean = function(data)
  if data then -- pack true
    table.insert(strary, tostr(0xc3))
  else -- pack false
    table.insert(strary, tostr(0xc2))
  end
end

packers.number = function(n)
  if math.floor(n) == n then -- integer
    if n >= 0 then -- positive integer
      if n < 128 then -- positive fixnum
        table.insert(strary, tostr(n))
      elseif n < 256 then -- uint8
        table.insert(strary, tostr(0xcc, n))
      elseif n < 65536 then -- uint16
        strary_append_int16(n, 0xcd)
      elseif n < 4294967296 then -- uint32
        strary_append_int32(n, 0xce)
      else -- lua cannot handle uint64, so double
        strary_append_double(n)
      end
    else -- negative integer
      if n >= -32 then -- negative fixnum
        table.insert(strary, tostr(0xe0 + ((n + 256) % 32)))
      elseif n >= -128 then -- int8
        table.insert(strary, tostr(0xd0, byte_mod(n, 0x100)))
      elseif n >= -32768 then -- int16
        strary_append_int16(n, 0xd1)
      elseif n >= -2147483648 then -- int32
        strary_append_int32(n, 0xd2)
      else -- lua cannot handle int64, so double
        strary_append_double(n)
      end
    end
  else -- floating point
    strary_append_double(n)
  end
end

packers.string = function(data)
  local n = #data
  if n < 32 then
    table.insert(strary, tostr(0xa0 + n))
  elseif n < 65536 then
    strary_append_int16(n, 0xda)
  elseif n < 4294967296 then
    strary_append_int32(n, 0xdb)
  else
    error("overflow")
  end
  table.insert(strary, data)
end

packers["function"] = function()
  error("unimplemented:function")
end

packers.userdata = function()
  error("unimplemented:userdata")
end

packers.thread = function()
  error("unimplemented:thread")
end

packers.table = function(data)
  local is_map, ndata, nmax = false, 0, 0
  for k, _ in pairs(data) do
    if type(k) == "number" then
      if k > nmax then
        nmax = k
      end
    else
      is_map = true
    end
    ndata = ndata + 1
  end
  if is_map then -- pack as map
    if ndata < 16 then
      table.insert(strary, tostr(0x80 + ndata))
    elseif ndata < 65536 then
      strary_append_int16(ndata, 0xde)
    elseif ndata < 4294967296 then
      strary_append_int32(ndata, 0xdf)
    else
      error("overflow")
    end
    for k, v in pairs(data) do
      packers[type(k)](k)
      packers[type(v)](v)
    end
  else -- pack as array
    if nmax < 16 then
      table.insert(strary, tostr(0x90 + nmax))
    elseif nmax < 65536 then
      strary_append_int16(nmax, 0xdc)
    elseif nmax < 4294967296 then
      strary_append_int32(nmax, 0xdd)
    else
      error("overflow")
    end
    for i = 1, nmax do
      packers[type(data[i])](data[i])
    end
  end
end

-- types decoding

local types_map = {
  [0xc0] = "nil",
  [0xc2] = "false",
  [0xc3] = "true",
  [0xca] = "float",
  [0xcb] = "double",
  [0xcc] = "uint8",
  [0xcd] = "uint16",
  [0xce] = "uint32",
  [0xcf] = "uint64",
  [0xd0] = "int8",
  [0xd1] = "int16",
  [0xd2] = "int32",
  [0xd3] = "int64",
  [0xda] = "raw16",
  [0xdb] = "raw32",
  [0xdc] = "array16",
  [0xdd] = "array32",
  [0xde] = "map16",
  [0xdf] = "map32",
}

local type_for = function(n)
  if types_map[n] then
    return types_map[n]
  elseif n < 0xc0 then
    if n < 0x80 then
      return "fixnum_posi"
    elseif n < 0x90 then
      return "fixmap"
    elseif n < 0xa0 then
      return "fixarray"
    else
      return "fixraw"
    end
  elseif n > 0xdf then
    return "fixnum_neg"
  else
    return "undefined"
  end
end

local types_len_map = {
  uint16 = 2,
  uint32 = 4,
  uint64 = 8,
  int16 = 2,
  int32 = 4,
  int64 = 8,
  float = 4,
  double = 8,
}

--- unpackers

local unpackers = {}

local unpack_number = function(offset, ntype, nlen)
  local b1, b2, b3, b4, b5, b6, b7, b8
  if nlen >= 2 then
    b1, b2 = string.byte(strbuf, offset + 1, offset + 2)
  end
  if nlen >= 4 then
    b3, b4 = string.byte(strbuf, offset + 3, offset + 4)
  end
  if nlen >= 8 then
    b5, b6, b7, b8 = string.byte(strbuf, offset + 5, offset + 8)
  end

  if ntype == "uint16_t" then
    return b1 * 256 + b2
  elseif ntype == "uint32_t" then
    return b1 * 65536 * 256 + b2 * 65536 + b3 * 256 + b4
  elseif ntype == "int16_t" then
    local n = b1 * 256 + b2
    local nn = (65536 - n) * -1
    if nn == -65536 then
      nn = 0
    end
    return nn
  elseif ntype == "int32_t" then
    local n = b1 * 65536 * 256 + b2 * 65536 + b3 * 256 + b4
    local nn = (4294967296 - n) * -1
    if nn == -4294967296 then
      nn = 0
    end
    return nn
  elseif ntype == "double_t" then
    local s = tostr(b8, b7, b6, b5, b4, b3, b2, b1)
    double_decode_count = double_decode_count + 1
    local n = bytestodouble(s)
    return n
  else
    error("unpack_number: not impl:" .. ntype)
  end
end

local function unpacker_number(offset)
  local obj_type = type_for(string.byte(strbuf, offset + 1, offset + 1))
  local nlen = types_len_map[obj_type]
  local ntype
  if obj_type == "float" then
    error("float is not implemented")
  else
    ntype = obj_type .. "_t"
  end
  return offset + nlen + 1, unpack_number(offset + 1, ntype, nlen)
end

local function unpack_map(offset, n)
  local r = {}
  local k, v
  for _ = 1, n do
    offset, k = unpackers.dynamic(offset)
    assert(offset)
    offset, v = unpackers.dynamic(offset)
    assert(offset)
    r[k] = v
  end
  return offset, r
end

local function unpack_array(offset, n)
  local r = {}
  for i = 1, n do
    offset, r[i] = unpackers.dynamic(offset)
    assert(offset)
  end
  return offset, r
end

function unpackers.dynamic(offset)
  if offset >= #strbuf then
    error("need more data")
  end
  local obj_type = type_for(string.byte(strbuf, offset + 1, offset + 1))
  return unpackers[obj_type](offset)
end

function unpackers.undefined()
  error("unimplemented:undefined")
end

unpackers["nil"] = function(offset)
  return offset + 1, nil
end

unpackers["false"] = function(offset)
  return offset + 1, false
end

unpackers["true"] = function(offset)
  return offset + 1, true
end

unpackers.fixnum_posi = function(offset)
  return offset + 1, string.byte(strbuf, offset + 1, offset + 1)
end

unpackers.uint8 = function(offset)
  return offset + 2, string.byte(strbuf, offset + 2, offset + 2)
end

unpackers.uint16 = unpacker_number
unpackers.uint32 = unpacker_number
unpackers.uint64 = unpacker_number

unpackers.fixnum_neg = function(offset)
  -- alternative to cast below:
  local n = string.byte(strbuf, offset + 1, offset + 1)
  local nn = (256 - n) * -1
  return offset + 1, nn
end

unpackers.int8 = function(offset)
  local i = string.byte(strbuf, offset + 2, offset + 2)
  if i > 127 then
    i = (256 - i) * -1
  end
  return offset + 2, i
end

unpackers.int16 = unpacker_number
unpackers.int32 = unpacker_number
unpackers.int64 = unpacker_number

unpackers.float = unpacker_number
unpackers.double = unpacker_number

unpackers.fixraw = function(offset)
  local n = byte_mod(string.byte(strbuf, offset + 1, offset + 1), 0x1f + 1)
  --  print("unpackers.fixraw: offset:", offset, "#buf:", #buf, "n:",n  )
  local b
  if (#strbuf - 1 - offset) < n then
    error("require more data")
  end

  if n > 0 then
    b = string.sub(strbuf, offset + 1 + 1, offset + 1 + 1 + n - 1)
  else
    b = ""
  end
  return offset + n + 1, b
end

unpackers.raw16 = function(offset)
  local n = unpack_number(offset + 1, "uint16_t", 2)
  if (#strbuf - 1 - 2 - offset) < n then
    error("require more data")
  end
  local b = string.sub(strbuf, offset + 1 + 1 + 2, offset + 1 + 1 + 2 + n - 1)
  return offset + n + 3, b
end

unpackers.raw32 = function(offset)
  local n = unpack_number(offset + 1, "uint32_t", 4)
  if (#strbuf - 1 - 4 - offset) < n then
    error("require more data (possibly bug)")
  end
  local b = string.sub(strbuf, offset + 1 + 1 + 4, offset + 1 + 1 + 4 + n - 1)
  return offset + n + 5, b
end

unpackers.fixarray = function(offset)
  return unpack_array(offset + 1, byte_mod(string.byte(strbuf, offset + 1, offset + 1), 0x0f + 1))
end

unpackers.array16 = function(offset)
  return unpack_array(offset + 3, unpack_number(offset + 1, "uint16_t", 2))
end

unpackers.array32 = function(offset)
  return unpack_array(offset + 5, unpack_number(offset + 1, "uint32_t", 4))
end

unpackers.fixmap = function(offset)
  return unpack_map(offset + 1, byte_mod(string.byte(strbuf, offset + 1, offset + 1), 0x0f + 1))
end

unpackers.map16 = function(offset)
  return unpack_map(offset + 3, unpack_number(offset + 1, "uint16_t", 2))
end

unpackers.map32 = function(offset)
  return unpack_map(offset + 5, unpack_number(offset + 1, "uint32_t", 4))
end

-- Main functions

local ljp_pack = function(data)
  strary = {}
  packers.dynamic(data)
  local s = table.concat(strary, "")
  return s
end

local ljp_unpack = function(s, offset)
  if offset == nil then
    offset = 0
  end
  if type(s) ~= "string" then
    return false, "invalid argument"
  end
  local data
  strbuf = s
  offset, data = unpackers.dynamic(offset)
  return offset, data
end

local function ljp_stat()
  return {
    double_decode_count = double_decode_count,
    double_encode_count = double_encode_count,
  }
end

local msgpack = {
  pack = ljp_pack,
  unpack = ljp_unpack,
  stat = ljp_stat,
}

return msgpack
