local mlist_to_mml = require'luamml-convert'
local process_mlist = mlist_to_mml.process
local make_root = mlist_to_mml.make_root
local register_family = mlist_to_mml.register_family

local mappings = require'luamml-legacy-mappings'
local write_xml = require'luamml-xmlwriter'
local write_struct = require'luamml-structelemwriter'

local filename_token = token.create'l__luamml_filename_tl'
local label_token = token.create'l__luamml_label_tl'
local left_brace = token.new(string.byte'{', 1)
local right_brace = token.new(string.byte'}', 2)

local output_hook_token
local global_text_families = {}
local text_families_meta = {__index = function(t, fam)
  if fam == nil then return nil end
  local assignment = global_text_families[fam]
  if assignment == nil then
    local fid = node.family_font(fam)
    local fontdir = font.getfont(fid)
    if not fontdir then
      -- FIXME(?): If there is no font...
      error'Please load your fonts?!?'
    end
    assignment = not (fontdir.MathConstants and next(fontdir.MathConstants))
  end
  t[fam] = assignment
  return assignment
end}

local properties = node.get_properties_table()
local mmode, hmode, vmode do
  local result, input = {}, tex.getmodevalues()
  for k,v in next, tex.getmodevalues() do
    if v == 'math' then mmode = k
    elseif v == 'horizontal' then hmode = k
    elseif v == 'vertical' then vmode = k
    else assert(v == 'unset')
    end
  end
  assert(mmode and hmode and vmode)
end

local funcid = luatexbase.new_luafunction'RegisterFamilyMapping'
token.set_lua('RegisterFamilyMapping', funcid, 'protected')
lua.get_functions_table()[funcid] = function()
  local fam = token.scan_int()
  local mapping = token.scan_string()
  if mappings[mapping] then
    register_family(fam, mappings[mapping])
    if global_text_families[fam] == nil then
      global_text_families[fam] = false
    end
  else
    tex.error(string.format('Unknown font mapping %q', mapping))
  end
end

local funcid = luatexbase.new_luafunction'RegisterFamilyMapping'
token.set_lua('RegisterTextFamily', funcid, 'protected')
lua.get_functions_table()[funcid] = function()
  local fam = token.scan_int()
  local _kind = token.scan_string()
  global_text_families[fam] = true
end

local function shallow_copy(t)
  local tt = {}
  for k,v in next, t do
    tt[k] = v
  end
  return tt
end

-- Possible flag values:
--   0: Skip
--   1: Generate MathML, but only save it for later usage in startmath node
--   3: Normal (This is the only supported one in display mode)
--  11: Generate MathML structure elements
--
--  More generally, flags is a bitfield with the defined bits:
--    Bit 5-7: See Bit 4
--    Bit 4: Overwrite mathstyle with bit 9-11
--    Bit 3: Generate MathML structure elements
--    Bit 2: Change root element name for saved element
--    Bit 1: Save MathML as a fully converted formula
--    Bit 0: Save MathML for later usage in startmath node. Ignored for display math.

local out_file

local mlist_result

local undefined_cmd = token.command_id'undefined_cs'
local call_cmd = token.command_id'call'

local labelled_mathml = {}

local function save_result(xml, display, structelem)
  mlist_result = make_root(xml, display and 0 or 2)
  if out_file then
    out_file:write(write_xml(mlist_result, tex.count.l__luamml_pretty_int & 1 == 1):sub(2) .. '\n')
  else
    token.put_next(filename_token)
    local filename = token.scan_argument()
    if filename ~= '' then
      assert(io.open(filename, 'w'))
        :write(write_xml(mlist_result, tex.count.l__luamml_pretty_int & 1 == 1):sub(2) .. '\n')
        :close()
    end
  end
  local tracing = tex.count.tracingmathml > 1
  if tracing then
    texio.write_nl(write_xml(mlist_result, tex.count.l__luamml_pretty_int & 2 == 2) .. '\n')
  end
  if output_hook_token then
    tex.runtoks(function()
      tex.sprint(-2, output_hook_token, left_brace, write_xml(mlist_result, tex.count.l__luamml_pretty_int & 4 == 4), right_brace)
    end)
  end
  if tex.count.l__luamml_flag_int & 8 == 8 then
    write_struct(mlist_result)
  end
  return mlist_result
end

luatexbase.add_to_callback('pre_mlist_to_hlist_filter', function(mlist, style)
  if tex.nest.top.mode == mmode then -- This is a equation label generated with \eqno
    return true
  end
  local flag = tex.count.l__luamml_flag_int
  if flag & 3 == 0 then
    return true
  end
  local display = style == 'display'
  local startmath = tex.nest.top.tail -- Must come before any write_struct calls which adds nodes
  style = flag & 16 == 16 and flag>>5 & 0x7 or display and 0 or 2
  local xml, core = process_mlist(mlist, style, setmetatable({}, text_families_meta))
  if flag & 2 == 2 then
    xml = save_result(shallow_copy(xml), display)
  end
  if flag & 4 == 4 then
    local element_type = token.get_macro'l__luamml_root_tl'
    if element_type ~= 'mrow' then
      if xml[0] == 'mrow' then
        xml[0] = element_type
      else
        xml = {[0] = element_type, xml}
      end
    end
  end
  if not display and flag & 1 == 1 then
    local props = properties[startmath]
    if not props then
      props = {}
      properties[startmath] = props
    end
    props.saved_mathml_table, props.saved_mathml_core = xml, core
    token.put_next(label_token)
    local label = token.scan_argument()
    if label ~= '' then
      if labelled_mathml[label] then
        tex.error('MathML Label already in use', {
            'A MathML expression has a label which is already used by another \z
             formula. If you do not want to label this formula with a unique \z
             label, set a empty label instead.'})
      else
        labelled_mathml[label] = xml
      end
    end
    if flag & 10 == 8 then
      write_struct(xml, true) -- This modifies xml in-place to reference the struture element
    end
  end
  return true
end, 'dump_list')

funcid = luatexbase.new_luafunction'luamml_get_last_mathml_stream:e'
token.set_lua('luamml_get_last_mathml_stream:e', funcid)
lua.get_functions_table()[funcid] = function()
  if not mlist_result then
    tex.error('No current MathML data', {
        "I was asked to provide MathML code for the last formula, but there weren't any new formulas since you last asked."
      })
  end
  local mml = write_xml(mlist_result, tex.count.l__luamml_pretty_int & 8 == 8)
  if tex.count.tracingmathml == 1 then
    texio.write_nl(mml .. '\n')
  end
  tex.sprint(-2, tostring(pdf.immediateobj('stream', mml, '/Subtype/application#2Fmathml+xml' .. token.scan_argument(true))))
  mlist_result = nil
end

funcid = luatexbase.new_luafunction'luamml_begin_single_file:'
token.set_lua('luamml_begin_single_file:', funcid, 'protected')
lua.get_functions_table()[funcid] = function()
  token.put_next(filename_token)
  local filename = token.scan_argument()
  if filename ~= '' then
    out_file = assert(io.open(filename, 'w'))
  end
end

funcid = luatexbase.new_luafunction'luamml_end_single_file:'
token.set_lua('luamml_end_single_file:', funcid, 'protected')
lua.get_functions_table()[funcid] = function()
  if out_file then
    out_file:close()
    out_file = nil
  end
end

funcid = luatexbase.new_luafunction'luamml_register_output_hook:N'
token.set_lua('__luamml_register_output_hook:N', funcid, 'protected')
lua.get_functions_table()[funcid] = function()
  output_hook_token = token.get_next()
end

funcid = luatexbase.new_luafunction'luamml_disable_output_hook:'
token.set_lua('__luamml_disable_output_hook:', funcid, 'protected')
lua.get_functions_table()[funcid] = function()
  output_hook_token = nil
end

local annotate_context = require'luamml-tex-annotate'
annotate_context.data.mathml = labelled_mathml

return {
  save_result = save_result,
  labelled = labelled_mathml,
}