-- created by Bertrand Legruley
-- lestrucsetastucesdetux@free.fr
-- lestrucsetastucesdetux.pages-perso.free.fr

local mp = require 'mp'

local grid = {
    "1", "2", "3", "4", "5",
    "6", "7", "8", "9", "0",
    "V", "<", "X"
}

local layout = {
    {x = 398, y = 472}, {x = 449, y = 472}, {x = 500, y = 472}, {x = 551, y = 472}, {x = 602, y = 472},
    {x = 398, y = 529}, {x = 449, y = 529}, {x = 500, y = 529}, {x = 551, y = 529}, {x = 602, y = 529},
    {x = 449, y = 621}, {x = 500, y = 620}, {x = 551, y = 621}
}

local state = {
    active = false,
    blink_timer = nil,
    bound_keys = {},
    error_msg = nil,
    input_buffer = "",
    is_clicking = false,
    old_osd_level = 1,
    selection = nil,
    show_error = false
}

local mouse_timer = nil

local function draw_numpad()
    if not state.active then return end

    local header_text = ""
    if state.error_msg then
        header_text = state.show_error and (string.format("{\\1c&H0098FF&}%s", state.error_msg)) or state.input_buffer
    elseif state.input_buffer == "" then
        header_text = "{\\1c&HE8731A&}?"
    else
        header_text = state.input_buffer
    end

    local ass = string.format("{\\an7\\pos(0,0)\\p1\\1c&H000000&\\1a&H20&\\bord0\\shad0}m 0 0 l 1000 0 l 1000 1000 l 0 1000{\\p0}")
    ass = ass .. string.format("\n{\\an5\\pos(500,380)\\q2}%s", header_text)

    local playlist = mp.get_property_native("playlist")
    local count = playlist and #playlist or 0

    for i, pos in ipairs(layout) do
        local char = grid[i]
        local is_hidden = false

        if char == "<" then
            local can_go_higher = false
            if state.input_buffer ~= "" and not state.error_msg then
                for j = 0, 9 do
                    if tonumber(state.input_buffer .. j) <= count then
                        can_go_higher = true
                        break
                    end
                end
            end
            if not can_go_higher then is_hidden = true end
        end

        if not is_hidden then
            local color = "FFFFFF"
            if char == "V" then
                color = "00FF00"
            elseif char == "X" then
                color = "0000FF"
            end

            if i == state.selection then
                if char == "V" or char == "X" then
                    color = "FFFFFF"
                else
                    color = "E8731A"
                end
            end

            ass = ass .. string.format("\n{\\an5\\pos(%d,%d)\\fnConsolas\\1c&H%s&}%s", pos.x, pos.y, color, char)
        end
    end

    mp.set_osd_ass(1000, 1000, ass)
end

local function stop_blink(no_draw)
    if state.blink_timer then
        state.blink_timer:kill()
        state.blink_timer = nil
    end
    state.error_msg = nil
    state.show_error = false
    if not no_draw then draw_numpad() end
end

local function start_blink(msg)
    stop_blink()
    state.error_msg = msg
    state.show_error = true
    state.blink_timer = mp.add_periodic_timer(1.0, function()
        state.show_error = not state.show_error
        draw_numpad()
    end)
    draw_numpad()
end

local function exit_numpad()
    stop_blink()
    if mouse_timer then
        mouse_timer:kill()
        mouse_timer = nil
    end
    for _, key in ipairs(state.bound_keys) do
        mp.remove_key_binding(key)
    end
    state.active = false
    state.bound_keys = {}
    state.is_clicking = false
    state.selection = nil
    mp.commandv("script-message", "numpad-visible", "no")
    mp.set_osd_ass(0, 0, "")
    mp.set_property_number("osd-level", state.old_osd_level)
end

local function validate_input()
    if state.input_buffer ~= "" then
        local num = tonumber(state.input_buffer)
        local playlist = mp.get_property_native("playlist")
        local count = playlist and #playlist or 0
        local target = num and (num - 1) or -1
        if target >= 0 and target < count then
            mp.set_property_number("playlist-pos", target)
            exit_numpad()
        else
            start_blink("Invalid")
        end
    else
        exit_numpad()
    end
end

local function handle_left_click()
    if not state.selection then return end

    local char = grid[state.selection]
    local playlist = mp.get_property_native("playlist")
    local count = playlist and #playlist or 0

    state.is_clicking = false

    if char == "X" then
        exit_numpad()
        return
    end

    if state.error_msg then
        state.input_buffer = ""
        stop_blink(true)
        if char == "V" then
            draw_numpad()
            return
        end
    end

    if char == "V" then
        validate_input()
    elseif char == "<" then
        if state.input_buffer ~= "" then
            state.input_buffer = state.input_buffer:sub(1, -2)
            draw_numpad()
        end
    else
        state.input_buffer = state.input_buffer .. char
        draw_numpad()

        local can_go_higher = false
        local current_digits = string.len(state.input_buffer)
        local max_digits = string.len(tostring(count))

        for i = 0, 9 do
            local next_val = tonumber(state.input_buffer .. i)
            if next_val and next_val <= count then
                can_go_higher = true
                break
            end
        end

        if current_digits >= max_digits or not can_go_higher then
            mp.add_timeout(0.5, function()
                if state.active then validate_input() end
            end)
        end
    end
end

local function update_mouse_hover()
    if not state.active or state.is_clicking then return end
    local mx, my = mp.get_mouse_pos()
    local ww, wh = mp.get_osd_size()
    if ww <= 0 or wh <= 0 then return end
    local vx, vy = mx * 1000 / ww, my * 1000 / wh
    local new_sel = nil
    for i, pos in ipairs(layout) do
        if vx > pos.x - 22 and vx < pos.x + 22 and vy > pos.y - 22 and vy < pos.y + 22 then
            local char = grid[i]
            if char == "<" then
                local playlist = mp.get_property_native("playlist")
                local count = playlist and #playlist or 0
                local can_go_higher = false
                if state.input_buffer ~= "" and not state.error_msg then
                    for j = 0, 9 do
                        if tonumber(state.input_buffer .. j) <= count then
                            can_go_higher = true
                            break
                        end
                    end
                end
                if can_go_higher then new_sel = i end
            else
                new_sel = i
            end
            break
        end
    end
    if new_sel ~= state.selection then
        state.selection = new_sel
        draw_numpad()
    end
end

local function toggle_numpad()
    if state.active then
        exit_numpad()
        return
    end

    state.active = true
    state.input_buffer = ""
    state.old_osd_level = mp.get_property_number("osd-level", 1)
    state.selection = nil

    mp.commandv("script-message", "numpad-visible", "yes")

    local function bind(key, name, fn, flags)
        local id = "numpad_" .. name
        mp.add_forced_key_binding(key, id, fn, flags)
        table.insert(state.bound_keys, id)
    end

    local function switch_to_playlist()
        exit_numpad()
        mp.commandv("script-message", "open-real-playlist")
    end

    local long_press_timer = nil

    bind("ESC", "esc", exit_numpad)
    bind("SPACE", "space_ignore", function() end)
    bind("MBTN_MID", "mclk_exit", exit_numpad)

    bind("MBTN_LEFT", "lclk", function(e)
        if e.event == "down" then
            state.is_clicking = true
            if not state.selection then
                long_press_timer = mp.add_timeout(0.5, function()
                    long_press_timer = nil
                    switch_to_playlist()
                end)
            end
            draw_numpad()
        elseif e.event == "up" then
            state.is_clicking = false
            if long_press_timer then
                long_press_timer:kill()
                long_press_timer = nil
            end
            if state.selection then
                handle_left_click()
            end
            draw_numpad()
        end
    end, {complex = true})

    bind("MBTN_LEFT_DBL", "ldbl", function()
        if not state.selection then
            mp.command("cycle fullscreen")
        end
    end)

    bind("MBTN_RIGHT", "rclk_val", function()
        if state.error_msg then
            stop_blink()
            state.input_buffer = ""
            draw_numpad()
        else
            validate_input()
        end
    end)

    mouse_timer = mp.add_periodic_timer(0.05, update_mouse_hover)
    draw_numpad()
end

mp.add_forced_key_binding("MBTN_RIGHT", "numpad_trigger", function(e)
    if state.active then return end
    if e.event == "down" then
        right_click_timer = mp.add_timeout(0.5, function()
            toggle_numpad()
            right_click_timer = nil
        end)
    elseif e.event == "up" then
        if right_click_timer then
            right_click_timer:kill()
            right_click_timer = nil
            mp.command("cycle pause")
        end
    end
end, {complex = true})

mp.observe_property("osd-height", "native", draw_numpad)
mp.observe_property("osd-width", "native", draw_numpad)

mp.register_script_message("playlist-visible", function(visible)
    if visible == "yes" and state.active then exit_numpad() end
end)

mp.register_script_message("toggle-numpad", toggle_numpad)
