--=============================================================================
-- zettelkasten.lua --- zk plugin
-- Copyright (c) 2016-2023 Wang Shidong & Contributors
-- Author: Wang Shidong < wsdjeg@outlook.com >
-- URL: https://spacevim.org
-- License: GPLv3
--=============================================================================
local M = {}
local api = vim.api
local fn = vim.fn

local log_levels = vim.log.levels
local log = require('zettelkasten.log')
local config = require('zettelkasten.config')
local formatter = require('zettelkasten.formatter')
local browser = require('zettelkasten.browser')

local NOTE_ID_STRFTIME_FORMAT = '%Y-%m-%d-%H-%M-%S'

local function set_qflist(lines, action, bufnr, use_loclist, what)
  what = what or {}
  local _, local_efm = pcall(vim.api.nvim_buf_get_option, bufnr, 'errorformat')
  what.efm = what.efm or local_efm
  if use_loclist then
    fn.setloclist(bufnr, lines, action, what)
  else
    fn.setqflist(lines, action, what)
  end
end

local function read_note(file_path, line_count)
  local file = io.open(file_path, 'r')
  assert(file ~= nil)
  assert(file:read(0) ~= nil)

  if line_count == nil then
    return file:read('*all')
  end

  local lines = {}
  while #lines < line_count do
    local line = file:read('*line')
    if line == nil then
      break
    end

    table.insert(lines, line)
  end

  return lines
end

local function get_all_tags(lookup_tag)
  if lookup_tag ~= nil and #lookup_tag > 0 then
    lookup_tag = string.gsub(lookup_tag, '\\<', '')
    lookup_tag = string.gsub(lookup_tag, '\\>', '')
  end

  local tags = browser.get_tags()
  if lookup_tag ~= nil and #lookup_tag > 0 then
    tags = vim.tbl_filter(function(tag)
      return string.match(tag.name, lookup_tag) ~= nil
    end, tags)
  end

  return tags
end

local function generate_note_id()
  return fn.strftime(NOTE_ID_STRFTIME_FORMAT)
end

function M.completefunc(find_start, base)
  if find_start == 1 and base == '' then
    local pos = api.nvim_win_get_cursor(0)
    local line = api.nvim_get_current_line()
    local line_to_cursor = line:sub(1, pos[2])
    return fn.match(line_to_cursor, '\\k*$')
  end

  local notes = vim.tbl_filter(function(note)
    -- here the note sometimes do not have title, then it is nil
    return string.match(note.title, base)
  end, browser.get_notes())

  local words = {}
  for _, ref in ipairs(notes) do
    table.insert(words, {
      word = ref.id,
      abbr = ref.title,
      dup = 0,
      empty = 0,
      kind = '[zettelkasten]',
      icase = 1,
    })
  end

  return words
end

function M.set_note_id(bufnr)
  local first_line = vim.api.nvim_buf_get_lines(bufnr, 0, 1, true)[1]
  local zk_id = generate_note_id()
  if #zk_id > 0 then
    first_line, _ = string.gsub(first_line, '# ', '')
    api.nvim_buf_set_lines(bufnr, 0, 1, true, { '# ' .. zk_id .. ' ' .. first_line })
    vim.cmd('file ' .. zk_id .. '.md')
  else
    log.notify("There's already a note with the same ID.", log_levels.ERROR, {})
  end
end

function M.tagfunc(pattern, flags, info)
  local in_insert = string.match(flags, 'i') ~= nil
  local pattern_provided = pattern ~= '\\<\\k\\k' or pattern == '*'
  local all_tags = {}
  if pattern_provided then
    all_tags = get_all_tags(pattern)
  else
    all_tags = get_all_tags()
  end

  local tags = {}
  for _, tag in ipairs(all_tags) do
    table.insert(tags, {
      name = string.gsub(tag.name, '#', ''),
      filename = tag.file_name,
      cmd = tostring(tag.linenr),
      kind = 'zettelkasten',
    })
  end

  if not in_insert then
    local notes = browser.get_notes()
    for _, note in ipairs(notes) do
      if string.find(note.id, pattern, 1, true) or not pattern_provided then
        table.insert(tags, {
          name = note.id,
          filename = note.file_name,
          cmd = '1',
          kind = 'zettelkasten',
        })
      end
    end
  end

  if #tags > 0 then
    return tags
  end

  return nil
end

function M.keyword_expr(word, opts)
  if not word then
    return {}
  end

  opts = opts or {}
  opts.preview_note = opts.preview_note or false
  opts.return_lines = opts.return_lines or false

  local note = browser.get_note(word)
  if note == nil then
    log.notify('Cannot find note.', log_levels.ERROR, {})
    return {}
  end

  local lines = {}
  if opts.preview_note and not opts.return_lines then
    vim.cmd(config.preview_command .. ' ' .. note.file_name)
  elseif opts.preview_note and opts.return_lines then
    vim.list_extend(lines, read_note(note.file_name))
  else
    table.insert(lines, note.title)
  end

  return lines
end

function M.get_back_references(note_id)
  local note = browser.get_note(note_id)
  if note == nil then
    return {}
  end

  local title_cache = {}
  local get_title = function(id)
    if title_cache[id] ~= nil then
      return title_cache[id]
    end

    title_cache[id] = browser.get_note(id).title
    return title_cache[id]
  end

  local references = {}
  for _, back_reference in ipairs(note.back_references) do
    table.insert(references, {
      id = back_reference.id,
      linenr = back_reference.linenr,
      title = back_reference.title,
      file_name = back_reference.file_name,
    })
  end

  return references
end

function M.show_back_references(cword, use_loclist)
  use_loclist = use_loclist or false
  local references = M.get_back_references(cword)
  if #references == 0 then
    log.notify('No back references found.', log_levels.ERROR, {})
    return
  end

  local lines = {}
  for _, ref in ipairs(references) do
    local line = {}
    table.insert(line, fn.fnamemodify(ref.file_name, ':.'))
    table.insert(line, ':')
    table.insert(line, ref.linenr)
    table.insert(line, ': ')
    table.insert(line, ref.title)

    table.insert(lines, table.concat(line, ''))
  end

  set_qflist(
    {},
    ' ',
    vim.api.nvim_get_current_buf(),
    use_loclist,
    { title = '[[' .. cword .. ']] References', lines = lines }
  )

  if use_loclist then
    vim.cmd([[botright lopen | wincmd p]])
  else
    vim.cmd([[botright copen | wincmd p]])
  end
end

function M.get_toc(note_id, format)
  format = format or '- [%h](%d)'
  local references = M.get_back_references(note_id)
  local lines = {}
  for _, note in ipairs(references) do
    table.insert(lines, {
      file_name = note.file_name,
      id = note.id,
      title = note.title,
    })
  end

  return formatter.format(lines, format)
end

function M.get_note_browser_content()
  if config.zettel_dir == '' then
    log.notify("'notes_path' option is required for note browsing.", log_levels.WARN, {})
    return {}
  end

  local all_notes = browser.get_notes()
  local lines = {}
  for _, note in ipairs(all_notes) do
    table.insert(lines, {
      file_name = note.file_name,
      id = note.id,
      references = note.references,
      back_references = note.back_references,
      tags = note.tags,
      title = note.title,
    })
  end

  return formatter.format(lines, config.browseformat)
end

function M.add_hover_command()
  if fn.exists(':ZkHover') == 2 then
    return
  end

  vim.cmd(
    [[command -buffer -nargs=1 ZkHover :lua require"zettelkasten"._internal_execute_hover_cmd(<q-args>)]]
  )
end

function M._internal_execute_hover_cmd(args)
  if args ~= nil and type(args) == 'string' then
    args = vim.split(args, ' ', true)
  else
    args = {}
  end

  local cword = ''
  if #args == 1 then
    cword = fn.expand('<cword>')
  else
    cword = args[#args]
  end

  local lines = M.keyword_expr(cword, {
    preview_note = vim.tbl_contains(args, '-preview'),
    return_lines = vim.tbl_contains(args, '-return-lines'),
  })
  if #lines > 0 then
    log.notify(table.concat(lines, '\n'), log_levels.INFO, {})
  end
end

function M.zknew(opt) -- {{{
  vim.cmd([[new | setlocal filetype=markdown]])
  if config.zettel_dir ~= '' then
    if vim.fn.isdirectory(config.zettel_dir) == 0 then
      vim.fn.mkdir(vim.fn.expand(config.zettel_dir), 'p', '0700')
    end
    vim.cmd('lcd ' .. config.zettel_dir)
  end

  vim.cmd('normal ggI# New Note')
  require('zettelkasten').set_note_id(vim.api.nvim_get_current_buf())
  vim.cmd('normal $')
end
-- }}}

function M.setup(opts)
  opts = opts or {}
  opts.notes_path = opts.notes_path or ''

  config._set(opts)
end

return M