-- frame-seek.lua
-- Allows seeking to a specific frame number or timestamp

local input = require("mp.input")

local jump_mode = nil -- "frame" or "time"
local relative = false
local minus = false
local fps = 0

function parse_timestamp(input_str)
    -- Formats:
    -- HH:MM:SS.ms
    -- MM:SS.ms
    -- SS.ms
    -- .ms

    -- More than 60 minutes or seconds can be entered - it will seek any amount accurately

    -- First try to match HH:MM:SS.ms
    local hours, minutes, seconds = input_str:match("^(%d+):(%d+):(%d+%.?%d*)$")
    if hours and minutes and seconds then
        return tonumber(hours) * 3600 + tonumber(minutes) * 60 + tonumber(seconds)
    end

    -- Try to match MM:SS.ms
    local minutes, seconds = input_str:match("^(%d+):(%d+%.?%d*)$")
    if minutes and seconds then
        return tonumber(minutes) * 60 + tonumber(seconds)
    end

    -- Try to match just seconds (with or without decimal)
    local seconds = input_str:match("^(%d+%.?%d*)$")
    if seconds then
        return tonumber(seconds)
    end

    local milliseconds = input_str:match("^%.(%d+)$")
    if milliseconds ~= nil then
        return tonumber("0." .. milliseconds)
    end

    return nil
end

function seek_to_frame(frame_num)
    fps = mp.get_property_number("estimated-vf-fps")
    if not fps or fps <= 0 then
        mp.osd_message("Error: Cannot determine framerate")
        return
    end

    local timestamp = frame_num / fps

    seek_to_timestamp(timestamp)
end

function seek_to_timestamp(timestamp)
    if minus then timestamp = -timestamp end

    local cur_time = mp.get_property_number("time-pos")
    if not cur_time then return end

    if relative then
        if timestamp == 0 then return end

        if math.abs(timestamp) < 10 then
            mp.commandv("seek", timestamp, "exact")
        else
            -- Only show OSD if seek >10s
            mp.command("seek " .. timestamp .. " exact")
        end
    else
        -- Handle imprecise float
        if math.abs(timestamp - cur_time) < 1e-7 then return end
        mp.command("seek " .. timestamp .. " absolute+exact")
    end

    mp.observe_property("time-pos", "number", display_osd_message)
end

function display_osd_message(_, timestamp)
    if timestamp == nil then return end
    mp.unobserve_property(display_osd_message)

    -- Format the display nicely
    local hours = math.floor(timestamp / 3600)
    local minutes = math.floor((timestamp % 3600) / 60)
    local seconds = math.floor(timestamp % 60)
    local milliseconds = math.floor((timestamp % 1) * 1000 + 0.5)

    local display_time = string.format("%02d:%02d", minutes, seconds)

    if hours ~= 0 then
        display_time = string.format("%d:", hours) .. display_time
    end

    if milliseconds ~= 0 or jump_mode == "frame" then
        display_time = display_time .. string.format(".%03d", milliseconds)
    end

    if jump_mode == "frame" and fps and fps > 0 then
        local frame_num = math.floor(timestamp * fps + 0.5)
        mp.osd_message(string.format("Seeking to frame %d (%s)", frame_num, display_time))
    else
        mp.osd_message(string.format("Seeking to %s", display_time))
    end
end

function jump_submit(input)
    if not input or input == "" then
        reset()
        return
    end

    -- Handle relative marker
    if input:sub(1, 1) == "r" then
        relative = true
        input = input:sub(2)
    end

    -- Handle negative input
    minus = false
    if input:sub(1, 1) == "-" then
        minus = true
        input = input:sub(2)
    end

    if jump_mode == "frame" then
        local frame_num = tonumber(input)
        if frame_num then
            seek_to_frame(math.floor(frame_num))
        else
            mp.osd_message("Invalid frame number")
        end
    elseif jump_mode == "time" then
        local timestamp = parse_timestamp(input)
        if timestamp then
            seek_to_timestamp(timestamp)
        else
            mp.osd_message("Invalid timestamp format")
        end
    end
end

function reset()
    jump_mode = nil
    relative = false
    minus = false
end

function run_script(mode, prompt, relative_flag)
    if mp.get_property("path") == nil then return end

    reset()
    jump_mode = mode
    relative = relative_flag

    input.get({
        prompt = prompt,
        submit = jump_submit,
    })
end

-- Register key bindings
mp.add_key_binding("ctrl+t", "seek-timestamp", function()
    run_script("time", "Seek to time:", false)
end)

mp.add_key_binding("ctrl+T", "seek-frame", function()
    run_script("frame", "Seek to frame:", false)
end)

mp.add_key_binding(nil, "seek-timestamp-relative", function()
    run_script("time", "Seek forward by time:", true)
end)

mp.add_key_binding(nil, "seek-frame-relative", function()
    run_script("frame", "Seek forward by frame:", true)
end)
