local AddonName = ...
local Private = select(2, ...)

Private.Features:Register({
  id = "undo",
  autoEnable = {"dev", "pr", "alpha"},
  enabled = true,
  persist = true,
})

local TimeMachine = {
  next = {
    forward = {},
    backward = {}
  },
  transaction = false,
  changes = {},
  actions = {},
  effects = {},
  index = 0,
  sub = Private.CreateSubscribableObject(),
}

Private.TimeMachine = TimeMachine

local function resolveKey(data, path)
  if type(path) ~= 'table' then
    return data, path
  end
  local tbl = data
  local i = 1
  while i < #path do
    if tbl[path[i]] == nil then
      tbl[path[i]] = {}
    elseif type(tbl[path[i]]) ~= 'table' then
      error("Path is not valid: " .. table.concat(path, '.') .. " at " .. path[i])
    end
    tbl = tbl[path[i]]
    i = i + 1
  end
  return tbl, path[#path]
end

local function copy(tbl, key)
  if type(tbl[key]) == "table" then
    return CopyTable(tbl[key])
  else
    return tbl[key]
  end
end

function TimeMachine:RegisterEffect(tag, func, idempotent)
  if self.effects[tag] then
    error("Effect already registered: " .. tag)
  end
  self.effects[tag] = {
    idempotent = idempotent,
    func = func
  }
end

TimeMachine:RegisterEffect("add", function(uid, data)
  Private.Add(data)
end, true)

TimeMachine:RegisterEffect("options_cu", function(uid, data)
  if WeakAuras.IsOptionsOpen() then
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end, true)

function TimeMachine:RegisterAction(actionType, actor, inverter, autoEffects)
  if self.actions[actionType] then
    error("Action already registered: " .. actionType)
  end
  self.actions[actionType] = {
    actor = actor,
    inverter = inverter,
    autoEffects = autoEffects,
  }
end
TimeMachine:RegisterAction("none",
  function(_data, _path)
  end,
  function(_data, path)
    return 'none', path, nil
  end
)

TimeMachine:RegisterAction("set",
  function(data, path, value)
    local tbl, key = resolveKey(data, path)
    tbl[key] = value
  end,
  function(data, path)
    local tbl, key = resolveKey(data, path)
    return 'set', path, copy(tbl, key)
  end,
  {"add", "options_cu"}
)

TimeMachine:RegisterAction("setmany",
  function(data, path, values)
    local tbl, key = resolveKey(data, path)
    for k, v in pairs(values) do
      tbl[key][k] = v
    end
  end,
  function(data, path, values)
    local tbl, key = resolveKey(data, path)
    local inverse = {}
    for k, v in pairs(values) do
      inverse[k] = copy(tbl[key], k)
    end
    return 'setmany', path, inverse
  end,
  {"add", "options_cu"}
)

TimeMachine:RegisterAction("insert",
  function(data, path, payload)
    local tbl, key = resolveKey(data, path)
    if payload.index == nil then
      table.insert(tbl[key], payload.value)
    else
      table.insert(tbl[key], payload.index, payload.value)
    end
  end,
  function(data, path, payload)
    return 'remove', path, payload.index
  end,
  {"add", "options_cu"}
)

TimeMachine:RegisterAction("remove",
  function(data, path, payload)
    local tbl, key = resolveKey(data, path)
    if payload == nil then
      table.remove(tbl[key])
    else
      table.remove(tbl[key], payload)
    end
  end,
  function(data, path, payload)
    local tbl, key = resolveKey(data, path)
    return 'insert', path, {index = payload, value = copy(tbl[key], payload or #tbl[key])}
  end,
  {"add", "options_cu"}
)

TimeMachine:RegisterAction("swap",
  function(data, path, payload)
    local tbl, key = resolveKey(data, path)
    tbl[key][payload[1]], tbl[key][payload[2]] = tbl[key][payload[2]], tbl[key][payload[1]]
  end,
  function(data, path, payload)
    return 'swap', path, {payload[2], payload[1]}
  end,
  {"add", "options_cu"}
)

TimeMachine:RegisterAction("move",
  function(data, path, payload)
    local tbl, key = resolveKey(data, path)
    local value = table.remove(tbl, payload[1])
    table.insert(tbl[key], payload[2], value)
  end,
  function(data, path, payload)
    return 'move', path, {payload[2], payload[1]}
  end,
  {"add", "options_cu"}
)

local function keyPathToString(path)
  if type(path) == 'table' then
    return table.concat(path, '.')
  else
    return path
  end
end

local function invertEffects(effects)
  local inverted = {}
  for i = #effects, 1, -1 do
    table.insert(inverted, effects[i])
  end
  return inverted
end

function TimeMachine:StartTransaction()
  if self.transaction then
    WeakAuras.prettyPrint("If you're reading this, a time machine transaction was started, but there was already one in  progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
    self:Reject()
  end
  self.transaction = true
end

function TimeMachine:Append(record)
  local action = self.actions[record.actionType]
  Private.DebugPrint("Forward action", record.actionType, "for", record.uid, "at", keyPathToString(record.path), "with", record.payload)
  if not action then
    error("No action for actionType: " .. record.actionType)
  end
  local inverter = action.inverter
  if not inverter then
    error("No inverter for action: " .. record.actionType)
  end
  local actionType, path, payload = inverter(Private.GetDataByUID(record.uid), record.path, record.payload)
  local inverseRecord = {
    uid = record.uid,
    actionType = actionType,
    path = path,
    payload = payload,
    suppressAutoEffects = record.suppressAutoEffects and CopyTable(record.suppressAutoEffects) or nil,
    effects = record.effects and invertEffects(record.effects) or nil,
  }
  Private.DebugPrint("Backward action", actionType, "for", record.uid, "at", keyPathToString(path), "with", payload)
  table.insert(self.next.forward, record)
  table.insert(self.next.backward, 1, inverseRecord)
  if not self.transaction then
    self:Commit(true)
  end
end

function TimeMachine:AppendMany(records)
  local commit = false
  if not self.transaction then
    self:StartTransaction()
    commit = true
  end
  for _, record in ipairs(records) do
    self:Append(record)
  end
  if commit then
    self:Commit()
  end
end

function TimeMachine:Reject()
  self.next = {
    forward = {},
    backward = {}
  }
  self.transaction = false
end

function TimeMachine:Commit(instant)
  if not self.transaction and not instant then
    WeakAuras.prettyPrint("If you're reading this, a time machine transaction was committed, but there was no transaction in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
    return
  end
  while self.index < #self.changes do
    table.remove(self.changes)
  end
  table.insert(self.changes, self.next)
  self.next = {
    forward = {},
    backward = {}
  }
  self.transaction = false
  return self:StepForward()
end

function TimeMachine:Apply(records, delayedEffects)
  for _, record in ipairs(records) do
    local action = self.actions[record.actionType]
    if not action then
      error("No action for actionType: " .. record.actionType)
    end
    local data = Private.GetDataByUID(record.uid)
    action.actor(data, record.path, record.payload)
    if action.autoEffects or record.effects then
      local effects = {}
      if action.autoEffects then
        for _, effect in ipairs(action.autoEffects) do
          if not record.suppressAutoEffects or not record.suppressAutoEffects[effect] then
            table.insert(effects, effect)
          end
        end
      end
      if record.effects then
        for _, effect in ipairs(record.effects) do
          table.insert(effects, effect)
        end
      end
      for _, effectType in ipairs(effects) do
        local effect = self.effects[effectType]
        if not effect then
          error("No effect for effectType: " .. effect)
        end
        if not delayedEffects or not effect.idempotent then
          if not record.effects or record.suppressAutoEffects then
            effect.func(record.uid, data)
          end
        else
          delayedEffects[record.uid] = delayedEffects[record.uid] or {}
          delayedEffects[record.uid][effectType] = true
        end
      end
    end
  end
  return delayedEffects
end

function TimeMachine:StepForward()
  if self.index < #self.changes then
    self.index = self.index + 1
    self:Apply(self.changes[self.index].forward)
    if self.sub:HasSubscribers("Step") then
      self.sub:Notify("Step", self.index)
    end
  end
end

function TimeMachine:StepBackward()
  if self.index > 0 then
    self:Apply(self.changes[self.index].backward)
    self.index = self.index - 1
    if self.sub:HasSubscribers("Step") then
      self.sub:Notify("Step", self.index)
    end
  end
end

--- much safer than the name suggests!
function TimeMachine:DestroyTheUniverse(id)
  if self.transaction then
    WeakAuras.prettyPrint("If you're reading this, a time machine transaction was destroyed, but there was one in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
    self:Reject()
  end
  if #self.changes > 0 then
    Private.DebugPrint(string.format("Destroying the universe where %i change(s) happpened, because an unexpected change happened to %q.", #self.changes, id))
  end
  self.changes = {}
  self.index = 0
  if self.sub:HasSubscribers("Step") then
    self.sub:Notify("Step", self.index)
  end
end

function TimeMachine:DescribeNext()
  return self.changes[self.index + 1] and self.changes[self.index + 1].forward
end

function TimeMachine:DescribePrevious()
  return self.changes[self.index] and self.changes[self.index].backward
end
