local harfbuzz = require("shapers.harfbuzz")

local shaper = pl.class(harfbuzz)
shaper._name = "fallback"

local fallbackQueue = pl.class()

function fallbackQueue:_init (text, fallbacks)
  self.fallbacks = fallbacks
  self.runs = {
    {
      options = self:currentOptions(),
      offset = 0,
      start = 1,
      -- WARNING: shaper index is in bytes, not UTF8 aware character
      -- lengths so do *not* use luautf8.len() here
      stop = text:len()
    },
  }
  self._fallbacks = fallbacks
  self.text = text
  self.pending = nil
end

function fallbackQueue:popFallback ()
  return table.remove(self.fallbacks, 1)
end

function fallbackQueue:popRun ()
  self:popFallback()
  return table.remove(self.runs, 1)
end

function fallbackQueue:currentOptions ()
  return self.fallbacks[1]
end

function fallbackQueue:nextFallback ()
  return self.fallbacks[2]
end

function fallbackQueue:currentRun ()
  return self.runs[1]
end

function fallbackQueue:currentText ()
  local run = self:currentRun()
  -- WARNING: shaper index is in bytes, not UTF8 aware character
  -- lengths so do *not* use luautf8.sub() here
  return self.text:sub(run.start, run.stop)
end

function fallbackQueue:addRun (offset, start)
  if not self.pending then
    SU.debug("font-fallback", function ()
      return ("New run pending for %s starting byte %s insert at %s"):format(self:currentText(), start, offset)
    end)
    local options = self:nextFallback()
    if not options then return false end
    options.size = SILE.measurement(options.size):tonumber()
    self.pending = {
      options = options,
      offset = offset,
      start = start
    }
  end
  return true
end

function fallbackQueue:pushNextRun (stop)
  if self.pending then
    SU.debug("font-fallback", function ()
      return ("Push pending run for %s ending at %s"):format(self:currentText(), stop)
    end)
    self.pending.stop = stop
    table.insert(self.runs, self.pending)
    self.pending = nil
  end
end

local activeFallbacks = {}

function shaper:shapeToken (text, options)
  local items = {}
  local fallbackOptions = { options }
  for _, font in ipairs(activeFallbacks) do
    table.insert(fallbackOptions, pl.tablex.merge(options, font, true))
  end
  local shapeQueue = fallbackQueue(text, fallbackOptions)
  repeat -- iterate fallbacks
    SU.debug("font-fallback", function ()
      return ("Start fallback iteration for text '%s'"):format(text)
    end)
    local run = shapeQueue:currentRun()
    local face = run.options.family:len() > 0 and run.options.family or run.options.filename
    local chunk = shapeQueue:currentText()
    SU.debug("font-fallback", function ()
      return ("Try shaping chunk '%s' with '%s'"):format(chunk, face)
    end)
    local candidate_items = self._base.shapeToken(self, chunk, run.options)
    local _index
    for _, item in ipairs(candidate_items) do
      item.fontOptions = run.options
      if item.gid == 0 or item.name == ".null" or item.name == ".notdef" then
        SU.debug("font-fallback", function ()
          return ("Glyph %s not found in %s"):format(item.text, face)
        end)
        local newstart = run.start + item.index
        local pending = shapeQueue:addRun(run.offset, newstart)
        if not pending then
          SU.warn(("Glyph(s) '%s' not available in any fallback font,\n  run with '-d font-fallback' for more detail.\n"):format(item.text))
          run.offset = run.offset + 1
          table.insert(items, run.offset, item) -- output tofu if we're out of fallbacks
        end
      else
        SU.debug("font-fallback", function ()
          return ("Found glyph '%s' in '%s'"):format(item.text, face)
        end)
        shapeQueue:pushNextRun(run.start + item.index - 1) -- if notdef run pending, end it
        if item.index == _index then
          local previous = items[run.offset]
          while previous.next do previous = previous.next end
          previous.next = item
        else
          _index = run.index
          run.offset = run.offset + 1
          table.insert(items, run.offset, item)
        end
      end
    end
    shapeQueue:pushNextRun(run.stop) -- if notdef run pending, end it
    shapeQueue:popRun()
  until not shapeQueue:currentRun()
  return items
end

function shaper:createNnodes (token, options)
  options.tracking = SILE.settings:get("shaper.tracking")
  local items, _ = self:shapeToken(token, options)
  if #items < 1 then return {} end
  local lang = options.language
  SILE.languageSupport.loadLanguage(lang)
  local nodeMaker = SILE.nodeMakers[lang] or SILE.nodeMakers.unicode
  local run = { [1] = { slice = {}, fontOptions = items[1].fontOptions, chunk = "" } }
  for i = 1, #items do
    if items[i].fontOptions ~= run[#run].fontOptions then
      run[#run+1] = { slice = {}, chunk = "", fontOptions = items[i].fontOptions }
      if i <#items then
        run[#run].fontOptions = items[i].fontOptions
      end
    end
    run[#run].chunk = run[#run].chunk .. items[i].text
    run[#run].slice[#(run[#run].slice)+1] = items[i]
  end
  local nodes = {}
  for i=1, #run do
    options = run[i].fontOptions
    SU.debug("font-fallback", "Shaping", run[i].chunk, "in", options.family)
    for node in nodeMaker(options):iterator(run[i].slice, run[i].chunk) do
      nodes[#nodes+1] = node
    end
  end
  SU.debug("font-fallback", nodes)
  return nodes
end

function shaper.clearFallbacks(_)
  activeFallbacks = {}
end

function shaper.addFallback(_, options)
  table.insert(activeFallbacks, options)
end

function shaper.removeFallback(_)
  table.remove(activeFallbacks)
end

function shaper.dumpFallbacks(_)
  return activeFallbacks
end

return shaper
