local mty = require'metaty'
--- EdFile: an editable file object, optimized for indexed and consequitive
--- reads and writes
local EdFile = mty'EdFile' {
  'lf   [lines.File]: indexed file',
  'dats [list]: list of Slc | Gap',
  'lens [list]: rolling sum of dat lengths',
}

local ds = require'ds'
local log = require'ds.log'
local U3File = require'lines.U3File'
local Gap = require'lines.Gap'
local File = require'lines.File'
local U3File = require'lines.U3File'

local push = table.insert
local getmt = getmetatable
local min, MAXINT = math.min, math.maxinteger
local index, newindex = mty.index, mty.newindex
local construct = mty.construct
local gt, binsearch = ds.lt, ds.binarySearch
local Slc = ds.Slc
local extend, inset, clear = ds.extend, ds.inset, ds.clear
local move, EMPTY = table.move, {}

getmetatable(EdFile).__index = mty.hardIndex
EdFile.__newindex            = mty.hardNewindex

getmetatable(EdFile).__call = function(T, v, mode)
  local lf, err = File{path=v, mode=mode or 'a+'}
  if not lf then return nil, err end
  return construct(T, {
    lf=lf, dats={Slc{si=1, ei=#lf}}, lens={},
  })
end

EdFile._updateLens = function(ef, max)
  max = max or MAXINT
  local lens, dats, len = ef.lens, ef.dats
  for i=#lens+1, #dats do
    len = (lens[i - 1] or 0) + #dats[i]
    lens[i] = len
    if len >= max then return end
  end
end

EdFile.__len = function(ef)
  ef:_updateLens()
  local l = ef.lens; return l[#l] or 0
end

--- get the index into dats where [$ef[i]] is located
EdFile._datindex = function(ef, i) --> di
  if i < 1 then return end
  local lens = ef.lens; local len = lens[#lens]
  if not len or i > len then ef:_updateLens(i) end
  if i > lens[#lens] then return end
  return binsearch(lens, i, gt) + 1
end

--- Get line at index
EdFile.get = function(ef, i) --> line
  local di = ef:_datindex(i); if not di then return end
  local dat = ef.dats[di]
  i = i - (ef.lens[di-1] or 0) -- i is now index into dat
  return (getmt(dat) == Slc) and ef.lf:get(dat.si + i - 1)
      or dat[i]
end

EdFile.write = function(ef, ...) --> self?, errmsg?
  local dats = ef.dats
  local last = dats[#dats]
  ef.lens[#dats] = nil
  local ok, errmsg
  if getmt(last) == Slc then
    ok, errmsg = ef.lf:write(...)
    last.ei = #ef.lf.idx
  else ok, errmsg = last:write(...) end
  return ok and self or nil, errmsg
end

--- Set line at index.
EdFile.set = function(ef, i, v)
  ef:inset(i, {v}, 1)
end

--- Return a read-only view of the EdFile which shares the
--- associated data structures.
EdFile.reader = function(ef)
  return EdFile {
    lf=ef.lf:reader(),
    dats=ef.dats, lens=ef.lens
  }
end

--- Flush the .lf member (which can only be extended).
--- To write all data to disk use :dumpf()
EdFile.flush = function(ef) return ef.lf:flush() end

--- Note: this does NOT necessarily flush what you expect.
EdFile.close = function(ef) return ef.lf:close() end

--- Dump EdFile to file or path
EdFile.dumpf = function(ef, f)
  local ef, efx = ef.lf.f, ef.lf.idx
  for i, d in ipairs(ef.dats) do
    if getmt(d) == Slc then
      local sp, ep = efx:get(d.si), efx:get(d.si + 1)
      assert(sp == ef:seek('set', sp))
      assert(f:write(ef:read(ep and (ep - sp + 1) or nil)))
    else
      -- TODO: use lines.dump instead?
      assert(f:write(concat(d, '\n')))
      if i < #ef.dats then assert(f:write'\n') end
    end
  end
end

--- appends to lf for extend when possible.
EdFile.extend = function(ef, values)
  if #values == 0 then return end
  local dlen = #ef.dats
  local last = ef.dats[dlen]
  if getmt(last) == Slc then
    local lf = ef.lf
    extend(lf, values); last.ei = #lf
  else extend(last, values) end
  local lens = ef.lens
  if dlen == #lens then
    lens[dlen] = lens[dlen] + #values
  end
  return ef
end

----------------------------
-- EdFile.inset
-- This is the major logic for mutating an EdFile

--- inset the dat, pushing to dats the values that have to be
--- reinserted into the EdFile.dats
local insetDat = function(dats, dat, i, values, rmlen)
  if getmt(dat) ~= Slc then
    inset(dat, i, values, rmlen)
    push(dats, dat)
    return
  end
  local dlen, vlen = #dat, values and #values or 0
  if (vlen == 0) and (rmlen == 0 or i > dlen) then
    push(dats, dat) -- no change
    return
  end
  if i > 1 then
    push(dats, Slc{si=dat.si, ei=i - 1})
  end
  if vlen > 0 then push(dats, Gap(values)) end
  if i + rmlen <= dat.ei then
    push(dats, Slc{si=i + rmlen, ei=dat.ei})
  end
end

--- insert into EdFile's dats.
EdFile.inset = function(ef, i, values, rmlen) --> rm?
  rmlen = rmlen or 0

  -- General algorith:
  -- * Get the first and last dats in [i:i+rmlen]. Inner dats are dropped.
  -- * Handle Slc types by splitting them
  -- * Handle rmlen for each section individually
  -- * Handle Gaps by joining them
  local lens, df, dl = ef.lens, ef:_datindex(i), nil
  if not df then
    -- special case: extend. This is special because it writes to the file.
    assert(i == #ef + 1, 'i > len+1')
    ef:extend(values)
    return
  end

  if rmlen > 0 then -- find last dat to remove (and in-between)
    dl = ef:_datindex(i + rmlen - 1)
    if dl then if (dl - df > 1) then
      -- update rmlen with dropped dats
      rmlen = rmlen - (lens[dl-1]-lens[df + 1])
    elseif df == dl then dl = nil end end
  end

  -- Note: rdats is replace dats (not rm), they
  -- are inset into dats at the end.
  local dats, rdats, ldat = ef.dats , {}                 , nil
  local first,   fi, ei   = dats[df], i - (lens[df-1] or 0)

  -- We handle the first and last items separately. By the end of these
  -- blocks we want them to be of type Gap with the rmlen values removed.
  if getmt(first) == Slc then
    -- split up first slice
    if 1 < fi then
      push(rdats, Slc{si=first.si, ei=first.si + fi - 2})
    end
    if dl then
      rmlen = rmlen - (#first - fi); assert(rmlen > 0, 'programmer error')
    elseif (fi + rmlen) <= first.ei then -- put Slc at end
      local slc = Slc{si=(first.si+fi-1) + rmlen, ei=first.ei}
      rmlen, ldat = 0, slc
    end
    fi, first = 1, nil
  else -- Gap
    local rmfirst = min(rmlen, #first - fi + 1)
    if rmfirst > 0 then
      first:inset(fi, nil, rmfirst)
      rmlen = rmlen - rmfirst
    end
  end

  local last, li
  if dl then
    last = dats[dl]
    if getmt(last) == Slc then
      if rmlen < #last then
        ldat = Slc{si=last.si + rmlen, ei=last.ei}
      end
      last = nil
    elseif rmlen > 0 then
      last:inset(1, nil, rmlen); rmlen = 0
    end
  end

  if last then
    if first then first:extend(last) -- join first+last
    else          first, fi = last, 1 end
  end
  if values and #values > 0 then
    first = first or Gap()
    first:inset(fi, values, 0)
  end
  if first then push(rdats, first) end
  if ldat  then push(rdats, ldat) end

  -- consolodate Gap objects
  first = dats[df-1]
  -- TODO: I think the second check is implied?
  if (getmt(first) == Gap) and (getmt(rdats[1]) == Gap) then
    first:extend(rdats[1]); rdats[1], df = first, df - 1
  end
  local last = rdats[#rdats]
  if dl and (getmt(last) == Gap) and (getmt(dats[dl+1]) == Gap) then
    last:extend(dats[dl+1]); dl = dl + 1
  end
  move(EMPTY, df, #lens, df, lens) -- clear end lens
  inset(dats, df, rdats, (dl or df) - df + 1)
end

EdFile.icopy = ds.defaultICopy

return EdFile
