-- Basic shim for LuaJIT's stringbuffer.
-- Note this does not implement the full API.
-- This is intentionally internal-only. If we want to expose it, we should
-- reimplement this a userdata and ship it as `string.buffer`
-- (minus the FFI stuff) for Lua 5.1.
local M = {}

local has_strbuffer, strbuffer = pcall(require, 'string.buffer')

if has_strbuffer then
  M.new = strbuffer.new

  -- Lua 5.1 does not have __len metamethod so we need to provide a len()
  -- function to use instead.

  --- @param buf vim._core.stringbuffer
  --- @return integer
  function M.len(buf)
    return #buf
  end

  return M
end

--- @class vim._core.stringbuffer.ptr
--- @field [integer] integer

--- @class vim._core.stringbuffer
--- @field private ptr vim._core.stringbuffer.ptr
--- @field private buf string[]
--- @field package len integer absolute length of the `buf`
--- @field package skip_ptr integer
local StrBuffer = {}
StrBuffer.__index = StrBuffer

function StrBuffer:_normalize()
  if #self.buf > 1 then
    self.buf = { table.concat(self.buf) }
  end

  if self.skip_ptr > 0 then
    if self.buf[1] then
      self.buf[1] = self.buf[1]:sub(self.skip_ptr + 1)
      self.len = self.len - self.skip_ptr
    end
    self.skip_ptr = 0
  end

  return self
end

--- @return string
function StrBuffer:tostring()
  return self:_normalize().buf[1] or ''
end

StrBuffer.__tostring = StrBuffer.tostring

--- @private
--- Efficiently peek at the first `n` characters of the buffer.
--- @param n integer
--- @return string
function StrBuffer:_peek(n)
  local skip, buf1 = self.skip_ptr, self.buf[1]
  if buf1 and (n + skip) < #buf1 then
    return buf1:sub(skip + 1, skip + n)
  end
  return self:tostring():sub(1, n)
end

--- @param chunk string
function StrBuffer:put(chunk)
  local s = tostring(chunk)
  self.buf[#self.buf + 1] = s
  self.len = self.len + #s
  return self
end

--- @param str string
function StrBuffer:set(str)
  return self:reset():put(str)
end

--- @param n? integer
--- @return string
function StrBuffer:get(n)
  n = n or self.len
  local r = self:_peek(n)
  self:skip(n)
  return r
end

--- @param n integer
function StrBuffer:skip(n)
  self.skip_ptr = math.min(self.len, self.skip_ptr + n)
  return self
end

function StrBuffer:reset()
  self.buf = {}
  self.skip_ptr = 0
  self.len = 0
  return self
end

--- Efficiently read the character at the 0-based index `k` in the buffer.
--- @param k integer
--- @return integer
function StrBuffer:_index(k)
  return self:_normalize().buf[1]:byte(k + 1)
end

--- @return vim._core.stringbuffer.ptr, integer
function StrBuffer:ref()
  return self.ptr, self.len - self.skip_ptr
end

function M.new()
  local self = setmetatable({}, StrBuffer):reset()
  ---@diagnostic disable-next-line: invisible
  self.ptr = setmetatable({}, {
    __index = function(_, k)
      return StrBuffer._index(self, k)
    end,
  })
  return self
end

--- @param buf vim._core.stringbuffer
function M.len(buf)
  return buf.len - buf.skip_ptr
end

return M
