local mod_name = (...):match ( "^(.*)%..-$" )

local md5 = require "resty.md5"
local str = require "resty.string"
local bson = require ( mod_name .. ".bson" )

local gridfs_file_mt = { }
local gridfs_file = { __index = gridfs_file_mt }
local get_bin_data = bson.get_bin_data

-- write size bytes from the buf string into mongo, by the offset 
function gridfs_file_mt:write(buf, offset, size)
  size = size or string.len(buf)
  if offset > self.file_size then return nil, "invalid offset" end
  if size > #buf then return nil, "invalid size" end

  local cn        -- number of chunks to be updated
  local af        -- number of bytes to be updated in first chunk
  local bn = 0    -- bytes number of buf already updated
  local nv = {}
  local od, t, i, r, err
  local of = offset % self.chunk_size
  local n = math.floor(offset/self.chunk_size)

  if of == 0 and size % self.chunk_size == 0 then
    --               chunk1 chunk2 chunk3
    -- old data      ====== ====== ======
    -- write buf            ====== ======
    --
    -- old data      ====== ====== ======
    -- write buf     ====== 

    cn = size/self.chunk_size
    for i = 1, cn do
      nv["$set"] = {data = get_bin_data(string.sub(buf, 
      self.chunk_size*(i-1) + 1, 
      self.chunk_size*(i-1) + self.chunk_size))}
      r, err = self.chunk_col:update({files_id = self.files_id, 
      n = n+i-1}, nv, 1, 0, true)
      if not r then return nil,"write failed: "..err end
    end
    bn = size
  else

    if of + size > self.chunk_size then
      --               chunk1 chunk2 chunk3
      -- old data      ====== ====== ======
      -- write buf        =======
      --               ...     -> of
      --                  ...  -> af
      af = self.chunk_size - of
    else
      af = size
    end

    cn = math.ceil((size + offset)/self.chunk_size) - n
    for i = 1, cn do
      if i == 1 then
        od = self.chunk_col:find_one(
        {files_id = self.files_id, n = n+i-1})
        if of ~= 0 and od then
          if size + of >= self.chunk_size then
            --               chunk1 chunk2 chunk3
            -- old data      ====== ====== ======
            -- write buf        =====
            t = string.sub(od.data, 1, of) 
            .. string.sub(buf, 1, af)
          else
            --               chunk1 chunk2 chunk3
            -- old data      ====== ====== ======
            -- write buf        ==
            t = string.sub(od.data, 1, of) 
            .. string.sub(buf, 1, af)
            .. string.sub(od.data, size + of + 1)
          end
          bn = af
        elseif of == 0 and od then
          if size < self.chunk_size then
            --               chunk1 chunk2 chunk3
            -- old data      ====== ====== ======
            -- write buf     ===
            t = string.sub(buf, 1) 
            .. string.sub(od.data, size + 1)
            bn = bn + size
          else
            --               chunk1 chunk2 chunk3
            -- old data      ====== ====== ======
            -- write buf     =========
            t = string.sub(buf, 1, self.chunk_size)
            bn = bn + self.chunk_size
          end
        else
          t = string.sub(buf, 1, self.chunk_size)
          bn = bn + #t --self.chunk_size
        end
        nv["$set"] = {data = get_bin_data(t)}
        r,err = self.chunk_col:update({files_id = self.files_id, 
        n = n+i-1}, nv, 1, 0, true)
        if not r then return nil,"write failed: "..err end
      elseif i == cn then
        od = self.chunk_col:find_one(
        {files_id = self.files_id, n = n + i - 1}
        )
        if od then
          t = string.sub(buf, bn + 1, size) 
          .. string.sub(od.data, size - bn + 1)
        else
          t = string.sub(buf, bn + 1, size) 
        end
        nv["$set"] = {data = get_bin_data(t)}
        r,err = self.chunk_col:update({files_id = self.files_id, 
        n = n+i-1}, nv, 1, 0, true)
        if not r then return nil,"write failed: "..err end
        bn = size
      else
        nv["$set"] = {data = get_bin_data(string.sub(buf, 
        bn + 1, bn + self.chunk_size))}
        r,err = self.chunk_col:update({files_id = self.files_id, 
        n = n+i-1}, nv, 1, 0, true)
        if not r then return nil,"write failed: "..err end
        bn = bn + self.chunk_size
      end
    end
  end

  local nf = offset + bn
  if nf > self.file_size then
    nv["$set"] = {length = nf}
    r,err = self.file_col:update({_id = self.files_id},nv, 
    0, 0, true)
    if not r then return nil,"write failed: "..err end
  end

  nv["$set"] = {md5 = 0}
  r,err = self.file_col:update({_id = self.files_id},nv, 
  0, 0, true)
  if not r then return nil,"write failed: "..err end
  return bn
end

-- read size bytes from mongo by the offset
function gridfs_file_mt:read(size, offset)
  size = size or self.file_size
  if size < 0 then
    return nil, "invalid size"
  end
  offset = offset or 0
  if offset < 0 or offset >= self.file_size then
    return nil, "invalid offset"
  end

  local n = math.floor(offset / self.chunk_size)
  local r
  local bytes = ""
  local rn = 0
  while true do
    r = self.chunk_col:find_one({files_id = self.files_id, n = n})
    if not r then return nil, "read chunk failed" end
    if size - rn < self.chunk_size then
      bytes = bytes .. string.sub(r.data, 1, size - rn)
      rn = size
    else
      bytes = bytes .. r.data
      rn = rn + self.chunk_size
    end
    n = n + 1
    if rn >= size then break end
  end
  return bytes
end

function gridfs_file_mt:update_md5()
  local n = math.floor(self.file_size/self.chunk_size)
  local md5_obj = md5:new()
  local r, i, err

  for i = 0, n do
    r = self.chunk_col:find_one({files_id = self.files_id, n = i})
    if not r then return false, "read chunk failed" end

    md5_obj:update(r.data) 
  end
  local md5hex = str.to_hex(md5_obj:final())

  local nv = {}
  nv["$set"] = {md5 = md5hex}
  self.file_md5 = md5hex
  r,err = self.file_col:update({_id = self.files_id}, nv, 0, 0, true)
  if not r then return false, "update failed: "..err end
  return true
end

return gridfs_file
