local core = require('cmp.core')
local source = require('cmp.source')
local config = require('cmp.config')
local feedkeys = require('cmp.utils.feedkeys')
local autocmd = require('cmp.utils.autocmd')
local keymap = require('cmp.utils.keymap')
local misc = require('cmp.utils.misc')

local cmp = {}

cmp.core = core.new()

---Expose types
for k, v in pairs(require('cmp.types.cmp')) do
  cmp[k] = v
end
cmp.lsp = require('cmp.types.lsp')
cmp.vim = require('cmp.types.vim')

---Expose event
cmp.event = cmp.core.event

---Export mapping for special case
cmp.mapping = require('cmp.config.mapping')

---Export default config presets
cmp.config = {}
cmp.config.disable = misc.none
cmp.config.compare = require('cmp.config.compare')
cmp.config.sources = require('cmp.config.sources')
cmp.config.mapping = require('cmp.config.mapping')

---Sync asynchronous process.
cmp.sync = function(callback)
  return function(...)
    cmp.core.filter:sync(1000)
    if callback then
      return callback(...)
    end
  end
end

---Suspend completion.
cmp.suspend = function()
  return cmp.core:suspend()
end

---Register completion sources
---@param name string
---@param s cmp.Source
---@return number
cmp.register_source = function(name, s)
  local src = source.new(name, s)
  cmp.core:register_source(src)
  return src.id
end

---Unregister completion source
---@param id number
cmp.unregister_source = function(id)
  cmp.core:unregister_source(id)
end

---Get current configuration.
---@return cmp.ConfigSchema
cmp.get_config = function()
  return require('cmp.config').get()
end

---Invoke completion manually
---@param option cmp.CompleteParams
cmp.complete = cmp.sync(function(option)
  option = option or {}
  config.set_onetime(option.config)
  cmp.core:complete(cmp.core:get_context({ reason = option.reason or cmp.ContextReason.Manual }))
  return true
end)

---Complete common string in current entries.
cmp.complete_common_string = cmp.sync(function()
  return cmp.core:complete_common_string()
end)

---Return view is visible or not.
cmp.visible = cmp.sync(function()
  return cmp.core.view:visible() or vim.fn.pumvisible() == 1
end)

---Get current selected entry or nil
cmp.get_selected_entry = cmp.sync(function()
  return cmp.core.view:get_selected_entry()
end)

---Get current active entry or nil
cmp.get_active_entry = cmp.sync(function()
  return cmp.core.view:get_active_entry()
end)

---Get current all entries
cmp.get_entries = cmp.sync(function()
  return cmp.core.view:get_entries()
end)

---Close current completion
cmp.close = cmp.sync(function()
  if cmp.core.view:visible() then
    local release = cmp.core:suspend()
    cmp.core.view:close()
    cmp.core:reset()
    vim.schedule(release)
    return true
  else
    return false
  end
end)

---Abort current completion
cmp.abort = cmp.sync(function()
  if cmp.core.view:visible() then
    local release = cmp.core:suspend()
    cmp.core.view:abort()
    vim.schedule(release)
    return true
  else
    return false
  end
end)

---Select next item if possible
cmp.select_next_item = cmp.sync(function(option)
  option = option or {}

  if cmp.core.view:visible() then
    local release = cmp.core:suspend()
    cmp.core.view:select_next_item(option)
    vim.schedule(release)
    return true
  elseif vim.fn.pumvisible() == 1 then
    -- Special handling for native pum. Required to facilitate key mapping processing.
    if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
      feedkeys.call(keymap.t('<C-n>'), 'in')
    else
      feedkeys.call(keymap.t('<Down>'), 'in')
    end
    return true
  end
  return false
end)

---Select prev item if possible
cmp.select_prev_item = cmp.sync(function(option)
  option = option or {}

  if cmp.core.view:visible() then
    local release = cmp.core:suspend()
    cmp.core.view:select_prev_item(option)
    vim.schedule(release)
    return true
  elseif vim.fn.pumvisible() == 1 then
    -- Special handling for native pum. Required to facilitate key mapping processing.
    if (option.behavior or cmp.SelectBehavior.Insert) == cmp.SelectBehavior.Insert then
      feedkeys.call(keymap.t('<C-p>'), 'in')
    else
      feedkeys.call(keymap.t('<Up>'), 'in')
    end
    return true
  end
  return false
end)

---Scrolling documentation window if possible
cmp.scroll_docs = cmp.sync(function(delta)
  if cmp.core.view:visible() then
    cmp.core.view:scroll_docs(delta)
    return true
  else
    return false
  end
end)

---Confirm completion
cmp.confirm = cmp.sync(function(option, callback)
  option = option or {}
  callback = callback or function() end

  local e = cmp.core.view:get_selected_entry() or (option.select and cmp.core.view:get_first_entry() or nil)
  if e then
    cmp.core:confirm(e, {
      behavior = option.behavior,
    }, function()
      callback()
      cmp.core:complete(cmp.core:get_context({ reason = cmp.ContextReason.TriggerOnly }))
    end)
    return true
  else
    -- Special handling for native puma. Required to facilitate key mapping processing.
    if vim.fn.complete_info({ 'selected' }).selected ~= -1 then
      feedkeys.call(keymap.t('<C-y>'), 'in')
      return true
    end
    return false
  end
end)

---Show status
cmp.status = function()
  local kinds = {}
  kinds.available = {}
  kinds.unavailable = {}
  kinds.installed = {}
  kinds.invalid = {}
  local names = {}
  for _, s in pairs(cmp.core.sources) do
    names[s.name] = true

    if config.get_source_config(s.name) then
      if s:is_available() then
        table.insert(kinds.available, s:get_debug_name())
      else
        table.insert(kinds.unavailable, s:get_debug_name())
      end
    else
      table.insert(kinds.installed, s:get_debug_name())
    end
  end
  for _, s in ipairs(config.get().sources) do
    if not names[s.name] then
      table.insert(kinds.invalid, s.name)
    end
  end

  if #kinds.available > 0 then
    vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
    vim.api.nvim_echo({ { '# ready source names\n', 'Special' } }, false, {})
    for _, name in ipairs(kinds.available) do
      vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
    end
  end

  if #kinds.unavailable > 0 then
    vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
    vim.api.nvim_echo({ { '# unavailable source names\n', 'Comment' } }, false, {})
    for _, name in ipairs(kinds.unavailable) do
      vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
    end
  end

  if #kinds.installed > 0 then
    vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
    vim.api.nvim_echo({ { '# unused source names\n', 'WarningMsg' } }, false, {})
    for _, name in ipairs(kinds.installed) do
      vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
    end
  end

  if #kinds.invalid > 0 then
    vim.api.nvim_echo({ { '\n', 'Normal' } }, false, {})
    vim.api.nvim_echo({ { '# unknown source names\n', 'ErrorMsg' } }, false, {})
    for _, name in ipairs(kinds.invalid) do
      vim.api.nvim_echo({ { ('- %s\n'):format(name), 'Normal' } }, false, {})
    end
  end
end

---@type cmp.Setup
cmp.setup = setmetatable({
  global = function(c)
    config.set_global(c)
  end,
  filetype = function(filetype, c)
    config.set_filetype(c, filetype)
  end,
  buffer = function(c)
    config.set_buffer(c, vim.api.nvim_get_current_buf())
  end,
  cmdline = function(type, c)
    config.set_cmdline(c, type)
  end,
}, {
  __call = function(self, c)
    self.global(c)
  end,
})

autocmd.subscribe('InsertEnter', function()
  feedkeys.call('', 'i', function()
    if config.enabled() then
      cmp.core:prepare()
      cmp.core:on_change('InsertEnter')
    end
  end)
end)

autocmd.subscribe('InsertLeave', function()
  cmp.core:reset()
  cmp.core.view:close()
end)

autocmd.subscribe('CmdlineEnter', function()
  if config.enabled() then
    cmp.core:prepare()
    cmp.core:on_change('InsertEnter')
  end
end)

autocmd.subscribe('CmdlineLeave', function()
  cmp.core:reset()
  cmp.core.view:close()
end)

autocmd.subscribe('TextChanged', function()
  if config.enabled() then
    cmp.core:on_change('TextChanged')
  end
end)

autocmd.subscribe('CursorMoved', function()
  if config.enabled() then
    cmp.core:on_moved()
  else
    cmp.core:reset()
    cmp.core.view:close()
  end
end)

autocmd.subscribe('InsertEnter', function()
  cmp.config.compare.scopes:update()
  cmp.config.compare.locality:update()
end)

cmp.event:on('complete_done', function(evt)
  if evt.entry then
    cmp.config.compare.recently_used:add_entry(evt.entry)
  end
  cmp.config.compare.scopes:update()
  cmp.config.compare.locality:update()
end)

cmp.event:on('confirm_done', function(evt)
  if evt.entry then
    cmp.config.compare.recently_used:add_entry(evt.entry)
  end
end)

return cmp