local util = require "digestif.util"
local Manuscript = require "digestif.Manuscript"
local Parser = require "digestif.Parser"

local co_wrap, co_yield = coroutine.wrap, coroutine.yield
local merge = util.merge
local path_join, path_split = util.path_join, util.path_split
local path_normalize = util.path_normalize
local format_filename_template = util.format_filename_template
local table_move, table_insert = table.move, table.insert

--* Parsing ConTeXt interface files

--** XML parser by Roberto Ierusalimschy
--
-- Cf. http://lua-users.org/wiki/LuaXml

local function parseargs(s)
  local arg = {}
  string.gsub(s, "([%w:_-]+)=([\"'])(.-)%2", function (w, _, a)
    arg[w] = a
  end)
  return arg
end

local function collect(s)
  local stack = {}
  local top = {}
  table.insert(stack, top)
  local ni,c,label,xarg, empty
  local i, j = 1, 1 -- luacheck: ignore j
  while true do
    ni,j,c,label,xarg, empty = string.find(s, "<(%/?)([%w:]+)(.-)(%/?)>", i)
    if not ni then break end
    local text = string.sub(s, i, ni-1)
    if not string.find(text, "^%s*$") then
      table.insert(top, text)
    end
    if empty == "/" then  -- empty element tag
      table.insert(top, {label=label, xarg=parseargs(xarg), empty=1})
    elseif c == "" then   -- start tag
      top = {label=label, xarg=parseargs(xarg)}
      table.insert(stack, top)   -- new level
    else  -- end tag
      local toclose = table.remove(stack)  -- remove top
      top = stack[#stack]
      if #stack < 1 then
        error("nothing to close with "..label)
      end
      if toclose.label ~= label then
        error("trying to close "..toclose.label.." with "..label)
      end
      table.insert(top, toclose)
    end
    i = j+1
  end
  local text = string.sub(s, i)
  if not string.find(text, "^%s*$") then
    table.insert(stack[#stack], text)
  end
  if #stack > 1 then
    error("unclosed "..stack[#stack].label)
  end
  return stack[1]
end

local function child_with_label(node, label)
  for _, n in ipairs(node) do
    if n.label == label then return n end
  end
end

local function children_with_label(node, label)
  local function iter(n, l)
    if n.label == l then
      co_yield(n)
    else
      for _, m in ipairs(n) do
        iter(m, l)
      end
    end
  end
  return co_wrap(function() return iter(node, label) end)
end

--** Parse ConTeXt interface (XML) files

local braces = {"{", "}"}
local brackets = {"[", "]"}
local parenthesis = {"(", ")"}

local function gen_tags(data)

  local inheritances = {}
  local inherit_type = {}
  local inherit_order = {}

  local function compute_meta(node)
    local fromtag = node.label:gsub('^cd:', '')
    local tbl = {}
    local other = 0
    for _, child in ipairs(node) do
      local attribs = child.xarg
      local val = attribs.type
      if val and val:match"^cd:" then
        tbl[#tbl+1]=val:sub(4)
      else
        other = other + 1
      end
    end
    if other == 0 and #tbl == 1 then
      return tbl[1]
    else
      return fromtag
    end
  end

  local function compute_values(node)
    local tbl = {}
    for _, child in ipairs(node) do
      if child.label == "cd:constant" then
        local val = child.xarg.type
        if not val:match"^cd:" then tbl[#tbl+1] = val end
      elseif child.label == "cd:inherit" then
        local val = child.xarg.name
        if not inheritances[tbl] then inheritances[tbl] = {} end
        local inh = inheritances[tbl]
        inherit_type[tbl] = "values"
        inherit_order[#inherit_order+1] = tbl
        inh[#inh+1] = val
      end
    end
    return #tbl > 0 and tbl or nil
  end

  local function compute_keys(node)
    local tbl = {}
    for _, child in ipairs(node) do
      if child.label == "cd:parameter" then
        local val = child.xarg.name
        tbl[val] = {
          values = compute_values(child),
          meta = compute_meta(child)
        }
      elseif child.label == "cd:inherit" then
        local val = child.xarg.name
        if not inheritances[tbl] then inheritances[tbl] = {} end
        local inh = inheritances[tbl]
        inherit_type[tbl] = "keys"
        inherit_order[#inherit_order+1] = tbl
        inh[#inh+1] = val
      end
    end
    return tbl
  end

  local function compute_argument(node)
    local ret = {delimiters=brackets}
    local attrs = node.xarg
    if attrs.optional == 'yes' then ret.optional = true end
    if attrs.list == 'yes' then ret.list = true end
    if node.label == "cd:keywords" then
      ret.meta=compute_meta(node)
      ret.values = compute_values(node)
    elseif node.label == "cd:assignments" then
      ret.meta="assignments"
      ret.keys = compute_keys(node)
    elseif node.label == "cd:constant" then
      ret.meta = node.xarg.type:gsub('^cd:', '')
    elseif node.label == "cd:content" then
      ret.meta="content"
      ret.delimiters = braces
    elseif node.label == "cd:csname" then
      ret.meta="command"
      ret.type="cs"
      ret.delimiters = nil
    elseif node.label == "cd:dimension" then
      ret.meta = "dimension"
      ret.type="dimen"
    end
    if attrs.delimiters == 'braces' then ret.delimiters = braces end
    if attrs.delimiters == 'parenthesis' then ret.delimiters = parenthesis end
    if attrs.delimiters == 'none' then ret.delimiters = false end
    return ret
  end

  local function compute_arguments(node)
    local tbl = {}
    for _, child in ipairs(node) do
      tbl[#tbl+1] = compute_argument(child)
    end
    return tbl
  end

  local function compute_instances(name, node)
    local tbl = {}
    if node == nil then return tbl end
    for _, instance in ipairs(node) do
      local inst_name = instance.xarg.value
      if name ~= inst_name then
        tbl[#tbl+1] = inst_name
      end
    end
    return tbl
  end

  local command_list = {}
  for node in children_with_label(data, "cd:command") do
    local attribs = node.xarg
    local arguments = child_with_label(node, "cd:arguments")
    arguments = arguments and compute_arguments(arguments)
    local cmd = {
      cs = attribs.name,
      environment = (attribs.type == "environment"),
      --source = attribs.file,
      --category = attribs.category,
      arguments = arguments,
    }
    command_list[#command_list+1] = cmd

    local instances = compute_instances(
      attribs.name, child_with_label(node, "cd:instances"))
    for _, name in ipairs(instances) do
      command_list[#command_list+1] = merge(cmd, {cs = name})
    end
  end

  -- FIXME: need to distinguish when doing
  local cmds_and_envs = {}
  for _, cmd in ipairs(command_list) do
    local cs = cmd.cs
    if cmd.link or not cmds_and_envs[cs] then
      cmds_and_envs[cs] = cmd
    end
  end

  for _, tbl in ipairs(inherit_order) do
    local inh = inheritances[tbl]
    --print("\n\nbefore", ser(tbl))
    for _, cs in ipairs(inh) do
      for _, arg in ipairs((cmds_and_envs[cs] or {}).arguments or {}) do
        util.update(tbl, arg[inherit_type[tbl]] or {})
      end
    end
    --print("\nafter", ser(tbl))
  end

  local tags = {
    generated = true,
    commands = {},
    environments = {}

  }

  for _, cmd in ipairs(command_list) do
    local cs = cmd.cs
    local list = cmd.environment and tags.environments or tags.commands
    local wikiname = cmd.environment and "start" .. cs or cs
    cmd.documentation = {
      [1] = {
        summary = "“" .. wikiname .. "” on ConTeXt Garden",
        uri = "https://wiki.contextgarden.net/Command/" .. wikiname
      }
    }
    cmd.environment, cmd.cs = nil, nil
    if not list[cs] then
      list[cs] = cmd
    else
      local variants = list[cs].variants
      if not variants then
        variants = {}
        list[cs].variants = variants
      end
      variants[#variants+1] = cmd
    end
  end

  return tags
end

-- Generate tags from a ConTeXt interface (XML) file.
--
-- Arguments:
--   file: The file name.
--   pkg: An optional package description table.
--
-- Returns:
--   The tags, or nil if file does not exist.
--
local function tags_from_xml(file, pkg)
  local ok, str = util.find_file(file, nil, true)
  if not ok then return end
  local ok, data = pcall(collect, str) -- luacheck: ignore ok
  if not ok then return end
  return setmetatable(gen_tags(data), {__index = pkg})
end

-- Monkey-patch to isolate XML dependency in this file.
(require "digestif.data").tags_from_xml = tags_from_xml

--** Core ConTeXt tags

local ctx_tags = (require "digestif.data".require_tags)("context-en.xml")

if ctx_tags then

  ctx_tags.ctan_package = "context"

  ctx_tags.documentation = {
    [1] = {
      summary = "ConTeXt Mark IV: an excursion",
      uri = "texmf:doc/context/documents/general/manuals/ma-cb-en.pdf"
    },
    [2] = {
      summary = "ConTeXt documentation library",
      uri = "https://wiki.contextgarden.net/Documentation"
    }
  }

  local commands = ctx_tags.commands
  local environments = ctx_tags.environments

  -- Add details to sectioning commands
  for _, tbl in ipairs{commands, environments} do
    for i, cs in ipairs{
      "part", "chapter", "section", "subsection", "subsubsection",
      "subsubsubsection", "subsubsubsubsection"
    } do
      tbl[cs].section_level = i
      tbl[cs].action = "section"
    end
    for i, cs in ipairs{
      "title", "subject", "subsubject", "subsubsubject",
      "subsubsubsubject", "subsubsubsubsubject"
    } do
      tbl[cs].section_level = i + 1
      tbl[cs].action = "section"
    end
  end

  -- Add details to file-inputting commands.
  for cs, filename in pairs {
    environment = "?.tex",
    project = "?.tex",
    component = "?.tex",
    usemodule = "t-?.xml"
  } do
    if commands[cs] then
      commands[cs].action = "input"
      commands[cs].filename = filename
    end
  end

  -- Add details to assorted commands.
  for cs, action in pairs {
    usebtxdataset = "input",
    cite = "cite",
    nocite = "cite",
    define = "define",
    definestartstop = "definestartstop",
    defineitemgroup = "definestartstop",
    definelist = "definestartstop",
    citation = "cite",
    nocitation = "cite",
    pagereference = "label",
    textreference = "label",
    ["in"] = "ref",
    at = "ref",
    about = "ref"
  } do
    if commands[cs] then
      commands[cs].action = action
    end
  end

  -- Add start/stop commands
  for env, cmd in pairs(environments) do
    commands["start" .. env] = cmd
    commands["stop" .. env] = {}
  end

end

--* ManuscriptContext class

local ManuscriptContext = util.class(Manuscript)

ManuscriptContext.parser = Parser()
ManuscriptContext.format = "context"
ManuscriptContext.packages = {}
ManuscriptContext.commands = {}
ManuscriptContext.environments = {}
ManuscriptContext.init_callbacks = {}
ManuscriptContext.scan_references_callbacks = {}
ManuscriptContext:add_package("context-en.xml")

-- Make a snippet for an environment.
function ManuscriptContext:snippet_env(cs, args)
  local argsnippet = args and self:snippet_args(args) or ""
  return "start" .. cs .. argsnippet .. "\n\t$0\n\\stop" .. cs
end

-- ConTeXt optional arguments can't always be distinguished by their
-- delimiters, for instance \citation[optional][mandatory].  Here we
-- patch Parser.parse_args to deal with this case, but just in the
-- simplest case where the optional arguments are in the beginning of
-- the argument list.
local original_parse_args = ManuscriptContext.parser.parse_args

local function new_parse_args(arglist, str, pos)
  local val = original_parse_args(arglist, str, pos)
  if val.incomplete and arglist[1].optional then
    arglist = table_move(arglist, 2, #arglist, 1, {})
    val = new_parse_args(arglist, str, pos)
    table_insert(val, 1, {omitted = true})
  end
  return val
end

ManuscriptContext.parser.parse_args = new_parse_args

--* Init scanning

local to_args = {}
for i = 1, 9 do
  local t = merge(to_args[i-1] or {})
  t[i] = {meta = "#" .. i}
  to_args[i] = t
end

function ManuscriptContext.init_callbacks.define(self, pos, cs)
  -- ugly! this function (and others below) parses the command twice
  local cont = self:parse_command(pos, cs).cont
  local csname, n_args
  for r in self:argument_items("command", pos, cs) do
    csname = self:substring_stripped(r):sub(2)
  end
  for r in self:argument_items("number", pos, cs) do
    n_args = tonumber(self:substring_stripped(r))
  end
  if csname then
    local idx = self:get_index "newcommand"
    idx[#idx+1] = {
      name = csname,
      pos = pos,
      cont = cont,
      manuscript = self,
      arguments = to_args[n_args]
    }
    if not self.commands[csname] then
      self.commands[csname] = idx[#idx]
    end
  end
  return cont
end

function ManuscriptContext.init_callbacks.definestartstop(self, pos, cs)
  local cont = self:parse_command(pos, cs).cont
  local name
  for r in self:argument_items("name", pos, cs) do
    name = self:substring_stripped(r)
  end
  if name then
    local idx = self:get_index "newenvironment"
    idx[#idx+1] = {
      name = name,
      pos = pos,
      cont = cont,
      manuscript = self,
    }
    if not self.environments[name] then
      self.environments[name] = idx[#idx]
    end
  end
  return cont
end

function ManuscriptContext.init_callbacks.label(self, pos, cs)
  local cont = self:parse_command(pos, cs).cont
  local idx = self.label_index
  for r in self:argument_items("reference", pos, cs) do
    idx[#idx + 1] = {
      name = self:substring_stripped(r),
      pos = r.pos,
      cont = r.cont,
      outer_pos = pos,
      outer_cont = cont,
      manuscript = self
    }
  end
  return cont
end

function ManuscriptContext.init_callbacks.section(self, pos, cs)
  local cont = self:parse_command(pos, cs).cont
  for r in self:argument_items("assignments", pos, cs) do
    local assigns = self:parse_kvlist(r)
    for i = 1, #assigns do
      local key = self:substring_stripped(assigns[i].key)
      local val = assigns[i].value
      if key == "reference" then
        local idx = self.label_index
        idx[#idx + 1] = {
          name = self:substring_stripped(val),
          pos = val.pos,
          cont = val.cont,
          outer_pos = pos,
          outer_cont = cont,
          manuscript = self
        }
      elseif key == "title" then
        local idx = self.section_index
        idx[#idx + 1] = {
          name = self:substring_stripped(val),
          level = self.commands[cs].section_level,
          pos = val.pos,
          cont = val.cont,
          outer_pos = pos,
          outer_cont = cont,
          manuscript = self
        }
      end
    end
  end
  for r in self:argument_items("reference", pos, cs) do
    local idx = self.label_index
    idx[#idx + 1] = {
      name = self:substring_stripped(r),
      pos = r.pos,
      cont = r.cont,
      outer_pos = pos,
      outer_cont = cont,
      manuscript = self
    }
  end
  for r in self:argument_items("text", pos, cs) do
    local idx = self.section_index
    idx[#idx + 1] = {
      name = self:substring_stripped(r),
      level = self.commands[cs].section_level,
      pos = r.pos,
      cont = r.cont,
      outer_pos = pos,
      outer_cont = cont,
      manuscript = self
    }
  end
  return cont
end

function ManuscriptContext.init_callbacks.input(self, pos, cs)
  local cont = self:parse_command(pos, cs).cont
  local idx = self.child_index
  local template = self.commands[cs].filename or "?"
  for r in self:argument_items("file", pos, cs) do
    local f = format_filename_template(template, self:substring_stripped(r))
    local ok = self:add_package(f)
    if not ok then
      idx[#idx + 1] = {
        name = path_normalize(path_join(path_split(self.filename), f)),
        pos = r.pos,
        cont = r.cont,
        manuscript = self
      }
    end
  end
  return cont
end

--* Reference scanning

function ManuscriptContext.scan_references_callbacks.ref(self, pos, cs)
  local cont = self:parse_command(pos, cs).cont
  local idx = self.ref_index
  for s in self:argument_items("reference", pos, cs) do
    idx[#idx + 1] = {
      name = self:substring_stripped(s),
      pos = s.pos,
      cont = s.cont,
      outer_pos = pos,
      outer_cont = cont,
      manuscript = self
    }
  end
  return cont
end

function ManuscriptContext.scan_references_callbacks.cite(self, pos, cs)
  local cont = self:parse_command(pos, cs).cont
  local idx = self.cite_index
  for s in self:argument_items("reference", pos, cs) do
    idx[#idx + 1] = {
      name = self:substring_stripped(s),
      pos = s.pos,
      cont = s.cont,
      outer_pos = pos,
      outer_cont = cont,
      manuscript = self
    }
  end
  return cont
end

return ManuscriptContext
