--------------------------------------------------------------------------------
--- Animation interpolators
-- @module lua-nucleo.util.anim.interpolator
-- This file is a part of lua-nucleo library
-- @copyright (c) lua-nucleo authors (see file `COPYRIGHT` for the license)
--------------------------------------------------------------------------------

-- TODO: Try to abstract from time

local assert, next, ipairs = assert, next, ipairs
local math_min, math_max, math_huge = math.min, math.max, math.huge
local table_sort = table.sort

local lower_bound = import 'lua-nucleo/algorithm.lua' { 'lower_bound' }

-- Need to check value to preserve ordering (table.sort() does not preserve
-- ordering for equal values).
local time_value_less = function(lhs, rhs)
  return (lhs.time < rhs.time) or ((lhs.time == rhs.time) and (lhs.value < rhs.value))
end

-- Assumes looped timeline
local times_and_values_looped = function(time_offset, time_scale)
  return function(data)
    local result = {}

    assert(next(data))

    local min_time = math_huge
    local max_time = -math_huge
    for time, value in pairs(data) do
      time = (time + time_offset) * time_scale -- Normalize time
      min_time = math_min(time, min_time)
      max_time = math_max(time, max_time)
      result[#result + 1] = { time = time; value = value; }
    end

    local period = max_time - min_time
    assert(period > 0, "bad data")

    for i, keyframe in ipairs(result) do
      -- Ensure we always use first loop of our period.
      -- Looks like this properly handles negative numbers as well.
      if keyframe.time ~= max_time then
        keyframe.time = (keyframe.time - min_time) % period
                      + (min_time % period)
      else
        -- TODO: ?! Preserving borderline value to get proper width
        keyframe.time = (keyframe.time - min_time) % period
                      + (min_time % period)
                      + period
      end
    end

    table_sort(result, time_value_less)

    result.period = period
    result.min_time = min_time % period

    return result
  end
end

local find_frame_interval_looped = function(keyframes, time, interval_hint)
  local period = assert(keyframes.period)
  local min_time = assert(keyframes.min_time)

  time = ((time - min_time) % period) + min_time

  -- TODO: Some frame iterator could be good here.

  local next_pos = interval_hint or lower_bound(keyframes, "time", time)
  local next_frame = keyframes[next_pos]
  if not next_frame then
    next_pos = 1
    next_frame = assert(keyframes[next_pos])
  end

  local prev_pos = next_pos - 1
  local prev_frame = keyframes[prev_pos]
  if not prev_frame then
    prev_pos = #keyframes
    prev_frame = assert(keyframes[prev_pos])
  end

  if interval_hint then
    -- We need to check if we're still in hinted interval.
    -- Optimized for case when time increases linearly, and rarely skips frames.
    -- TODO: WTF with that range check?! Note we ought to imitate lower_bound() behavior above.
    local n = 1
    while
      not (
          (time == next_frame.time) or
          (time > prev_frame.time and time < next_frame.time)
        )
    do
      prev_pos = next_pos
      prev_frame = next_frame

      next_pos = prev_pos + 1
      next_frame = keyframes[next_pos]
      if not next_frame then
        next_pos = 1
        next_frame = assert(keyframes[next_pos])
      end

      n = n + 1
      assert(n <= #keyframes, "infinite seeking loop detected") -- Sanity check
    end
  end

  return time, prev_pos, prev_frame, next_pos, next_frame
end

local looped_linear_interpolator = function(keyframes, time, interval_hint)
  local time, prev_pos, prev_frame, next_pos, next_frame = find_frame_interval_looped(keyframes, time, interval_hint)

  local prev_value, next_value = prev_frame.value, next_frame.value
  local prev_time, next_time = prev_frame.time, next_frame.time

  local value =
    prev_value +
    (time - prev_time) *
    (next_value - prev_value) / (next_time - prev_time)

  return value, next_pos
end

local nearest_left_interpolator = function(keyframes, time, interval_hint)
  local time, prev_pos, prev_frame, next_pos, next_frame = find_frame_interval_looped(keyframes, time, interval_hint)

  local value
  if time == next_frame.time then
    value = next_frame.value
  else
    value = prev_frame.value
  end

  return value, next_pos
end

return
{
  times_and_values_looped = times_and_values_looped;
  looped_linear_interpolator = looped_linear_interpolator;
  nearest_left_interpolator = nearest_left_interpolator;
}
