local path = require("plenary.path").path

local M = {}

M.strdisplaywidth = (function()
  local fallback = function(str, col)
    str = tostring(str)
    if vim.in_fast_event() then
      return #str - (col or 0)
    end
    return vim.fn.strdisplaywidth(str, col)
  end

  if jit and path.sep ~= [[\]] then
    local ffi = require "ffi"
    ffi.cdef [[
      typedef unsigned char char_u;
      int linetabsize_col(int startcol, char_u *s);
    ]]

    local ffi_func = function(str, col)
      str = tostring(str)
      local startcol = col or 0
      local s = ffi.new("char[?]", #str + 1)
      ffi.copy(s, str)
      return ffi.C.linetabsize_col(startcol, s) - startcol
    end

    local ok = pcall(ffi_func, "hello")
    if ok then
      return ffi_func
    else
      return fallback
    end
  else
    return fallback
  end
end)()

M.strcharpart = (function()
  local fallback = function(str, nchar, charlen)
    if vim.in_fast_event() then
      return str:sub(nchar + 1, charlen)
    end
    return vim.fn.strcharpart(str, nchar, charlen)
  end

  if jit and path.sep ~= [[\]] then
    local ffi = require "ffi"
    ffi.cdef [[
      typedef unsigned char char_u;
      int utf_ptr2len(const char_u *const p);
    ]]

    local function utf_ptr2len(str)
      local c_str = ffi.new("char[?]", #str + 1)
      ffi.copy(c_str, str)
      return ffi.C.utf_ptr2len(c_str)
    end

    local ok = pcall(utf_ptr2len, "🔭")
    if not ok then
      return fallback
    end

    return function(str, nchar, charlen)
      local nbyte = 0
      if nchar > 0 then
        while nchar > 0 and nbyte < #str do
          nbyte = nbyte + utf_ptr2len(str:sub(nbyte + 1))
          nchar = nchar - 1
        end
      else
        nbyte = nchar
      end

      local len = 0
      if charlen then
        while charlen > 0 and nbyte + len < #str do
          local off = nbyte + len
          if off < 0 then
            len = len + 1
          else
            len = len + utf_ptr2len(str:sub(off + 1))
          end
          charlen = charlen - 1
        end
      else
        len = #str - nbyte
      end

      if nbyte < 0 then
        len = len + nbyte
        nbyte = 0
      elseif nbyte > #str then
        nbyte = #str
      end
      if len < 0 then
        len = 0
      elseif nbyte + len > #str then
        len = #str - nbyte
      end

      return str:sub(nbyte + 1, nbyte + len)
    end
  else
    return fallback
  end
end)()

local truncate = function(str, len, dots, direction)
  if M.strdisplaywidth(str) <= len then
    return str
  end
  local start = direction > 0 and 0 or str:len()
  local current = 0
  local result = ""
  local len_of_dots = M.strdisplaywidth(dots)
  local concat = function(a, b, dir)
    if dir > 0 then
      return a .. b
    else
      return b .. a
    end
  end
  while true do
    local part = M.strcharpart(str, start, 1)
    current = current + M.strdisplaywidth(part)
    if (current + len_of_dots) > len then
      result = concat(result, dots, direction)
      break
    end
    result = concat(result, part, direction)
    start = start + direction
  end
  return result
end

M.truncate = function(str, len, dots, direction)
  str = tostring(str) -- We need to make sure its an actually a string and not a number
  dots = dots or "…"
  direction = direction or 1
  if direction ~= 0 then
    return truncate(str, len, dots, direction)
  else
    if M.strdisplaywidth(str) <= len then
      return str
    end
    local len1 = math.floor((len + M.strdisplaywidth(dots)) / 2)
    local s1 = truncate(str, len1, dots, 1)
    local len2 = len - M.strdisplaywidth(s1) + M.strdisplaywidth(dots)
    local s2 = truncate(str, len2, dots, -1)
    return s1 .. s2:sub(dots:len() + 1)
  end
end

M.align_str = function(string, width, right_justify)
  local str_len = M.strdisplaywidth(string)
  return right_justify and string.rep(" ", width - str_len) .. string or string .. string.rep(" ", width - str_len)
end

M.dedent = function(str, leave_indent)
  -- Check each line and detect the minimum indent.
  local indent
  local info = {}
  for line in str:gmatch "[^\n]*\n?" do
    -- It matches '' for the last line.
    if line ~= "" then
      local chars, width
      local line_indent = line:match "^[ \t]+"
      if line_indent then
        chars = #line_indent
        width = M.strdisplaywidth(line_indent)
        if not indent or width < indent then
          indent = width
        end
        -- Ignore empty lines
      elseif line ~= "\n" then
        indent = 0
      end
      table.insert(info, { line = line, chars = chars, width = width })
    end
  end

  -- Build up the result
  leave_indent = leave_indent or 0
  local result = {}
  for _, i in ipairs(info) do
    local line
    if i.chars then
      local content = i.line:sub(i.chars + 1)
      local indent_width = i.width - indent + leave_indent
      line = (" "):rep(indent_width) .. content
    elseif i.line == "\n" then
      line = "\n"
    else
      line = (" "):rep(leave_indent) .. i.line
    end
    table.insert(result, line)
  end
  return table.concat(result)
end

return M
