local actions = require('telescope.actions')
local action_state = require('telescope.actions.state')
local conf = require('telescope.config').values
local finders = require('telescope.finders')
local make_entry = require('telescope.make_entry')
local pickers = require('telescope.pickers')
local entry_display = require('telescope.pickers.entry_display')
local utils = require('telescope.utils')
local Path = require('plenary.path')

local string = string
---@diagnostic disable-next-line: undefined-global
local vim = vim
---@diagnostic disable-next-line: undefined-global
local jit = jit

local F = vim.F
local fn = vim.fn
local api = vim.api
local CocActionAsync = fn.CocActionAsync

local config = {}

local function is_ready(feature)
  if vim.g.coc_service_initialized ~= 1 then
    print('Coc is not ready!')
    return
  end

  if feature and not fn.CocHasProvider(feature) then
    print('Coc: server does not support ' .. feature)
    return
  end

  return true
end

local locations_to_items = function(locs)
  if not locs then
    return
  end
  local items = {}
  for _, l in ipairs(locs) do
    if l.targetUri and l.targetRange then
      -- LocationLink
      l.uri = l.targetUri
      l.range = l.targetRange
    end
    local bufnr = vim.uri_to_bufnr(l.uri)
    local is_readable = fn.filereadable(vim.uri_to_fname(l.uri))
    if is_readable == 0 then
      fn.bufload(bufnr)
    end
    local is_loaded = api.nvim_buf_is_loaded(bufnr)
    local line = ''

    if not is_loaded then
      local content = vim.fn.readfile(vim.uri_to_fname(l.uri))
      line = content[l.range.start.line + 1]
    else
      line = (api.nvim_buf_get_lines(bufnr, l.range.start.line, l.range.start.line + 1, false) or { '' })[1] or ''
    end

    local filename = vim.uri_to_fname(l.uri)
    local row = l.range.start.line
    items[#items + 1] = {
      filename = filename,
      lnum = row + 1,
      col = l.range.start.character + 1,
      text = line,
    }
  end

  return items
end

local mru = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready() then
    return
  end

  local home = vim.call('coc#util#get_data_home')
  local data = Path:new(home .. Path.path.sep .. 'mru'):read()
  if not data or #data == 0 then
    return
  end

  local results = {}
  local exists = {}
  local cwd = vim.loop.cwd() .. Path.path.sep
  for _, val in ipairs(utils.max_split(data, '\n')) do
    local p = Path:new(val)
    local lowerPrefix = val:sub(1, #cwd):gsub(Path.path.sep, ''):lower()
    local lowerCWD = cwd:gsub(Path.path.sep, ''):lower()
    if lowerCWD == lowerPrefix and p:exists() and p:is_file() and exists[val:sub(#cwd + 1)] == nil then
      exists[val:sub(#cwd + 1)] = true
      results[#results + 1] = val:sub(#cwd + 1)
    end
  end

  local make_display = function(text)
    local display, hl_group = utils.transform_devicons(text, text)

    if hl_group then
      return display, { { { 1, 3 }, hl_group } }
    else
      return display
    end
  end

  pickers.new(opts, {
    prompt_title = 'Coc MRU',
    sorter = conf.generic_sorter(opts),
    previewer = conf.qflist_previewer(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = function(line)
        return {
          valid = line ~= nil,
          value = line,
          ordinal = line,
          display = make_display(line),
        }
      end,
    }),
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local function CocActionWithTimeout(type, ...)
  local result = nil
  local completed = false

  local args = { ... }
  table.insert(args, 1, type)
  table.insert(args, function(_, res)
    result = res
    completed = true
  end)

  CocActionAsync(unpack(args))

  local timeout = (config and config.timeout) or 3000
  local waited = vim.wait(timeout, function()
    return completed
  end, 50)

  if not waited then
    print("Timeout waiting for " .. type .. " result (timeout: " .. timeout .. "ms)")
  end

  return result
end

local links = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready('documentLink') then
    return
  end

  local res = CocActionWithTimeout('links')
  if type(res) ~= 'table' then
    return
  end

  if vim.tbl_isempty(res) then
    print('No links available')
    return
  end

  local results = {}
  for _, l in ipairs(res) do
    results[#results + 1] = {
      lnum = l.range.start.line + 1,
      col = l.range.start.character,
      text = l.target,
    }
    if l.target:find('file://') then
      results[#results].filename = vim.uri_to_fname(l.target)
    end
  end

  pickers.new(opts, {
    prompt_title = 'Coc Document Links',
    sorter = conf.generic_sorter(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
    }),
    attach_mappings = function(prompt_bufnr)
      actions.select_default:replace(function()
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        local text = selection.text
        if text:find('https?://') then
          local opener = (jit.os == 'OSX' and 'open') or 'xdg-open'
          os.execute(opener .. ' ' .. text)
        else
          vim.fn.execute('edit ' .. selection.filename)
        end
      end)

      return true
    end,
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local handle_code_actions = function(opts, mode)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready('codeAction') then
    return
  end

  local results = CocActionWithTimeout('codeActions', mode)
  if type(results) ~= 'table' then
    return
  end

  if vim.tbl_isempty(results) then
    print('No available code actions')
    return
  end

  for i, x in ipairs(results) do
    x.idx = i
  end

  pickers.new(opts, {
    prompt_title = 'Coc Code Actions',
    sorter = conf.generic_sorter(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = function(line)
        return {
          valid = line ~= nil,
          value = line,
          ordinal = line.idx .. line.title,
          display = line.idx .. ': ' .. line.title,
        }
      end,
    }),
    attach_mappings = function(prompt_bufnr)
      actions.select_default:replace(function()
        actions.close(prompt_bufnr)
        local selection = action_state.get_selected_entry()
        CocActionAsync('doCodeAction', selection.value)
      end)

      return true
    end,
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

-- TODO
-- range code action
local cursor_code_actions = function(opts)
  handle_code_actions(opts, 'cursor')
end

local line_code_actions = function(opts)
  handle_code_actions(opts, 'line')
end

local file_code_actions = function(opts)
  handle_code_actions(opts, nil)
end

local function list_or_jump(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready(opts.coc_provider) then
    return
  end

  local defs = CocActionWithTimeout(opts.coc_action)
  if type(defs) ~= 'table' then
    return
  end

  if vim.tbl_isempty(defs) then
    print(('No %s found'):format(opts.coc_action))
  elseif #defs == 1 and not config.prefer_locations then
    CocActionAsync('runCommand', 'workspace.openLocation', nil, defs[1])
  else
    local results = locations_to_items(defs)
    if not results then
      return
    end
    pickers.new(opts, {
      prompt_title = opts.coc_title,
      previewer = conf.qflist_previewer(opts),
      sorter = conf.generic_sorter(opts),
      finder = finders.new_table({
        results = results,
        entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
      }),
      push_cursor_on_edit = config.push_cursor_on_edit,
    }):find()
  end
end

local definitions = function(opts)
  opts.coc_provider = 'definition'
  opts.coc_action = 'definitions'
  opts.coc_title = 'Coc Definitions'
  list_or_jump(opts)
end

local declarations = function(opts)
  opts.coc_provider = 'declaration'
  opts.coc_action = 'declarations'
  opts.coc_title = 'Coc Declarations'
  list_or_jump(opts)
end

local implementations = function(opts)
  opts.coc_provider = 'implementation'
  opts.coc_action = 'implementations'
  opts.coc_title = 'Coc Implementations'
  list_or_jump(opts)
end

local type_definitions = function(opts)
  opts.coc_provider = 'typeDefinition'
  opts.coc_action = 'typeDefinitions'
  opts.coc_title = 'Coc TypeDefinitions'
  list_or_jump(opts)
end

local references = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready('reference') then
    return
  end

  local excludeDeclaration = opts.excludeDeclaration or false
  local refs = CocActionWithTimeout('references', excludeDeclaration)
  if type(refs) ~= 'table' or vim.tbl_isempty(refs) then
    return
  end

  local results = locations_to_items(refs)
  if not results then
    return
  end

  local displayer = entry_display.create({
    separator = '▏',
    items = {
      { width = 6 },
      { width = 40 },
      { remaining = true },
    },
  })

  local make_display = function(entry)
    local line_info = { table.concat({ entry.lnum, entry.col }, ':'), 'TelescopeResultsLineNr' }
    local filename = utils.transform_path(opts, entry.filename)

    return displayer({
      line_info,
      filename,
      entry.text:gsub('.* | ', ''),
    })
  end

  pickers.new(opts, {
    prompt_title = 'Coc References',
    previewer = conf.qflist_previewer(opts),
    sorter = conf.generic_sorter(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = function(entry)
        return {
          valid = true,

          value = entry,
          ordinal = (not opts.ignore_filename and entry.filename or '') .. ' ' .. entry.text,
          display = make_display,

          filename = entry.filename,
          lnum = entry.lnum,
          col = entry.col,
          text = entry.text,
        }
      end,
    }),
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local references_used = function(opts)
  opts.excludeDeclaration = true
  references(opts)
end

local locations = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  local refs = vim.g.coc_jump_locations
  local results = locations_to_items(refs)
  if not results then
    return
  end
  pickers.new(opts, {
    prompt_title = 'Coc Locations',
    previewer = conf.qflist_previewer(opts),
    sorter = conf.generic_sorter(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = opts.entry_maker or make_entry.gen_from_quickfix(opts),
    }),
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local document_symbols = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready('documentSymbol') then
    return
  end

  local current_buf = api.nvim_get_current_buf()
  local symbols = CocActionWithTimeout('documentSymbols', current_buf)
  if type(symbols) ~= 'table' or vim.tbl_isempty(symbols) then
    return
  end

  local results = {}
  for _, s in ipairs(symbols) do
    results[#results + 1] = {
      filename = api.nvim_buf_get_name(current_buf),
      lnum = s.lnum,
      col = s.col,
      kind = s.kind,
      text = string.format('[%s] %s', s.kind, s.text),
    }
  end

  opts.ignore_filename = opts.ignore_filename or true
  opts.path_display = { "hidden" }
  pickers.new(opts, {
    prompt_title = 'Coc Document Symbols',
    previewer = conf.qflist_previewer(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
    }),
    sorter = conf.prefilter_sorter({
      tag = 'symbol_type',
      sorter = conf.generic_sorter(opts),
    }),
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local function get_workspace_symbols_requester()
  return function(prompt)
    local results = {}
    local symbols = CocActionWithTimeout('getWorkspaceSymbols', prompt)
    if type(symbols) ~= 'table' or vim.tbl_isempty(symbols) then
      return results
    end
    for _, s in ipairs(symbols) do
      local filename = vim.uri_to_fname(s.location.uri)
      local kind = vim.lsp.protocol.SymbolKind[s.kind] or 'Unknown'
      results[#results + 1] = {
        filename = filename,
        lnum = s.location.range.start.line + 1,
        col = s.location.range.start.character + 1,
        kind = kind,
        text = string.format('[%s] %s', kind, s.name),
      }
    end
    return results
  end
end

local workspace_symbols = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  pickers.new(opts, {
    prompt_title = 'Coc Workspace Symbols',
    finder = finders.new_dynamic({
      entry_maker = opts.entry_maker or make_entry.gen_from_lsp_symbols(opts),
      fn = get_workspace_symbols_requester(),
    }),
    previewer = conf.qflist_previewer(opts),
    sorter = conf.generic_sorter(),
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local diagnostics = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready() then
    return
  end

  local diagnosticsList = CocActionWithTimeout('diagnosticList')
  if type(diagnosticsList) ~= 'table' or vim.tbl_isempty(diagnosticsList) then
    return
  end

  opts = opts or {}
  local results = {}
  local buf_names = {}
  local current_buf = api.nvim_get_current_buf()
  local current_filename = api.nvim_buf_get_name(current_buf)
  if vim.fn.has("win32") then
    current_filename = string.lower(string.sub(current_filename, 1, 1)) .. string.sub(current_filename, 2)
  end
  if opts.get_all then
    local bufs = api.nvim_list_bufs()
    for _, bn in ipairs(bufs) do
      buf_names[api.nvim_buf_get_name(bn)] = bn
    end
  end
  for _, d in ipairs(diagnosticsList) do
    if d.severity == 'Information' then
      d.severity = 'Info'
    elseif d.severity == 'Warning' then
      d.severity = 'Warn'
    end
    if opts.get_all or (d.file == current_filename) then
      results[#results + 1] = {
        bufnr = buf_names[d.file] or current_buf,
        filename = d.file,
        lnum = d.lnum,
        col = d.col,
        start = d.location.range.start,
        finish = d.location.range['end'],
        text = vim.trim(d.message:gsub('[\n]', '')),
        type = d.severity:upper(),
      }
    end
  end

  opts.path_display = F.if_nil(opts.path_display, { 'hidden' })
  pickers.new(opts, {
    prompt_title = 'Coc Diagnostics',
    previewer = conf.qflist_previewer(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = opts.entry_maker or make_entry.gen_from_diagnostics(opts),
    }),
    sorter = conf.prefilter_sorter({
      tag = 'type',
      sorter = conf.generic_sorter(opts),
    }),
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local workspace_diagnostics = function(opts)
  opts = F.if_nil(opts, {})
  opts.path_display = F.if_nil(opts.path_display, { 'shorten' })
  opts.prompt_title = 'Coc Workspace Diagnostics'
  opts.get_all = true
  diagnostics(opts)
end

local notifications = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready() then
    return
  end

  local results = CocActionWithTimeout('notificationHistory')
  if type(results) ~= 'table' then
    return
  end

  local displayer = entry_display.create({
    separator = ' ',
    items = {
      { width = 8 },
      { width = 10 },
      { remaining = true },
    },
  })

  local make_display = function(entry)
    return displayer({
      { entry.kind or 'Info', 'TelescopeResultsFunction' },
      { entry.time:sub(12, 19), 'TelescopeResultsComment' },
      entry.message,
    })
  end

  pickers.new(opts, {
    prompt_title = 'Coc Notification History',
    sorter = conf.generic_sorter(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = function(line)
        return {
          valid = line ~= nil,
          value = line,
          ordinal = line.time .. ' ' .. line.kind .. ' ' .. line.message,
          display = make_display,
          message = line.message,
          kind = line.kind,
          time = line.time,
        }
      end,
    }),
    attach_mappings = function(prompt_bufnr)
      actions.select_default:replace(function()
        -- Do nothing on Enter
        actions.close(prompt_bufnr)
      end)
      return true
    end,
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local commands = function(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  if not is_ready() then
    return
  end

  local cmds = CocActionWithTimeout('commands')
  if type(cmds) ~= 'table' or vim.tbl_isempty(cmds) then
    print('No commands available')
    return
  end

  local results = {}

  local ok, history = pcall(function()
    return vim.split((Path:new(vim.fn['coc#util#get_data_home'](), 'commands'):read()), '\n')
  end)
  if ok then
    local id2title = {}
    for _, cmd in ipairs(cmds) do
      id2title[cmd.id] = cmd.title
    end

    local exists = {}
    for _, id in ipairs(history) do
      if not exists[id] then
        local title = id2title[id]
        if title then
          table.insert(results, { id = id, title = title })
          exists[id] = true
        end
      end
    end

    for _, cmd in pairs(cmds) do
      if not exists[cmd.id] then
        table.insert(results, cmd)
        exists[cmd.id] = true
      end
    end
  else
    results = cmds
  end

  local displayer = entry_display.create({
    separator = ' ',
    items = {
      { width = 40 },
      { remaining = true },
    },
  })
  local make_display = function(entry)
    return displayer({
      { entry.value, 'TelescopeResultsFunction' },
      entry.description,
    })
  end

  pickers.new(opts, {
    prompt_title = 'Coc Commands',
    sorter = conf.generic_sorter(opts),
    finder = finders.new_table({
      results = results,
      entry_maker = function(line)
        return {
          value = line.id,
          valid = line.id ~= nil,
          ordinal = line.id,
          display = make_display,
          description = line.title or '',
        }
      end,
    }),
    attach_mappings = function(prompt_bufnr)
      actions.select_default:replace(function()
        local selection = action_state.get_selected_entry()
        actions.close(prompt_bufnr)
        CocActionAsync('runCommand', selection.value)
      end)
      return true
    end,
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

local function subcommands(opts)
  if config.theme then
    opts = vim.tbl_deep_extend("force", opts or {}, config.theme)
  end
  local cmds = require('telescope.command').get_extensions_subcommand().coc
  cmds = vim.tbl_filter(function(v)
    return v ~= 'coc'
  end, cmds)

  pickers.new(opts, {
    prompt_title = 'Telescope Coc',
    finder = finders.new_table({
      results = cmds,
    }),
    sorter = conf.generic_sorter(opts),
    attach_mappings = function(prompt_bufnr)
      actions.select_default:replace(function()
        local selection = action_state.get_selected_entry()
        actions.close(prompt_bufnr)
        vim.defer_fn(function()
          require('telescope').extensions.coc[selection.value](opts)
        end, 20)
      end)
      return true
    end,
    push_cursor_on_edit = config.push_cursor_on_edit,
  }):find()
end

return require('telescope').register_extension({
  setup = function(ext_config)
    if ext_config.theme then
      config.theme = require('telescope.themes')['get_' .. ext_config.theme]()
    end
    if ext_config.prefer_locations then
      config.prefer_locations = true
    end
    if ext_config.push_cursor_on_edit then
      config.push_cursor_on_edit = true
    end
    if ext_config.timeout then
      config.timeout = ext_config.timeout
    end
  end,
  exports = {
    coc = subcommands,
    mru = mru,
    links = links,
    commands = commands,
    locations = locations,
    references = references,
    references_used = references_used,
    diagnostics = diagnostics,
    definitions = definitions,
    declarations = declarations,
    implementations = implementations,
    type_definitions = type_definitions,
    code_actions = cursor_code_actions,
    line_code_actions = line_code_actions,
    file_code_actions = file_code_actions,
    document_symbols = document_symbols,
    workspace_symbols = workspace_symbols,
    workspace_diagnostics = workspace_diagnostics,
    notifications = notifications,
  },
})

-- vim: set sw=2 ts=2 sts=2 et
