-- Compatibility shim for lua 5.2/5.3
unpack = unpack or table.unpack

-- these are used internally by lua.c
mp.UNKNOWN_TYPE.info = "this value is inserted if the C type is not supported"
mp.UNKNOWN_TYPE.type = "UNKNOWN_TYPE"

mp.ARRAY.info = "native array"
mp.ARRAY.type = "ARRAY"

mp.MAP.info = "native map"
mp.MAP.type = "MAP"

function mp.get_script_name()
    return mp.script_name
end

function mp.get_opt(key, def)
    local opts = mp.get_property_native("options/script-opts")
    local val = opts[key]
    if val == nil then
        val = def
    end
    return val
end

function mp.input_define_section(section, contents, flags)
    if flags == nil or flags == "" then
        flags = "default"
    end
    mp.commandv("define-section", section, contents, flags)
end

function mp.input_enable_section(section, flags)
    if flags == nil then
        flags = ""
    end
    mp.commandv("enable-section", section, flags)
end

function mp.input_disable_section(section)
    mp.commandv("disable-section", section)
end

function mp.get_mouse_pos()
    local m = mp.get_property_native("mouse-pos")
    return m.x, m.y
end

-- For dispatching script-binding. This is sent as:
--      script-message-to $script_name $binding_name $keystate
-- The array is indexed by $binding_name, and has functions like this as value:
--      fn($binding_name, $keystate)
local dispatch_key_bindings = {}

local message_id = 0
local function reserve_binding()
    message_id = message_id + 1
    return "__keybinding" .. tostring(message_id)
end

local function dispatch_key_binding(name, state, key_name, key_text)
    local fn = dispatch_key_bindings[name]
    if fn then
        fn(name, state, key_name, key_text)
    end
end

-- "Old", deprecated API

-- each script has its own section, so that they don't conflict
local default_section = "input_dispatch_" .. mp.script_name

-- Set the list of key bindings. These will override the user's bindings, so
-- you should use this sparingly.
-- A call to this function will remove all bindings previously set with this
-- function. For example, set_key_bindings({}) would remove all script defined
-- key bindings.
-- Note: the bindings are not active by default. Use enable_key_bindings().
--
-- list is an array of key bindings, where each entry is an array as follow:
--      {key, callback_press, callback_down, callback_up}
-- key is the key string as used in input.conf, like "ctrl+a"
--
-- callback can be a string too, in which case the following will be added like
-- an input.conf line: key .. " " .. callback
-- (And callback_down is ignored.)
function mp.set_key_bindings(list, section, flags)
    local cfg = ""
    for i = 1, #list do
        local entry = list[i]
        local key = entry[1]
        local cb = entry[2]
        local cb_down = entry[3]
        local cb_up = entry[4]
        if type(cb) ~= "string" then
            local mangle = reserve_binding()
            dispatch_key_bindings[mangle] = function(name, state)
                local event = state:sub(1, 1)
                local is_mouse = state:sub(2, 2) == "m"
                local def = (is_mouse and "u") or "d"
                if event == "r" then
                    return
                end
                if event == "p" and cb then
                    cb()
                elseif event == "d" and cb_down then
                    cb_down()
                elseif event == "u" and cb_up then
                    cb_up()
                elseif event == def and cb then
                    cb()
                end
            end
            cfg = cfg .. key .. " script-binding " ..
                  mp.script_name .. "/" .. mangle .. "\n"
        else
            cfg = cfg .. key .. " " .. cb .. "\n"
        end
    end
    mp.input_define_section(section or default_section, cfg, flags)
end

function mp.enable_key_bindings(section, flags)
    mp.input_enable_section(section or default_section, flags)
end

function mp.disable_key_bindings(section)
    mp.input_disable_section(section or default_section)
end

function mp.set_mouse_area(x0, y0, x1, y1, section)
    mp.input_set_section_mouse_area(section or default_section, x0, y0, x1, y1)
end

-- "Newer" and more convenient API

local key_bindings = {}
local key_binding_counter = 0
local key_bindings_dirty = false

function mp.flush_keybindings()
    if not key_bindings_dirty then
        return
    end
    key_bindings_dirty = false

    for i = 1, 2 do
        local section, flags
        local def = i == 1
        if def then
            section = "input_" .. mp.script_name
            flags = "default"
        else
            section = "input_forced_" .. mp.script_name
            flags = "force"
        end
        local bindings = {}
        for k, v in pairs(key_bindings) do
            if v.bind and v.forced ~= def then
                bindings[#bindings + 1] = v
            end
        end
        table.sort(bindings, function(a, b)
            return a.priority < b.priority
        end)
        local cfg = ""
        for _, v in ipairs(bindings) do
            cfg = cfg .. v.bind .. "\n"
        end
        mp.input_define_section(section, cfg, flags)
        -- TODO: remove the section if the script is stopped
        mp.input_enable_section(section, "allow-hide-cursor+allow-vo-dragging")
    end
end

local function add_binding(attrs, key, name, fn, rp)
    if (type(name) ~= "string") and (name ~= nil) then
        rp = fn
        fn = name
        name = nil
    end
    rp = rp or ""
    if name == nil then
        name = reserve_binding()
    end
    local repeatable = rp == "repeatable" or rp["repeatable"]
    if rp["forced"] then
        attrs.forced = true
    end
    local key_cb, msg_cb
    if not fn then
        fn = function() end
    end
    if rp["complex"] then
        local key_states = {
            ["u"] = "up",
            ["d"] = "down",
            ["r"] = "repeat",
            ["p"] = "press",
        }
        key_cb = function(name, state, key_name, key_text)
            if key_text == "" then
                key_text = nil
            end
            fn({
                event = key_states[state:sub(1, 1)] or "unknown",
                is_mouse = state:sub(2, 2) == "m",
                key_name = key_name,
                key_text = key_text,
            })
        end
        msg_cb = function()
            fn({event = "press", is_mouse = false})
        end
    else
        key_cb = function(name, state)
            -- Emulate the same semantics as input.c uses for most bindings:
            -- For keyboard, "down" runs the command, "up" does nothing;
            -- for mouse, "down" does nothing, "up" runs the command.
            -- Also, key repeat triggers the binding again.
            local event = state:sub(1, 1)
            local is_mouse = state:sub(2, 2) == "m"
            if event == "r" and not repeatable then
                return
            end
            if is_mouse and (event == "u" or event == "p") then
                fn()
            elseif (not is_mouse) and (event == "d" or event == "r" or event == "p") then
                fn()
            end
        end
        msg_cb = fn
    end
    if key and #key > 0 then
        attrs.bind = key .. " script-binding " .. mp.script_name .. "/" .. name
    end
    attrs.name = name
    -- new bindings override old ones (but do not overwrite them)
    key_binding_counter = key_binding_counter + 1
    attrs.priority = key_binding_counter
    key_bindings[name] = attrs
    key_bindings_dirty = true
    dispatch_key_bindings[name] = key_cb
    mp.register_script_message(name, msg_cb)
end

function mp.add_key_binding(...)
    add_binding({forced=false}, ...)
end

function mp.add_forced_key_binding(...)
    add_binding({forced=true}, ...)
end

function mp.remove_key_binding(name)
    key_bindings[name] = nil
    dispatch_key_bindings[name] = nil
    key_bindings_dirty = true
    mp.unregister_script_message(name)
end

local timers = {}

local timer_mt = {}
timer_mt.__index = timer_mt

function mp.add_timeout(seconds, cb)
    local t = mp.add_periodic_timer(seconds, cb)
    t.oneshot = true
    return t
end

function mp.add_periodic_timer(seconds, cb)
    local t = {
        timeout = seconds,
        cb = cb,
        oneshot = false,
    }
    setmetatable(t, timer_mt)
    t:resume()
    return t
end

function timer_mt.stop(t)
    if timers[t] then
        timers[t] = nil
        t.next_deadline = t.next_deadline - mp.get_time()
    end
end

function timer_mt.kill(t)
    timers[t] = nil
    t.next_deadline = nil
end
mp.cancel_timer = timer_mt.kill

function timer_mt.resume(t)
    if not timers[t] then
        local timeout = t.next_deadline
        if timeout == nil then
            timeout = t.timeout
        end
        t.next_deadline = mp.get_time() + timeout
        timers[t] = t
    end
end

function timer_mt.is_enabled(t)
    return timers[t] ~= nil
end

-- Return the timer that expires next.
local function get_next_timer()
    local best = nil
    for t, _ in pairs(timers) do
        if (best == nil) or (t.next_deadline < best.next_deadline) then
            best = t
        end
    end
    return best
end

function mp.get_next_timeout()
    local timer = get_next_timer()
    if not timer then
        return
    end
    local now = mp.get_time()
    return timer.next_deadline - now
end

-- Run timers that have met their deadline at the time of invocation.
-- Return: time>0 in seconds till the next due timer, 0 if there are due timers
--         (aborted to avoid infinite loop), or nil if no timers
local function process_timers()
    local t0 = nil
    while true do
        local timer = get_next_timer()
        if not timer then
            return
        end
        local now = mp.get_time()
        local wait = timer.next_deadline - now
        if wait > 0 then
            return wait
        else
            if not t0 then
                t0 = now  -- first due callback: always executes, remember t0
            elseif timer.next_deadline > t0 then
                -- don't block forever with slow callbacks and endless timers.
                -- we'll continue right after checking mpv events.
                return 0
            end

            if timer.oneshot then
                timer:kill()
            else
                timer.next_deadline = now + timer.timeout
            end
            timer.cb()
        end
    end
end

local messages = {}

function mp.register_script_message(name, fn)
    messages[name] = fn
end

function mp.unregister_script_message(name)
    messages[name] = nil
end

local function message_dispatch(ev)
    if #ev.args > 0 then
        local handler = messages[ev.args[1]]
        if handler then
            handler(unpack(ev.args, 2))
        end
    end
end

local property_id = 0
local properties = {}

function mp.observe_property(name, t, cb)
    local id = property_id + 1
    property_id = id
    properties[id] = cb
    mp.raw_observe_property(id, name, t)
end

function mp.unobserve_property(cb)
    for prop_id, prop_cb in pairs(properties) do
        if cb == prop_cb then
            properties[prop_id] = nil
            mp.raw_unobserve_property(prop_id)
        end
    end
end

local function property_change(ev)
    local prop = properties[ev.id]
    if prop then
        prop(ev.name, ev.data)
    end
end

-- used by default event loop (mp_event_loop()) to decide when to quit
mp.keep_running = true

local event_handlers = {}

function mp.register_event(name, cb)
    local list = event_handlers[name]
    if not list then
        list = {}
        event_handlers[name] = list
    end
    list[#list + 1] = cb
    return mp.request_event(name, true)
end

function mp.unregister_event(cb)
    for name, sub in pairs(event_handlers) do
        local found = false
        for i, e in ipairs(sub) do
            if e == cb then
                found = true
                break
            end
        end
        if found then
            -- create a new array, just in case this function was called
            -- from an event handler
            local new = {}
            for i = 1, #sub do
                if sub[i] ~= cb then
                    new[#new + 1] = sub[i]
                end
            end
            event_handlers[name] = new
            if #new == 0 then
                mp.request_event(name, false)
            end
        end
    end
end

-- default handlers
mp.register_event("shutdown", function() mp.keep_running = false end)
mp.register_event("client-message", message_dispatch)
mp.register_event("property-change", property_change)

-- called before the event loop goes back to sleep
local idle_handlers = {}

function mp.register_idle(cb)
    idle_handlers[#idle_handlers + 1] = cb
end

function mp.unregister_idle(cb)
    local new = {}
    for _, handler in ipairs(idle_handlers) do
        if handler ~= cb then
            new[#new + 1] = handler
        end
    end
    idle_handlers = new
end

-- sent by "script-binding"
mp.register_script_message("key-binding", dispatch_key_binding)

mp.msg = {
    log = mp.log,
    fatal = function(...) return mp.log("fatal", ...) end,
    error = function(...) return mp.log("error", ...) end,
    warn = function(...) return mp.log("warn", ...) end,
    info = function(...) return mp.log("info", ...) end,
    verbose = function(...) return mp.log("v", ...) end,
    debug = function(...) return mp.log("debug", ...) end,
    trace = function(...) return mp.log("trace", ...) end,
}

_G.print = mp.msg.info

package.loaded["mp"] = mp
package.loaded["mp.msg"] = mp.msg

function mp.wait_event(t)
    local r = mp.raw_wait_event(t)
    if r and r.file_error and not r.error then
        -- compat; deprecated
        r.error = r.file_error
    end
    return r
end

_G.mp_event_loop = function()
    mp.dispatch_events(true)
end

local function call_event_handlers(e)
    local handlers = event_handlers[e.event]
    if handlers then
        for _, handler in ipairs(handlers) do
            handler(e)
        end
    end
end

mp.use_suspend = false

local suspend_warned = false

function mp.dispatch_events(allow_wait)
    local more_events = true
    if mp.use_suspend then
        if not suspend_warned then
            mp.msg.error("mp.use_suspend is now ignored.")
            suspend_warned = true
        end
    end
    while mp.keep_running do
        local wait = 0
        if not more_events then
            wait = process_timers() or 1e20 -- infinity for all practical purposes
            if wait ~= 0 then
                local idle_called = nil
                for _, handler in ipairs(idle_handlers) do
                    idle_called = true
                    handler()
                end
                if idle_called then
                    -- handlers don't complete in 0 time, and may modify timers
                    wait = mp.get_next_timeout() or 1e20
                    if wait < 0 then
                        wait = 0
                    end
                end
            end
            -- Resume playloop - important especially if an error happened while
            -- suspended, and the error was handled, but no resume was done.
            mp.resume_all()
            if allow_wait ~= true then
                return
            end
        end
        local e = mp.wait_event(wait)
        more_events = false
        if e.event ~= "none" then
            call_event_handlers(e)
            more_events = true
        end
    end
end

mp.register_idle(mp.flush_keybindings)

-- additional helpers

function mp.osd_message(text, duration)
    if not duration then
        duration = "-1"
    else
        duration = tostring(math.floor(duration * 1000))
    end
    mp.commandv("show-text", text, duration)
end

local hook_table = {}

local hook_mt = {}
hook_mt.__index = hook_mt

function hook_mt.cont(t)
    if t._id == nil then
        mp.msg.error("hook already continued")
    else
        mp.raw_hook_continue(t._id)
        t._id = nil
    end
end

function hook_mt.defer(t)
    t._defer = true
end

mp.register_event("hook", function(ev)
    local fn = hook_table[tonumber(ev.id)]
    local hookobj = {
        _id = ev.hook_id,
        _defer = false,
    }
    setmetatable(hookobj, hook_mt)
    if fn then
        fn(hookobj)
    end
    if (not hookobj._defer) and hookobj._id ~= nil then
        hookobj:cont()
    end
end)

function mp.add_hook(name, pri, cb)
    local id = #hook_table + 1
    hook_table[id] = cb
    -- The C API suggests using 0 for a neutral priority, but lua.rst suggests
    -- 50 (?), so whatever.
    mp.raw_hook_add(id, name, pri - 50)
end

local async_call_table = {}
local async_next_id = 1

function mp.command_native_async(node, cb)
    local id = async_next_id
    async_next_id = async_next_id + 1
    local res, err = mp.raw_command_native_async(id, node)
    if not res then
        cb(false, nil, err)
        return res, err
    end
    local t = {cb = cb, id = id}
    async_call_table[id] = t
    return t
end

mp.register_event("command-reply", function(ev)
    local id = tonumber(ev.id)
    local t = async_call_table[id]
    local cb = t.cb
    t.id = nil
    async_call_table[id] = nil
    if ev.error then
        cb(false, nil, ev.error)
    else
        cb(true, ev.result, nil)
    end
end)

function mp.abort_async_command(t)
    if t.id ~= nil then
        mp.raw_abort_async_command(t.id)
    end
end

local overlay_mt = {}
overlay_mt.__index = overlay_mt
local overlay_new_id = 0

function mp.create_osd_overlay(format)
    overlay_new_id = overlay_new_id + 1
    local overlay = {
        format = format,
        id = overlay_new_id,
        data = "",
        res_x = 0,
        res_y = 720,
    }
    setmetatable(overlay, overlay_mt)
    return overlay
end

function overlay_mt.update(ov)
    local cmd = {}
    for k, v in pairs(ov) do
        cmd[k] = v
    end
    cmd.name = "osd-overlay"
    cmd.res_x = math.floor(cmd.res_x)
    cmd.res_y = math.floor(cmd.res_y)
    return mp.command_native(cmd)
end

function overlay_mt.remove(ov)
    mp.command_native {
        name = "osd-overlay",
        id = ov.id,
        format = "none",
        data = "",
    }
end

-- legacy API
function mp.set_osd_ass(res_x, res_y, data)
    if not mp._legacy_overlay then
        mp._legacy_overlay = mp.create_osd_overlay("ass-events")
    end
    if mp._legacy_overlay.res_x ~= res_x or
       mp._legacy_overlay.res_y ~= res_y or
       mp._legacy_overlay.data ~= data
    then
        mp._legacy_overlay.res_x = res_x
        mp._legacy_overlay.res_y = res_y
        mp._legacy_overlay.data = data
        mp._legacy_overlay:update()
    end
end

function mp.get_osd_size()
    local prop = mp.get_property_native("osd-dimensions")
    return prop.w, prop.h, prop.aspect
end

function mp.get_osd_margins()
    local prop = mp.get_property_native("osd-dimensions")
    return prop.ml, prop.mt, prop.mr, prop.mb
end

local mp_utils = package.loaded["mp.utils"]

function mp_utils.format_table(t, set)
    if not set then
        set = { [t] = true }
    end
    local res = "{"
    -- pretty expensive but simple way to distinguish array and map parts of t
    local keys = {}
    local vals = {}
    local arr = 0
    for i = 1, #t do
        if t[i] == nil then
            break
        end
        keys[i] = i
        vals[i] = t[i]
        arr = i
    end
    for k, v in pairs(t) do
        if not (type(k) == "number" and k >= 1 and k <= arr and keys[k]) then
            keys[#keys + 1] = k
            vals[#keys] = v
        end
    end
    for i = 1, #keys do
        if #res > 1 then
            res = res .. ", "
        end
        if i > arr then
            res = res .. mp_utils.to_string(keys[i], set) .. " = "
        end
        res = res .. mp_utils.to_string(vals[i], set)
    end
    res = res .. "}"
    return res
end

function mp_utils.to_string(v, set)
    if type(v) == "string" then
        return "\"" .. v .. "\""
    elseif type(v) == "table" then
        if set then
            if set[v] then
                return "[cycle]"
            end
            set[v] = true
        end
        return mp_utils.format_table(v, set)
    else
        return tostring(v)
    end
end

function mp_utils.getcwd()
    return mp.get_property("working-directory")
end

function mp_utils.getpid()
    return mp.get_property_number("pid")
end

function mp_utils.format_bytes_humanized(b)
    local d = {"Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"}
    local i = 1
    while b >= 1024 do
        b = b / 1024
        i = i + 1
    end
    return string.format("%0.2f %s", b, d[i] and d[i] or "*1024^" .. (i-1))
end

function mp_utils.subprocess(t)
    local cmd = {}
    cmd.name = "subprocess"
    cmd.capture_stdout = true
    for k, v in pairs(t) do
        if k == "cancellable" then
            k = "playback_only"
        elseif k == "max_size" then
            k = "capture_size"
        end
        cmd[k] = v
    end
    local res, err = mp.command_native(cmd)
    if res == nil then
        -- an error usually happens only if parsing failed (or no args passed)
        res = {error_string = err, status = -1}
    end
    if res.error_string ~= "" then
        res.error = res.error_string
    end
    return res
end

function mp_utils.subprocess_detached(t)
    mp.commandv("run", unpack(t.args))
end

function mp_utils.shared_script_property_set(name, value)
    if value ~= nil then
        -- no such thing as change-list with mpv_node, so build a string value
        mp.commandv("change-list", "shared-script-properties", "append",
                    name .. "=" .. value)
    else
        mp.commandv("change-list", "shared-script-properties", "remove", name)
    end
end

function mp_utils.shared_script_property_get(name)
    local map = mp.get_property_native("shared-script-properties")
    return map and map[name]
end

-- cb(name, value) on change and on init
function mp_utils.shared_script_property_observe(name, cb)
    -- it's _very_ wasteful to observe the mpv core "super" property for every
    -- shared sub-property, but then again you shouldn't use this
    mp.observe_property("shared-script-properties", "native", function(_, val)
        cb(name, val and val[name])
    end)
end

return {}
