--[[
MIT License

Copyright (c) 2021 Oscar Manglaras

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]

local mp = require 'mp'
local msg = require 'mp.msg'
local utils = require 'mp.utils'
local options = require 'mp.options'

-- Default options
local opts = {
    -- All drawing is scaled by this value, including the text borders and the
    -- cursor. Change it if you have a high-DPI display.
    scale = 1,
    -- Set the font used for the REPL and the console. This probably doesn't
    -- have to be a monospaced font.
    font = "",
    -- Set the font size used for the REPL and the console. This will be
    -- multiplied by "scale."
    font_size = 16,
}

options.read_options(opts, "user_input")

local API_VERSION = "0.1.0"
local API_MAJOR_MINOR = API_VERSION:match("%d+%.%d+")

local co = nil
local queue  = {}
local active_ids = {}
local histories = {}
local request = nil

local line = ''


--[[
    The below code is a modified implementation of text input from mpv's console.lua:
    https://github.com/mpv-player/mpv/blob/7ca14d646c7e405f3fb1e44600e2a67fc4607238/player/lua/console.lua

    Modifications:
        removed support for log messages, sending commands, tab complete, help commands
        removed update timer
        Changed esc key to call handle_esc function
        handle_esc and handle_enter now resume the main coroutine with a response table
        made history specific to request ids
        localised all functions - reordered some to fit
        keybindings use new names
]]--

------------------------------START ORIGINAL MPV CODE-----------------------------------
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------

-- Copyright (C) 2019 the mpv developers
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted, provided that the above
-- copyright notice and this permission notice appear in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
-- SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

local assdraw = require 'mp.assdraw'

local function detect_platform()
    local o = {}
    -- Kind of a dumb way of detecting the platform but whatever
    if mp.get_property_native('options/vo-mmcss-profile', o) ~= o then
        return 'windows'
    elseif mp.get_property_native('options/macos-force-dedicated-gpu', o) ~= o then
        return 'macos'
    elseif os.getenv('WAYLAND_DISPLAY') then
        return 'wayland'
    end
    return 'x11'
end

-- Pick a better default font for Windows and macOS
local platform = detect_platform()
if platform == 'windows' then
    opts.font = 'Consolas'
elseif platform == 'macos' then
    opts.font = 'Menlo'
else
    opts.font = 'monospace'
end

local repl_active = false
local insert_mode = false
local cursor = 1
local key_bindings = {}
local global_margin_y = 0

-- Escape a string for verbatim display on the OSD
local function ass_escape(str)
    -- There is no escape for '\' in ASS (I think?) but '\' is used verbatim if
    -- it isn't followed by a recognised character, so add a zero-width
    -- non-breaking space
    str = str:gsub('\\', '\\\239\187\191')
    str = str:gsub('{', '\\{')
    str = str:gsub('}', '\\}')
    -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of
    -- consecutive newlines
    str = str:gsub('\n', '\239\187\191\\N')
    -- Turn leading spaces into hard spaces to prevent ASS from stripping them
    str = str:gsub('\\N ', '\\N\\h')
    str = str:gsub('^ ', '\\h')
    return str
end

-- Render the REPL and console as an ASS OSD
local function update()
    local dpi_scale = mp.get_property_native("display-hidpi-scale", 1.0)

    dpi_scale = dpi_scale * opts.scale

    local screenx, screeny, aspect = mp.get_osd_size()
    screenx = screenx / dpi_scale
    screeny = screeny / dpi_scale

    -- Clear the OSD if the REPL is not active
    if not repl_active then
        mp.set_osd_ass(screenx, screeny, '')
        return
    end

    local ass = assdraw.ass_new()
    local style = '{\\r' ..
                  '\\1a&H00&\\3a&H00&\\4a&H99&' ..
                  '\\1c&Heeeeee&\\3c&H111111&\\4c&H000000&' ..
                  '\\fn' .. opts.font .. '\\fs' .. opts.font_size ..
                  '\\bord1\\xshad0\\yshad1\\fsp0\\q1}'

    local queue_style = '{\\r' ..
                        '\\1a&H00&\\3a&H00&\\4a&H99&' ..
                        '\\1c&Heeeeee&\\3c&H111111&\\4c&H000000&' ..
                        '\\fn' .. opts.font .. '\\fs' .. opts.font_size .. '\\c&H66ccff&' ..
                        '\\bord1\\xshad0\\yshad1\\fsp0\\q1}'

    -- Create the cursor glyph as an ASS drawing. ASS will draw the cursor
    -- inline with the surrounding text, but it sets the advance to the width
    -- of the drawing. So the cursor doesn't affect layout too much, make it as
    -- thin as possible and make it appear to be 1px wide by giving it 0.5px
    -- horizontal borders.
    local cheight = opts.font_size * 8
    local cglyph = '{\\r' ..
                   '\\1a&H44&\\3a&H44&\\4a&H99&' ..
                   '\\1c&Heeeeee&\\3c&Heeeeee&\\4c&H000000&' ..
                   '\\xbord0.5\\ybord0\\xshad0\\yshad1\\p4\\pbo24}' ..
                   'm 0 0 l 1 0 l 1 ' .. cheight .. ' l 0 ' .. cheight ..
                   '{\\p0}'
    local before_cur = ass_escape(line:sub(1, cursor - 1))
    local after_cur = ass_escape(line:sub(cursor))

    ass:new_event()
    ass:an(1)
    ass:pos(2, screeny - 2 - global_margin_y * screeny)

    if (#queue == 2) then ass:append(queue_style .. string.format("There is 1 more request queued\\N"))
    elseif (#queue > 2) then ass:append(queue_style .. string.format("There are %d more requests queued\\N", #queue-1)) end
    ass:append(style .. request.text .. '\\N')
    ass:append('> ' .. before_cur)
    ass:append(cglyph)
    ass:append(style .. after_cur)

    -- Redraw the cursor with the REPL text invisible. This will make the
    -- cursor appear in front of the text.
    ass:new_event()
    ass:an(1)
    ass:pos(2, screeny - 2)
    ass:append(style .. '{\\alpha&HFF&}> ' .. before_cur)
    ass:append(cglyph)
    ass:append(style .. '{\\alpha&HFF&}' .. after_cur)

    mp.set_osd_ass(screenx, screeny, ass.text)
end

-- Naive helper function to find the next UTF-8 character in 'str' after 'pos'
-- by skipping continuation bytes. Assumes 'str' contains valid UTF-8.
local function next_utf8(str, pos)
    if pos > str:len() then return pos end
    repeat
        pos = pos + 1
    until pos > str:len() or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
    return pos
end

-- As above, but finds the previous UTF-8 charcter in 'str' before 'pos'
local function prev_utf8(str, pos)
    if pos <= 1 then return pos end
    repeat
        pos = pos - 1
    until pos <= 1 or str:byte(pos) < 0x80 or str:byte(pos) > 0xbf
    return pos
end

-- Insert a character at the current cursor position (any_unicode)
local function handle_char_input(c)
    if insert_mode then
        line = line:sub(1, cursor - 1) .. c .. line:sub(next_utf8(line, cursor))
    else
        line = line:sub(1, cursor - 1) .. c .. line:sub(cursor)
    end
    cursor = cursor + #c
    update()
end

-- Remove the character behind the cursor (Backspace)
local function handle_backspace()
    if cursor <= 1 then return end
    local prev = prev_utf8(line, cursor)
    line = line:sub(1, prev - 1) .. line:sub(cursor)
    cursor = prev
    update()
end

-- Remove the character in front of the cursor (Del)
local function handle_del()
    if cursor > line:len() then return end
    line = line:sub(1, cursor - 1) .. line:sub(next_utf8(line, cursor))
    update()
end

-- Toggle insert mode (Ins)
local function handle_ins()
    insert_mode = not insert_mode
end

-- Move the cursor to the next character (Right)
local function next_char(amount)
    cursor = next_utf8(line, cursor)
    update()
end

-- Move the cursor to the previous character (Left)
local function prev_char(amount)
    cursor = prev_utf8(line, cursor)
    update()
end

-- Clear the current line (Ctrl+C)
local function clear()
    line = ''
    cursor = 1
    insert_mode = false
    request.history.pos = #request.history.list + 1
    update()
end

-- Close the REPL if the current line is empty, otherwise do nothing (Ctrl+D)
local function maybe_exit()
    if line == '' then
    else
        handle_del()
    end
end

local function handle_esc()
    coroutine.resume(co, {
        line = nil,
        err = "exited"
    })
end

-- Run the current command and clear the line (Enter)
local function handle_enter()
    if request.history.list[#request.history.list] ~= line and line ~= "" then
        request.history.list[#request.history.list + 1] = line
    end
    coroutine.resume(co, {
        line = line
    })
end

-- Go to the specified position in the command history
local function go_history(new_pos)
    local old_pos = request.history.pos
    request.history.pos = new_pos

    -- Restrict the position to a legal value
    if request.history.pos > #request.history.list + 1 then
        request.history.pos = #request.history.list + 1
    elseif request.history.pos < 1 then
        request.history.pos = 1
    end

    -- Do nothing if the history position didn't actually change
    if request.history.pos == old_pos then
        return
    end

    -- If the user was editing a non-history line, save it as the last history
    -- entry. This makes it much less frustrating to accidentally hit Up/Down
    -- while editing a line.
    if old_pos == #request.history.list + 1 and line ~= '' and request.history.list[#request.history.list] ~= line then
        request.history.list[#request.history.list + 1] = line
    end

    -- Now show the history line (or a blank line for #history + 1)
    if request.history.pos <= #request.history.list then
        line = request.history.list[request.history.pos]
    else
        line = ''
    end
    cursor = line:len() + 1
    insert_mode = false
    update()
end

-- Go to the specified relative position in the command history (Up, Down)
local function move_history(amount)
    go_history(request.history.pos + amount)
end

-- Go to the first command in the command history (PgUp)
local function handle_pgup()
    go_history(1)
end

-- Stop browsing history and start editing a blank line (PgDown)
local function handle_pgdown()
    go_history(#request.history.list + 1)
end

-- Move to the start of the current word, or if already at the start, the start
-- of the previous word. (Ctrl+Left)
local function prev_word()
    -- This is basically the same as next_word() but backwards, so reverse the
    -- string in order to do a "backwards" find. This wouldn't be as annoying
    -- to do if Lua didn't insist on 1-based indexing.
    cursor = line:len() - select(2, line:reverse():find('%s*[^%s]*', line:len() - cursor + 2)) + 1
    update()
end

-- Move to the end of the current word, or if already at the end, the end of
-- the next word. (Ctrl+Right)
local function next_word()
    cursor = select(2, line:find('%s*[^%s]*', cursor)) + 1
    update()
end

-- Move the cursor to the beginning of the line (HOME)
local function go_home()
    cursor = 1
    update()
end

-- Move the cursor to the end of the line (END)
local function go_end()
    cursor = line:len() + 1
    update()
end

-- Delete from the cursor to the beginning of the word (Ctrl+Backspace)
local function del_word()
    local before_cur = line:sub(1, cursor - 1)
    local after_cur = line:sub(cursor)

    before_cur = before_cur:gsub('[^%s]+%s*$', '', 1)
    line = before_cur .. after_cur
    cursor = before_cur:len() + 1
    update()
end

-- Delete from the cursor to the end of the word (Ctrl+Del)
local function del_next_word()
    if cursor > line:len() then return end

    local before_cur = line:sub(1, cursor - 1)
    local after_cur = line:sub(cursor)

    after_cur = after_cur:gsub('^%s*[^%s]+', '', 1)
    line = before_cur .. after_cur
    update()
end

-- Delete from the cursor to the end of the line (Ctrl+K)
local function del_to_eol()
    line = line:sub(1, cursor - 1)
    update()
end

-- Delete from the cursor back to the start of the line (Ctrl+U)
local function del_to_start()
    line = line:sub(cursor)
    cursor = 1
    update()
end

-- Returns a string of UTF-8 text from the clipboard (or the primary selection)
local function get_clipboard(clip)
    if platform == 'x11' then
        local res = utils.subprocess({
            args = { 'xclip', '-selection', clip and 'clipboard' or 'primary', '-out' },
            playback_only = false,
        })
        if not res.error then
            return res.stdout
        end
    elseif platform == 'wayland' then
        local res = utils.subprocess({
            args = { 'wl-paste', clip and '-n' or  '-np' },
            playback_only = false,
        })
        if not res.error then
            return res.stdout
        end
    elseif platform == 'windows' then
        local res = utils.subprocess({
            args = { 'powershell', '-NoProfile', '-Command', [[& {
                Trap {
                    Write-Error -ErrorRecord $_
                    Exit 1
                }

                $clip = ""
                if (Get-Command "Get-Clipboard" -errorAction SilentlyContinue) {
                    $clip = Get-Clipboard -Raw -Format Text -TextFormatType UnicodeText
                } else {
                    Add-Type -AssemblyName PresentationCore
                    $clip = [Windows.Clipboard]::GetText()
                }

                $clip = $clip -Replace "`r",""
                $u8clip = [System.Text.Encoding]::UTF8.GetBytes($clip)
                [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
            }]] },
            playback_only = false,
        })
        if not res.error then
            return res.stdout
        end
    elseif platform == 'macos' then
        local res = utils.subprocess({
            args = { 'pbpaste' },
            playback_only = false,
        })
        if not res.error then
            return res.stdout
        end
    end
    return ''
end

-- Paste text from the window-system's clipboard. 'clip' determines whether the
-- clipboard or the primary selection buffer is used (on X11 and Wayland only.)
local function paste(clip)
    local text = get_clipboard(clip)
    local before_cur = line:sub(1, cursor - 1)
    local after_cur = line:sub(cursor)
    line = before_cur .. text .. after_cur
    cursor = cursor + text:len()
    update()
end

-- List of input bindings. This is a weird mashup between common GUI text-input
-- bindings and readline bindings.
local function get_bindings()
    local bindings = {
        { 'esc',         handle_esc                             },
        { 'enter',       handle_enter                           },
        { 'kp_enter',    handle_enter                           },
        { 'shift+enter', function() handle_char_input('\n') end },
        { 'ctrl+j',      handle_enter                           },
        { 'ctrl+m',      handle_enter                           },
        { 'bs',          handle_backspace                       },
        { 'shift+bs',    handle_backspace                       },
        { 'ctrl+h',      handle_backspace                       },
        { 'del',         handle_del                             },
        { 'shift+del',   handle_del                             },
        { 'ins',         handle_ins                             },
        { 'shift+ins',   function() paste(false) end            },
        { 'mbtn_mid',    function() paste(false) end            },
        { 'left',        function() prev_char() end             },
        { 'ctrl+b',      function() prev_char() end             },
        { 'right',       function() next_char() end             },
        { 'ctrl+f',      function() next_char() end             },
        { 'up',          function() move_history(-1) end        },
        { 'ctrl+p',      function() move_history(-1) end        },
        { 'wheel_up',    function() move_history(-1) end        },
        { 'down',        function() move_history(1) end         },
        { 'ctrl+n',      function() move_history(1) end         },
        { 'wheel_down',  function() move_history(1) end         },
        { 'wheel_left',  function() end                         },
        { 'wheel_right', function() end                         },
        { 'ctrl+left',   prev_word                              },
        { 'alt+b',       prev_word                              },
        { 'ctrl+right',  next_word                              },
        { 'alt+f',       next_word                              },
        { 'ctrl+a',      go_home                                },
        { 'home',        go_home                                },
        { 'ctrl+e',      go_end                                 },
        { 'end',         go_end                                 },
        { 'pgup',        handle_pgup                            },
        { 'pgdwn',       handle_pgdown                          },
        { 'ctrl+c',      clear                                  },
        { 'ctrl+d',      maybe_exit                             },
        { 'ctrl+k',      del_to_eol                             },
        { 'ctrl+u',      del_to_start                           },
        { 'ctrl+v',      function() paste(true) end             },
        { 'meta+v',      function() paste(true) end             },
        { 'ctrl+bs',     del_word                               },
        { 'ctrl+w',      del_word                               },
        { 'ctrl+del',    del_next_word                          },
        { 'alt+d',       del_next_word                          },
        { 'kp_dec',      function() handle_char_input('.') end  },
    }

    for i = 0, 9 do
        bindings[#bindings + 1] =
            {'kp' .. i, function() handle_char_input('' .. i) end}
    end

    return bindings
end

local function text_input(info)
    if info.key_text and (info.event == "press" or info.event == "down"
                          or info.event == "repeat")
    then
        handle_char_input(info.key_text)
    end
end

local function define_key_bindings()
    if #key_bindings > 0 then
        return
    end
    for _, bind in ipairs(get_bindings()) do
        -- Generate arbitrary name for removing the bindings later.
        local name = "_userinput_" .. bind[1]
        key_bindings[#key_bindings + 1] = name
        mp.add_forced_key_binding(bind[1], name, bind[2], {repeatable = true})
    end
    mp.add_forced_key_binding("any_unicode", "_userinput_text", text_input,
        {repeatable = true, complex = true})
    key_bindings[#key_bindings + 1] = "_userinput_text"
end

local function undefine_key_bindings()
    for _, name in ipairs(key_bindings) do
        mp.remove_key_binding(name)
    end
    key_bindings = {}
end

-- Set the REPL visibility ("enable", Esc)
local function set_active(active)
    if active == repl_active then return end
    if active then
        repl_active = true
        insert_mode = false
        define_key_bindings()
    else
        clear()
        repl_active = false
        undefine_key_bindings()
        collectgarbage()
    end
    update()
end


mp.observe_property("user-data/osc/margins", "native", function(_, val)
    if val then
        global_margins = val
    else
        global_margins = { t = 0, b = 0 }
    end
    update()
end)

-- Redraw the REPL when the OSD size changes. This is needed because the
-- PlayRes of the OSD will need to be adjusted.
mp.observe_property('osd-width', 'native', update)
mp.observe_property('osd-height', 'native', update)
mp.observe_property('display-hidpi-scale', 'native', update)

----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------
-------------------------------END ORIGINAL MPV CODE------------------------------------

--[[
    sends a response to the original script in the form of a json string
    it is expected that all requests get a response, if the input is nil then err should say why
    current error codes are:
        exited          the user closed the input instead of pressing Enter
        already_queued  a request with the specified id was already in the queue
        cancelled       a script cancelled the request
        replace         replaced by another request
]]
local function send_response(res)
    if res.source then
        mp.commandv("script-message-to", res.source, res.response, (utils.format_json(res)))
    else
        mp.commandv("script-message", res.response, (utils.format_json(res)))
    end
end

-- push new request onto the queue
-- if a request with the same id already exists and the queueable flag is not enabled then
-- a nil result will be returned to the function
function push_request(req)
    if active_ids[req.id] then
        if req.replace then
            for i, q_req in ipairs(queue) do
                if q_req.id == req.id then
                    send_response{ err = "replaced", response = q_req.response, source = q_req.source }
                    queue[i] = req
                    if i == 1 then request = req end
                end
            end
            update()
            return
        end

        if not req.queueable then
            send_response{ err = "already_queued", response = req.response, source = req.source }
            return
        end
    end

    table.insert(queue, req)
    active_ids[req.id] = (active_ids[req.id] or 0) + 1
    if #queue == 1 then coroutine.resume(co) end
    update()
end

-- safely removes an item from the queue and updates the set of active requests
function remove_request(index)
    local req = table.remove(queue, index)
    active_ids[req.id] = active_ids[req.id] - 1

    if active_ids[req.id] == 0 then active_ids[req.id] = nil end
    return req
end

--an infinite loop that moves through the request queue
--uses a coroutine to handle asynchronous operations
local function driver()
    while (true) do
        while queue[1] do
            request = queue[1]
            line = request.default_input
            cursor = request.cursor_pos

            if repl_active then update()
            else set_active(true) end

            res = coroutine.yield()
            if res then
                res.source, res.response = request.source, request.response
                send_response(res)
                remove_request(1)
            end
        end

        set_active(false)
        coroutine.yield()
    end
end

co = coroutine.create(driver)

--cancels any input request that returns true for the given predicate function
local function cancel_input_request(pred)
    for i = #queue, 1, -1 do
        if pred(i) then
            req = remove_request(i)
            send_response{ err = "cancelled", response = req.response, source = req.source }

            --if we're removing the first item then that means the coroutine is waiting for a response
            --we will need to tell the coroutine to resume, upon which it will move to the next request
            --if there is something in the buffer then save it to the history before erasing it
            if i == 1 then
                local old_line = line
                if old_line ~= "" then table.insert(histories[req.id].list, old_line) end
                clear()
                coroutine.resume(co)
            end
        end
    end
end

mp.register_script_message("cancel-user-input/uid", function(uid)
    cancel_input_request(function(i) return queue[i].response == uid end)
end)

-- removes all requests with the specified id from the queue
mp.register_script_message("cancel-user-input/id", function(id)
    cancel_input_request(function(i) return queue[i].id == id end)
end)

-- ensures a request has the correct fields and is correctly formatted
local function format_request_fields(req)
    assert(req.version, "input requests require an API version string")
    if not string.find(req.version, API_MAJOR_MINOR, 1, true) then
        error(("input request has invalid version: expected %s.x, got %s"):format(API_MAJOR_MINOR, req.version))
    end

    assert(req.response, "input requests require a response string")
    assert(req.id, "input requests require an id string")

    req.text = ass_escape(req.request_text or "")
    req.default_input = req.default_input or ""
    req.cursor_pos = tonumber(req.cursor_pos) or 1
    req.id = req.id or "mpv"

    if req.cursor_pos ~= 1 then
        if req.cursor_pos  < 1 then req.cursor_pos  = 1
        elseif req.cursor_pos  > #req.default_input then req.cursor_pos  = #req.default_input + 1 end
    end

    if not histories[req.id] then histories[req.id] = {pos = 1, list = {}} end
    req.history = histories[req.id]
    return req
end

-- updates the fields of a specific request
mp.register_script_message("update-user-input/uid", function(uid, req_opts)
    req_opts = utils.parse_json(req_opts)
    req_opts.response = uid
    for i, req in ipairs(queue) do
        if req.response == uid then
            local success, result = pcall(format_request_fields, req_opts)
            if not success then return msg.error(result) end

            queue[i] = result
            if i == 1 then request = queue[1] end
            update()
            return
        end
    end
end)

--the function that parses the input requests
local function input_request(req)
    req = format_request_fields(req)
    push_request(req)
end

-- script message to recieve input requests, get-user-input.lua acts as an interface to call this script message
mp.register_script_message("request-user-input", function(req)
    msg.debug(req)
    req = utils.parse_json(req)
    local success, err = pcall(input_request, req)
    if not success then
        send_response{ err = err, response = req.response, source = req.source}
        msg.error(err)
    end
end)