local lpeg = require "lpeg"
local util = require "digestif.util"

local B, P, R, S = lpeg.B, lpeg.P, lpeg.R, lpeg.S
local C, Cp, Ct, Cg = lpeg.C, lpeg.Cp, lpeg.Ct, lpeg.Cg
local concat = table.concat
local merge = util.merge
local search, gobble_until, case_fold = util.search, util.gobble_until, util.case_fold
local split, replace = util.split, util.replace

local bibtex = {}

local function ipairs_from(t, i)
  return ipairs(t), t, i - 1
end

--* Parser

local char = P(1)
local at_sign = P"@"
local newline = P"\n"
local space = S" \r\n\t"
local whitespace = space^0
local comment = P"%" * (1 - newline)^0 * newline
local junk = (comment + 1 - at_sign)^0
local number = C(R"09"^1) * whitespace
local name = C((R"az" + R"AZ" + R"09" + S"!$&*+-./:;<>?[]^_`|")^1) * whitespace
local lbrace = P"{" * whitespace
local rbrace = P"}" * whitespace
local lparen = P"(" * whitespace
local rparen = P")" * whitespace
local equals = P"=" * whitespace
local hash = P"#" * whitespace
local comma = P"," * whitespace
local lit_string = C(case_fold "string") * whitespace
local lit_comment = C(case_fold "comment") * whitespace
local lit_preamble = C(case_fold "preamble") * whitespace

local Cstart = Cg(Cp(), "start")
local Cstop = Cg(Cp(), "stop")

local curly_braced_string = util.between_balanced("{", "}")
local round_braced_string = util.between_balanced("(", ")")
local braced_string = (curly_braced_string + round_braced_string) * whitespace
local quoted_string = '"' * C(gobble_until('"')) * '"' * whitespace
local simple_value = quoted_string + braced_string + number + Ct(name)
local value = simple_value * (hash * simple_value)^0
local field = Ct(name * equals * value) + whitespace
local fields = field * (comma * field)^0

local token = curly_braced_string/0 + char
local nonspace = token - space
local author_sep = space * "and" * space

-- either curly or round braced
local function braced(patt)
  return lbrace * patt * rbrace + lparen * patt * rparen
end

local string_entry = at_sign * lit_string * braced(fields)
local comment_entry = at_sign * lit_comment * braced_string
local preamble_entry = at_sign * lit_preamble * braced(value)
local regular_entry = at_sign * name * braced(name * comma * fields)
local entry = string_entry + comment_entry + preamble_entry + regular_entry

-- this pattern produces the parse tree
-- TODO: catch premature end on invalid entry
local all_entries = Ct((junk * Ct(Cstart * entry * Cstop))^0)

--* Translate parse tree

-- BibItem class
local BibItem = {}
local mt = {__index = BibItem}
setmetatable(
  BibItem, {
    __call = function(_,t) return setmetatable(t, mt) end
})

-- replace user-defined strings and concatenate
local function process_value(val, strings, i)
  i = i or 2
  local t = {}
  for _, v in ipairs_from(val, i) do
    if type(v) == "table" then
      t[#t+1] = strings[v[1]] or ""
    else
      t[#t+1] = v
    end
  end
  return concat(t)
end

local default_options = {
  with_authors = true,
  with_title = true
}

--- Parse a bibtex file.
function bibtex.parse(src, options)
  options = merge(default_options, options)
  local entries = all_entries:match(src)
  local strings = merge(options.strings)
  local preambles = {}
  local ids = {}
  local items = {
    strings = strings,
    preambles = preambles,
    ids = ids
  }
  for _, t in ipairs(entries) do
    local entry_type = t[1]:lower()
    if entry_type == "preamble" then
      preambles[#preambles + 1] = t[2]
    elseif entry_type == "string" then
      for _, u in ipairs_from(t, 2) do
        local key = u[1]:lower()
        local val = process_value(u, strings)
        strings[key] = val
      end
    elseif entry_type ~= "comment" then
      local id = t[2]
      local fields = {} -- luacheck: ignore fields
      for _, u in ipairs_from(t, 3) do
        local key = u[1]:lower()
        local val = process_value(u, strings)
        fields[key] = val
      end
      local item = BibItem {
        id = id,
        type = entry_type,
        fields = fields,
        pos = t.start,
        cont = t.stop
      }
      ids[id] = item
      items[#items + 1] = item
    end
  end
  return items
end

--* Deuglify strings

local tex_symbols = {
  oe = "œ",
  OE = "Œ",
  ae = "ӕ",
  AE = "Ӕ",
  aa = "å",
  AA = "Å",
  ss = "ß",
   o = "ø",
   O = "Ø",
   l = "ł",
   L = "Ł",
   -- corner cases
   i = "{i}", -- not "ı"
   j = "j", -- not "ȷ"
   [" "] = " ",
}

local tex_accents = util.map(
  replace("◌", ""),
  {
    ['"'] = "◌̈",
    ["'"] = "◌́",
    ["."] = "◌̇",
    ["="] = "◌̄",
    ["^"] = "◌̂",
    ["`"] = "◌̀",
    ["~"] = "◌̃",
    ["c"] = "◌̧",
    ["d"] = "◌̣",
    ["H"] = "◌̋",
    ["u"] = "◌̆",
    ["b"] = "◌̱",
    ["v"] = "◌̌",
    ["t"] = "◌͡"
  }
)

local tex_letter = (R"AZ" + R"az")
local tex_char_or_math =  "$" * gobble_until("$") * "$" + char -- for deuglification purposes
local tex_cs_patt = "\\" * C(tex_letter^1 + char)* whitespace
local tex_accent_patt = tex_cs_patt * (curly_braced_string + C(char))
local function repl_accents_fun(cs, arg)
  local acc = tex_accents[cs]
  if not acc then
    return -- must return 0 values!
  else
    return arg .. acc
  end
end

local detexify_symbols = replace(tex_cs_patt, tex_symbols, tex_char_or_math)
local detexify_accents = replace(tex_accent_patt, repl_accents_fun, tex_char_or_math)
local debracify = replace(curly_braced_string, 1, tex_char_or_math)
local detitlify = replace(B(space) * C(tex_letter), string.lower, tex_char_or_math)
local clean = util.clean(space)

local function deuglify_name (s)
  return
    clean(
      debracify(
        detexify_accents(
          detexify_symbols(s))))
end

local function deuglify_title (s)
  return
    clean(
      debracify(
        detitlify(
          detexify_accents(
            detexify_symbols(s)))))
end

--* Pretty-printing

local split_authors = split(author_sep, token)
local split_name = split(comma, token)
local split_last = search(Cp() * C(nonspace^1) * whitespace * P(-1))

function BibItem:authors()
  local t = {}
  local authors = self.fields.author
  if not authors then return {} end
  for _, author in ipairs(split_authors(authors)) do
    local u = {}
    local parts = split_name(author)
    if #parts == 3 then
      u.first = parts[3]
      u.last = parts[1]
      u.suffix = parts[2]
    elseif #parts == 2 then
      u.first = parts[2]
      u.last = parts[1]
    else
      local p, l = split_last:match(author)
      if p then
        u.first = author:sub(1, p - 1)
        u.last = l
      else
        u.last = author
      end
    end
    t[#t + 1] = u
  end
  return t
end

function BibItem:pretty_print()
  local t, a = {}, {}
  for _, author in ipairs(self:authors()) do
    a[#a + 1] = deuglify_name(author.last)
  end
  t[#t + 1] = concat(a, ", ")
  t[#t + 1] = (self.fields.year or self.fields.date or "(n.d.)") .. ";"
  t[#t + 1] = deuglify_title(self.fields.title or "")
  return concat(t, " ")
end

return bibtex
