local opts = {
    pan_follows_cursor_margin = 50,
    pan_follows_cursor_move_if_full_view = false,

    status_line_enabled = false,
    status_line_position = "bottom_left",
    status_line_size = 36,
    status_line = "${filename} [${playlist-pos-1}/${playlist-count}]",

    minimap_enabled = true,
    minimap_center = "92,92",
    minimap_scale = 12,
    minimap_max_size = "16,16",
    minimap_image_opacity = "88",
    minimap_image_color = "BBBBBB",
    minimap_view_opacity = "BB",
    minimap_view_color = "222222",
    minimap_view_above_image = true,
    minimap_hide_when_full_image_in_view = true,

    ruler_show_distance=true,
    ruler_show_coordinates=true,
    ruler_coordinates_space="both",
    ruler_show_angles="degrees",
    ruler_line_width=2,
    ruler_dots_radius=3,
    ruler_font_size=36,
    ruler_line_color="33",
    ruler_confirm_bindings="MBTN_LEFT,ENTER",
    ruler_exit_bindings="ESC",
    ruler_set_first_point_on_begin=false,
    ruler_clear_on_second_point_set=false,

    command_on_first_image_loaded="",
    command_on_image_loaded="",
    command_on_non_image_loaded="",
}
(require 'mp.options').read_options(opts)
function split(input)
    local ret = {}
    for str in string.gmatch(input, "([^,]+)") do
        ret[#ret + 1] = str
    end
    return ret
end
function str_to_num(array)
    local ret = {}
    for _, v in ipairs(array) do
        ret[#ret + 1] = tonumber(v)
    end
    return ret
end
opts.minimap_center=str_to_num(split(opts.minimap_center))
opts.minimap_max_size=str_to_num(split(opts.minimap_max_size))
opts.ruler_confirm_bindings=split(opts.ruler_confirm_bindings)
opts.ruler_exit_bindings=split(opts.ruler_exit_bindings)

function clamp(value, low, high)
    if value <= low then
        return low
    elseif value >= high then
        return high
    else
        return value
    end
end

local msg = require 'mp.msg'
local assdraw = require 'mp.assdraw'

local ass = { -- shared ass state
    status_line = "",
    minimap = "",
    ruler = "",
}

local cleanup = nil -- function set up by drag-to-pan/pan-follows cursor and must be called to clean lingering state
local mouse_move_callbacks = {} -- functions that are called when mouse_move is triggered
function add_mouse_move_callback(key, func)
    if #mouse_move_callbacks == 0 then
        mp.add_forced_key_binding("mouse_move", "image-viewer-internal", function()
            for _, func in pairs(mouse_move_callbacks) do
                func()
            end
        end)
    end
    mouse_move_callbacks[key] = func
end
function remove_mouse_move_callback(key)
    mouse_move_callbacks[key] = nil
    for _,_ in pairs(mouse_move_callbacks) do
        return
    end
    mp.remove_key_binding("image-viewer-internal")
end

video_dimensions_stale = true
function get_video_dimensions()
    -- this function is very much ripped from video/out/aspect.c in mpv's source
    if not video_dimensions_stale then return _video_dimensions end
    local video_params = mp.get_property_native("video-out-params")
    if not video_params then
        _video_dimensions = nil
        return nil
    end
    if not _timestamp then _timestamp = 0 end
    _timestamp = _timestamp + 1
    _video_dimensions = {
        timestamp = _timestamp,
        top_left = {x = 0, y = 0},
        bottom_right = {x = 0, y = 0},
        size = {w = 0, h = 0},
        ratios = {w = 0, h = 0}, -- by how much the original video got scaled
    }
    local keep_aspect = mp.get_property_bool("keepaspect")
    local w = video_params["w"]
    local h = video_params["h"]
    local dw = video_params["dw"]
    local dh = video_params["dh"]
    if mp.get_property_number("video-rotate") % 180 == 90 then
        w, h = h,w
        dw, dh = dh, dw
    end
    local window_w, window_h = mp.get_osd_size()

    if keep_aspect then
        local unscaled = mp.get_property_native("video-unscaled")
        local panscan = mp.get_property_number("panscan")

        local fwidth = window_w
        local fheight = math.floor(window_w / dw * dh)
        if fheight > window_h or fheight < h then
            local tmpw = math.floor(window_h / dh * dw)
            if tmpw <= window_w then
                fheight = window_h
                fwidth = tmpw
            end
        end
        local vo_panscan_area = window_h - fheight
        local f_w = fwidth / fheight
        local f_h = 1
        if vo_panscan_area == 0 then
            vo_panscan_area = window_h - fwidth
            f_w = 1
            f_h = fheight / fwidth
        end
        if unscaled or unscaled == "downscale-big" then
            vo_panscan_area = 0
            if unscaled or (dw <= window_w and dh <= window_h) then
                fwidth = dw
                fheight = dh
            end
        end

        local scaled_width = fwidth + math.floor(vo_panscan_area * panscan * f_w)
        local scaled_height = fheight + math.floor(vo_panscan_area * panscan * f_h)

        local split_scaling = function (dst_size, scaled_src_size, zoom, align, pan)
            scaled_src_size = math.floor(scaled_src_size * 2 ^ zoom)
            align = (align + 1) / 2
            local dst_start = math.floor((dst_size - scaled_src_size) * align + pan * scaled_src_size)
            if dst_start < 0 then
                --account for C int cast truncating as opposed to flooring
                dst_start = dst_start + 1
            end
            local dst_end = dst_start + scaled_src_size;
            if dst_start >= dst_end then
                dst_start = 0
                dst_end = 1
            end
            return dst_start, dst_end
        end
        local zoom = mp.get_property_number("video-zoom")

        local align_x = mp.get_property_number("video-align-x")
        local pan_x = mp.get_property_number("video-pan-x")
        _video_dimensions.top_left.x, _video_dimensions.bottom_right.x = split_scaling(window_w, scaled_width, zoom, align_x, pan_x)

        local align_y = mp.get_property_number("video-align-y")
        local pan_y = mp.get_property_number("video-pan-y")
        _video_dimensions.top_left.y, _video_dimensions.bottom_right.y = split_scaling(window_h,  scaled_height, zoom, align_y, pan_y)
    else
        _video_dimensions.top_left.x = 0
        _video_dimensions.bottom_right.x = window_w
        _video_dimensions.top_left.y = 0
        _video_dimensions.bottom_right.y = window_h
    end
    _video_dimensions.size.w = _video_dimensions.bottom_right.x - _video_dimensions.top_left.x
    _video_dimensions.size.h = _video_dimensions.bottom_right.y - _video_dimensions.top_left.y
    _video_dimensions.ratios.w = _video_dimensions.size.w / w
    _video_dimensions.ratios.h = _video_dimensions.size.h / h
    video_dimensions_stale = false
    return _video_dimensions
end

for _, p in ipairs({
    "keepaspect",
    "video-out-params",
    "video-unscaled",
    "panscan",
    "video-zoom",
    "video-align-x",
    "video-pan-x",
    "video-align-y",
    "video-pan-y",
    "osd-width",
    "osd-height",
}) do
    mp.observe_property(p, "native", function() video_dimensions_stale = true end)
end

function drag_to_pan_handler(table)
    if cleanup then
        cleanup()
        cleanup = nil
    end
    if table["event"] == "down" then
        local video_dimensions = get_video_dimensions()
        if not video_dimensions then return end
        local mouse_pos_origin, video_pan_origin = {}, {}
        local moved = false
        mouse_pos_origin.x, mouse_pos_origin.y = mp.get_mouse_pos()
        video_pan_origin.x = mp.get_property("video-pan-x")
        video_pan_origin.y = mp.get_property("video-pan-y")
        local idle = function()
            if moved then
                local mX, mY = mp.get_mouse_pos()
                local pX = video_pan_origin.x + (mX - mouse_pos_origin.x) / video_dimensions.size.w
                local pY = video_pan_origin.y + (mY - mouse_pos_origin.y) / video_dimensions.size.h
                mp.command("no-osd set video-pan-x " .. clamp(pX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(pY, -3, 3))
                moved = false
            end
        end
        mp.register_idle(idle)
        add_mouse_move_callback("drag-to-pan", function() moved = true end)
        cleanup = function()
            remove_mouse_move_callback("drag-to-pan")
            mp.unregister_idle(idle)
        end
    end
end

function pan_follows_cursor_handler(table)
    if cleanup then
        cleanup()
        cleanup = nil
    end
    if table["event"] == "down" then
        local video_dimensions = get_video_dimensions()
        if not video_dimensions then return end
        local window_w, window_h = mp.get_osd_size()
        local moved = true
        local idle = function()
            if moved then
                local mX, mY = mp.get_mouse_pos()
                local x = math.min(1, math.max(- 2 * mX / window_w + 1, -1))
                local y = math.min(1, math.max(- 2 * mY / window_h + 1, -1))
                local command = ""
                local margin, move_full = opts.pan_follows_cursor_margin, opts.pan_follows_cursor_move_if_full_view
                if (not move_full and window_w < video_dimensions.size.w) then
                    command = command .. "no-osd set video-pan-x " .. clamp(x * (video_dimensions.size.w - window_w + 2 * margin) / (2 * video_dimensions.size.w), -3, 3) .. ";"
                elseif mp.get_property_number("video-pan-x") ~= 0 then
                    command = command .. "no-osd set video-pan-x " .. "0;"
                end
                if (not move_full and window_h < video_dimensions.size.h) then
                    command = command .. "no-osd set video-pan-y " .. clamp(y * (video_dimensions.size.h - window_h + 2 * margin) / (2 * video_dimensions.size.h), -3, 3) .. ";"
                elseif mp.get_property_number("video-pan-y") ~= 0 then
                    command = command .. "no-osd set video-pan-y " .. "0;"
                end
                if command ~= "" then
                    mp.command(command)
                end
                moved = false
            end
        end
        mp.register_idle(idle)
        add_mouse_move_callback("pan-follows-cursor", function() moved = true end)
        cleanup = function()
            remove_mouse_move_callback("pan-follows-cursor")
            mp.unregister_idle(idle)
        end
    end
end

function cursor_centric_zoom_handler(amt)
    local zoom_inc = tonumber(amt)
    if not zoom_inc or zoom_inc == 0 then return end
    local video_dimensions = get_video_dimensions()
    if not video_dimensions then return end
    local mouse_pos_origin, video_pan_origin = {}, {}
    mouse_pos_origin.x, mouse_pos_origin.y = mp.get_mouse_pos()
    video_pan_origin.x = mp.get_property("video-pan-x")
    video_pan_origin.y = mp.get_property("video-pan-y")
    local zoom_origin = mp.get_property("video-zoom")
    -- how far the cursor is form the middle of the video (in percentage)
    local rx = (video_dimensions.top_left.x + video_dimensions.size.w / 2 - mouse_pos_origin.x) / (video_dimensions.size.w / 2)
    local ry = (video_dimensions.top_left.y + video_dimensions.size.h / 2 - mouse_pos_origin.y) / (video_dimensions.size.h / 2)

    -- the size in pixels of the (in|de)crement
    local diffHeight = (2 ^ zoom_inc - 1) * video_dimensions.size.h
    local diffWidth  = (2 ^ zoom_inc - 1) * video_dimensions.size.w
    local newPanX = (video_pan_origin.x * video_dimensions.size.w + rx * diffWidth / 2) / (video_dimensions.size.w + diffWidth)
    local newPanY = (video_pan_origin.y * video_dimensions.size.h + ry * diffHeight / 2) / (video_dimensions.size.h + diffHeight)
    mp.command("no-osd set video-zoom " .. zoom_origin + zoom_inc .. "; no-osd set video-pan-x " .. clamp(newPanX, -3, 3) .. "; no-osd set video-pan-y " .. clamp(newPanY, -3, 3))
end

function align_border(x, y)
    local video_dimensions = get_video_dimensions()
    if not video_dimensions then return end
    local window_w, window_h = mp.get_osd_size()
    local x, y = tonumber(x), tonumber(y)
    local command = ""
    if x then
        command = command .. "no-osd set video-pan-x " .. clamp(x * (video_dimensions.size.w - window_w) / (2 * video_dimensions.size.w), -3, 3) .. ";"
    end
    if y then
        command = command .. "no-osd set video-pan-y " .. clamp(y * (video_dimensions.size.h - window_h) / (2 * video_dimensions.size.h), -3, 3) .. ";"
    end
    if command ~= "" then
        mp.command(command)
    end
end

function pan_image(axis, amount, zoom_invariant, image_constrained)
    amount = tonumber(amount)
    if not amount or amount == 0 or axis ~= "x" and axis ~= "y" then return end
    if zoom_invariant == "yes" then
        amount = amount / 2 ^ mp.get_property_number("video-zoom")
    end
    local prop = "video-pan-" .. axis
    local old_pan = mp.get_property_number(prop)
    if image_constrained == "yes" then
        local video_dimensions = get_video_dimensions()
        if not video_dimensions then return end
        local measure = axis == "x" and "w" or "h"
        local window = {}
        window.w, window.h = mp.get_osd_size()
        local pixels_moved = amount * video_dimensions.size[measure]
        -- should somehow refactor this
        if pixels_moved > 0 then
            if window[measure] > video_dimensions.size[measure] then
                if video_dimensions.bottom_right[axis] >= window[measure] then return end
                if video_dimensions.bottom_right[axis] + pixels_moved > window[measure] then
                    amount = (window[measure] - video_dimensions.bottom_right[axis]) / video_dimensions.size[measure]
                end
            else
                if video_dimensions.top_left[axis] >= 0 then return end
                if video_dimensions.top_left[axis] + pixels_moved > 0 then
                    amount = (0 - video_dimensions.top_left[axis]) / video_dimensions.size[measure]
                end
            end
        else
            if window[measure] > video_dimensions.size[measure] then
                if video_dimensions.top_left[axis] <= 0 then return end
                if video_dimensions.top_left[axis] + pixels_moved < 0 then
                    amount = (0 - video_dimensions.top_left[axis]) / video_dimensions.size[measure]
                end
            else
                if video_dimensions.bottom_right[axis] <= window[measure] then return end
                if video_dimensions.bottom_right[axis] + pixels_moved < window[measure] then
                    amount = (window[measure] - video_dimensions.bottom_right[axis]) / video_dimensions.size[measure]
                end
            end
        end
    end
    mp.set_property_number(prop, old_pan + amount)
end

function rotate_video(amt)
    local rot = mp.get_property_number("video-rotate")
    rot = (rot + amt) % 360
    mp.set_property_number("video-rotate", rot)
end

function reset_pan_if_visible()
    local video_dimensions = get_video_dimensions()
    if not video_dimensions then return end
    local window_w, window_h = mp.get_osd_size()
    local command = ""
    if (window_w >= video_dimensions.size.w) then
        command = command .. "no-osd set video-pan-x 0" .. ";"
    end
    if (window_h >= video_dimensions.size.h) then
        command = command .. "no-osd set video-pan-y 0" .. ";"
    end
    if command ~= "" then
        mp.command(command)
    end
end

function force_print_filename()
    mp.set_property("msg-level", "cplayer=info")
    mp.commandv("print-text", mp.get_property("path"))
    mp.set_property("msg-level", "all=no")
end

function draw_ass()
    local ww, wh = mp.get_osd_size()
    local merge = function(a, b)
        return b ~= "" and (a .. "\n" .. b) or a
    end
    mp.set_osd_ass(ww, wh, merge(merge(ass.status_line, ass.minimap), ass.ruler))
end

local status_line_enabled = false
local status_line_stale = true

function mark_status_line_stale()
    status_line_stale = true
end

function refresh_status_line()
    if not status_line_stale then return end
    status_line_stale = false
    local path = mp.get_property("path")
    if path == nil or path == "" then
        ass.status_line = ""
        draw_ass()
        return
    end
    local expanded = mp.command_native({ "expand-text", opts.status_line })
    if not expanded then
        msg.warn("Error expanding status line")
        ass.status_line = ""
        draw_ass()
        return
    end
    local w,h = mp.get_osd_size()
    local an, x, y
    local margin = 10
    if opts.status_line_position == "top_left" then
        x = margin
        y = margin
        an = 7
    elseif opts.status_line_position == "top_right" then
        x = w-margin
        y = margin
        an = 9
    elseif opts.status_line_position == "bottom_right" then
        x = w-margin
        y = h-margin
        an = 3
    else
        x = margin
        y = h-margin
        an = 1
    end
    local a = assdraw:ass_new()
    a:new_event()
    a:an(an)
    a:pos(x,y)
    a:append("{\\fs".. opts.status_line_size.. "}{\\bord1.0}")
    a:append(expanded)
    ass.status_line = a.text
    draw_ass()
end

function enable_status_line()
    if status_line_enabled then return end
    status_line_enabled = true
    local start = 0
    while true do
        local s, e, cap = string.find(opts.status_line, "%${[?!]?([%l%d-/]*)", start)
        if not s then break end
        mp.observe_property(cap, nil, mark_status_line_stale)
        start = e
    end
    mp.observe_property("path", nil, mark_status_line_stale)
    mp.observe_property("osd-width", nil, mark_status_line_stale)
    mp.observe_property("osd-height", nil, mark_status_line_stale)
    mp.register_idle(refresh_status_line)
    mark_status_line_stale()
end

function disable_status_line()
    if not status_line_enabled then return end
    status_line_enabled = false
    mp.unobserve_property(mark_status_line_stale)
    mp.unregister_idle(refresh_status_line)
    ass.status_line = ""
    draw_ass()
end

if opts.status_line_enabled then
    enable_status_line()
end

if opts.command_on_image_loaded ~= "" or opts.command_on_non_image_loaded ~= "" then
    local was_image = false
    local frame_count = nil
    local audio_tracks = nil
    local out_params_ready = nil
    local path = nil

    function state_changed()
        function set_image(is_image)
            if is_image and not was_image and opts.command_on_first_image_loaded ~= "" then
                mp.command(opts.command_on_first_image_loaded)
            end
            if is_image and opts.command_on_image_loaded ~= "" then
                mp.command(opts.command_on_image_loaded)
            end
            if not is_image and was_image and opts.command_on_non_image_loaded ~= "" then
                mp.command(opts.command_on_non_image_loaded)
            end
            was_image = is_image
        end
        -- only do things when state is consistent
        if path ~= nil and audio_tracks ~= nil then
            if frame_count == nil and audio_tracks > 0 then
                set_image(false)
            elseif out_params_ready and frame_count ~= nil then
                -- png have 0 frames, jpg 1 ¯\_(ツ)_/¯
                set_image((frame_count == 0 or frame_count == 1) and audio_tracks == 0)
            end
        end
    end

    mp.observe_property("video-out-params/par", "number", function(_, val)
        out_params_ready = (val ~= nil and val > 0)
        state_changed()
    end)
    mp.observe_property("estimated-frame-count", "number", function(_, val)
        frame_count = val
        state_changed()
    end)
    mp.observe_property("path", "string", function(_, val)
        if not val or val == "" then
            path = nil
        else
            path = val
        end
        state_changed()
    end)
    mp.register_event("tracks-changed", function()
        audio_tracks = 0
        local tracks = 0
        for _, track in ipairs(mp.get_property_native("track-list")) do
            tracks = tracks + 1
             if track.type == "audio" then
                 audio_tracks = audio_tracks + 1
             end
        end
        if tracks == 0 then
            audio_tracks = nil
        end
        state_changed()
    end)
end

function refresh_minimap()
    local dim = get_video_dimensions()
    if not dim then
        ass.minimap = ""
        draw_ass()
        return
    end
    if _minimap_old_timestamp and dim.timestamp == _minimap_old_timestamp then return end
    _minimap_old_timestamp = dim.timestamp
    local ww, wh = mp.get_osd_size()
    if opts.minimap_hide_when_full_image_in_view then
        if dim.top_left.x >= 0 and
           dim.top_left.y >= 0 and
           dim.bottom_right.x <= ww and
           dim.bottom_right.y <= wh
        then
            ass.minimap = ""
            draw_ass()
            return
        end
    end
    local center = {
        x=opts.minimap_center[1]/100*ww,
        y=opts.minimap_center[2]/100*wh
    }
    local cutoff = {
        x=opts.minimap_max_size[1]/100*ww/2,
        y=opts.minimap_max_size[2]/100*wh/2
    }
    local a = assdraw.ass_new()
    local draw = function(x, y, w, h, opacity, color)
        a:new_event()
        a:pos(center.x, center.y)
        a:append("{\\bord0}")
        a:append("{\\shad0}")
        a:append("{\\c&" .. color .. "&}")
        a:append("{\\2a&HFF}")
        a:append("{\\3a&HFF}")
        a:append("{\\4a&HFF}")
        a:append("{\\1a&H" .. opacity .. "}")
        w=w/2
        h=h/2
        a:draw_start()
        local rounded = {true,true,true,true} -- tl, tr, br, bl
        local x0,y0,x1,y1 = x-w, y-h, x+w, y+h
        if x0 < -cutoff.x then
            x0 = -cutoff.x
            rounded[4] = false
            rounded[1] = false
        end
        if y0 < -cutoff.y then
            y0 = -cutoff.y
            rounded[1] = false
            rounded[2] = false
        end
        if x1 > cutoff.x then
            x1 = cutoff.x
            rounded[2] = false
            rounded[3] = false
        end
        if y1 > cutoff.y then
            y1 = cutoff.y
            rounded[3] = false
            rounded[4] = false
        end

        local r = 3
        local c = 0.551915024494 * r
        if rounded[0] then
            a:move_to(x0 + r, y0)
        else
            a:move_to(x0,y0)
        end
        if rounded[1] then
            a:line_to(x1 - r, y0)
            a:bezier_curve(x1 - r + c, y0, x1, y0 + r - c, x1, y0 + r)
        else
            a:line_to(x1, y0)
        end
        if rounded[2] then
            a:line_to(x1, y1 - r)
            a:bezier_curve(x1, y1 - r + c, x1 - r + c, y1, x1 - r, y1)
        else
            a:line_to(x1, y1)
        end
        if rounded[3] then
            a:line_to(x0 + r, y1)
            a:bezier_curve(x0 + r - c, y1, x0, y1 - r + c, x0, y1 - r)
        else
            a:line_to(x0, y1)
        end
        if rounded[4] then
            a:line_to(x0, y0 + r)
            a:bezier_curve(x0, y0 + r - c, x0 + r - c, y0, x0 + r, y0)
        else
            a:line_to(x0, y0)
        end
        a:draw_stop()
    end
    local image = function()
        draw((dim.top_left.x + dim.size.w/2 - ww/2) / opts.minimap_scale,
             (dim.top_left.y + dim.size.h/2 - wh/2) / opts.minimap_scale,
             dim.size.w / opts.minimap_scale,
             dim.size.h / opts.minimap_scale,
             opts.minimap_image_opacity,
             opts.minimap_image_color)
    end
    local view = function()
        draw(0,
             0,
             ww / opts.minimap_scale,
             wh / opts.minimap_scale,
             opts.minimap_view_opacity,
             opts.minimap_view_color)
    end
    if opts.minimap_view_above_image then
        image()
        view()
    else
        view()
        image()
    end
    ass.minimap = a.text
    draw_ass()
end

local minimap_enabled = false

function enable_minimap()
    if minimap_enabled then return end
    minimap_enabled = true
    mp.register_idle(refresh_minimap)
end

function disable_minimap()
    if not minimap_enabled then return end
    minimap_enabled = false
    ass.minimap = a.text
    draw_ass()
    mp.unregister_idle(refresh_minimap)
end

if opts.minimap_enabled then
    enable_minimap()
end

local ruler_state = 0 -- {0,1,2,3} = {inactive,setting first point,setting second point,done}
local ruler_first_point = nil -- in video space coordinates
local ruler_second_point = nil -- in video space coordinates

function cursor_video_space()
    local dim = get_video_dimensions()
    if not dim then return nil end
    local mx, my = mp.get_mouse_pos()
    local ret = {}
    ret.x = (mx - dim.top_left.x) / dim.ratios.w
    ret.y = (my - dim.top_left.y) / dim.ratios.h
    return ret
end
function video_space_to_screen(point)
    local dim = get_video_dimensions()
    if not dim then return nil end
    local ret = {}
    ret.x = point.x * dim.ratios.w + dim.top_left.x
    ret.y = point.y * dim.ratios.h + dim.top_left.y
    return ret
end

function refresh_ruler()
    local dim = get_video_dimensions()
    if not dim then
        ass.ruler = ""
        draw_ass()
        return
    end

    local line_start = {}
    local line_end = {}
    if ruler_second_point then
        line_start.image = ruler_first_point
        line_start.screen = video_space_to_screen(ruler_first_point)
        line_end.image = ruler_second_point
        line_end.screen = video_space_to_screen(ruler_second_point)
    elseif ruler_first_point then
        line_start.image = ruler_first_point
        line_start.screen = video_space_to_screen(ruler_first_point)
        line_end.image = cursor_video_space()
        line_end.screen = {}
        line_end.screen.x, line_end.screen.y = mp.get_mouse_pos()
    else
        local mx, my = mp.get_mouse_pos()
        line_start.image = cursor_video_space()
        line_start.screen = {}
        line_start.screen.x, line_start.screen.y = mp.get_mouse_pos()
        line_end = line_start
    end
    local distinct = (math.abs(line_start.screen.x - line_end.screen.x) >= 1
                   or math.abs(line_start.screen.y - line_end.screen.y) >= 1)

    local a = assdraw:ass_new()
    local draw_setup = function(bord)
        a:new_event()
        a:pos(0,0)
        a:append("{\\bord" .. bord .. "}")
        a:append("{\\shad0}")
        local r = opts.ruler_line_color
        a:append("{\\3c&H".. r .. r .. r .. "&}")
        a:append("{\\1a&HFF}")
        a:append("{\\2a&HFF}")
        a:append("{\\3a&H00}")
        a:append("{\\4a&HFF}")
        a:draw_start()
    end
    local dot = function(pos, size)
        draw_setup(size)
        a:move_to(pos.x, pos.y-0.5)
        a:line_to(pos.x, pos.y+0.5)
    end
    local line = function(from, to, size)
        draw_setup(size)
        a:move_to(from.x, from.y)
        a:line_to(to.x, to.y)
    end
    if distinct then
        dot(line_start.screen, opts.ruler_dots_radius)
        line(line_start.screen, line_end.screen, opts.ruler_line_width)
        dot(line_end.screen, opts.ruler_dots_radius)
    else
        dot(line_start.screen, opts.ruler_dots_radius)
    end

    local line_info = function()
        if not opts.ruler_show_distance then return end
        a:new_event()
        a:append("{\\fs36}{\\bord1}")
        a:pos((line_start.screen.x + line_end.screen.x) / 2, (line_start.screen.y + line_end.screen.y) / 2)
        local an = 1
        if line_start.image.x < line_end.image.x then an = an + 2 end
        if line_start.image.y < line_end.image.y then an = an + 6 end
        a:an(an)
        local image = math.sqrt(math.pow(line_start.image.x - line_end.image.x, 2) + math.pow(line_start.image.y - line_end.image.y, 2))
        local screen = math.sqrt(math.pow(line_start.screen.x - line_end.screen.x, 2) + math.pow(line_start.screen.y - line_end.screen.y, 2))
        if opts.ruler_coordinates_space == "both" then
            a:append(string.format("image: %.1f\\Nscreen: %.1f", image, screen))
        elseif opts.ruler_coordinates_space == "image" then
            a:append(string.format("%.1f", image))
        elseif opts.ruler_coordinates_space == "window" then
            a:append(string.format("%.1f", screen))
        end
    end
    local dot_info = function(pos, opposite)
        if not opts.ruler_show_coordinates then return end
        a:new_event()
        a:append("{\\fs" .. opts.ruler_font_size .."}{\\bord1}")
        a:pos(pos.screen.x, pos.screen.y)
        local an
        if distinct then
            an = 1
            if line_start.image.x > line_end.image.x then an = an + 2 end
            if line_start.image.y < line_end.image.y then an = an + 6 end
        else
            an = 7
        end
        if opposite then
            an = 9 + 1 - an
        end
        a:an(an)
        if opts.ruler_coordinates_space == "both" then
            a:append(string.format("image: %.1f, %.1f\\Nscreen: %i, %i",
                pos.image.x, pos.image.y, pos.screen.x, pos.screen.y))
        elseif opts.ruler_coordinates_space == "image" then
            a:append(string.format("%.1f, %.1f", pos.image.x, pos.image.y))
        elseif opts.ruler_coordinates_space == "window" then
            a:append(string.format("%i, %i", pos.screen.x, pos.screen.y))
        end
    end
    dot_info(line_start, true)
    if distinct then
        line_info()
        dot_info(line_end, false)
    end
    if distinct and opts.ruler_show_angles ~= "no" then
        local dist = 50
        local pos_from_angle = function(mult, angle)
            return {
                x = line_start.screen.x + mult * dist * math.cos(angle),
                y = line_start.screen.y + mult * dist * math.sin(angle)
            }
        end
        local extended = {x=line_start.screen.x, y=line_start.screen.y}
        if line_end.screen.x > line_start.screen.x then
            extended.x = extended.x + dist
        else
            extended.x = extended.x - dist
        end
        line(line_start.screen, extended, math.max(0, opts.ruler_line_width-0.5))
        local angle = math.atan(math.abs(line_start.image.y - line_end.image.y) / math.abs(line_start.image.x - line_end.image.x))
        local fix_angle
        local an
        if line_end.image.y < line_start.image.y and line_end.image.x > line_start.image.x then
            -- upper-right
            an = 4
            fix_angle = function(angle) return - angle end
        elseif line_end.image.y < line_start.image.y then
            -- upper-left
            an = 6
            fix_angle = function(angle) return math.pi + angle end
        elseif line_end.image.x < line_start.image.x then
            -- bottom-left
            an = 6
            fix_angle = function(angle) return math.pi - angle end
        else
            -- bottom-right
            an = 4
            fix_angle = function(angle) return angle end
        end
        -- should implement this https://math.stackexchange.com/questions/873224/calculate-control-points-of-cubic-bezier-curve-approximating-a-part-of-a-circle
        local cp1 = pos_from_angle(1, fix_angle(angle*1/4))
        local cp2 = pos_from_angle(1, fix_angle(angle*3/4))
        local p2 = pos_from_angle(1, fix_angle(angle))
        a:bezier_curve(cp1.x, cp1.y, cp2.x, cp2.y, p2.x, p2.y)

        a:new_event()
        a:append("{\\fs" .. opts.ruler_font_size .."}{\\bord1}")
        local text_pos = pos_from_angle(1.1, fix_angle(angle*2/3)) -- you'd think /2 would make more sense, but *2/3  looks better
        a:pos(text_pos.x, text_pos.y)
        a:an(an)
        if opts.ruler_show_angles == "both" then
            a:append(string.format("%.2f\\N%.1f°", angle, angle / math.pi * 180))
        elseif opts.ruler_show_angles == "degrees" then
            a:append(string.format("%.1f°", angle / math.pi * 180))
        elseif opts.ruler_show_angles == "radians" then
            a:append(string.format("%.2f", angle))
        end
    end

    ass.ruler = a.text
    draw_ass()
end

function ruler_next()
    if ruler_state == 0 then
        mp.register_idle(refresh_ruler)
        add_mouse_move_callback("ruler", function() end) -- only used to get an idle event on mouse move
        for _,key in ipairs(opts.ruler_confirm_bindings) do
            mp.add_forced_key_binding(key, "ruler-next-" .. key, ruler_next)
        end
        for _,key in ipairs(opts.ruler_exit_bindings) do
            mp.add_forced_key_binding(key, "ruler-stop-" .. key, ruler_stop)
        end
        ruler_state = 1
        if opts.ruler_set_first_point_on_begin then
            ruler_next()
        end
    elseif ruler_state == 1 then
        ruler_first_point = cursor_video_space()
        ruler_state = 2
    elseif ruler_state == 2 then
        ruler_state = 3
        ruler_second_point = cursor_video_space()
        if opts.ruler_clear_on_second_point_set then
            ruler_next()
        end
    else
        ruler_stop()
    end
end

function ruler_stop()
    if ruler_state == 0 then return end
    mp.unregister_idle(refresh_ruler)
    for _,key in ipairs(opts.ruler_confirm_bindings) do
        mp.remove_key_binding("ruler-next-" .. key)
    end
    for _,key in ipairs(opts.ruler_exit_bindings) do
        mp.remove_key_binding("ruler-stop-" .. key)
    end
    remove_mouse_move_callback("ruler")
    ruler_state = 0
    ruler_first_point = nil
    ruler_second_point = nil
    ass.ruler = ""
    draw_ass()
end

mp.add_key_binding(nil, "drag-to-pan", drag_to_pan_handler, {complex = true})
mp.add_key_binding(nil, "pan-follows-cursor", pan_follows_cursor_handler, {complex = true})
mp.add_key_binding(nil, "cursor-centric-zoom", cursor_centric_zoom_handler)
mp.add_key_binding(nil, "align-border", align_border)
mp.add_key_binding(nil, "pan-image", pan_image)
mp.add_key_binding(nil, "rotate-video", rotate_video)
mp.add_key_binding(nil, "reset-pan-if-visible", reset_pan_if_visible)
mp.add_key_binding(nil, "force-print-filename", force_print_filename)

mp.add_key_binding(nil, "ruler", ruler_next)

mp.add_key_binding(nil, "enable-status-line", enable_status_line)
mp.add_key_binding(nil, "disable-status-line", disable_status_line)
mp.add_key_binding(nil, "toggle-status-line", function() if status_line_enabled then disable_status_line() else enable_status_line() end end)

mp.add_key_binding(nil, "enable-minimap", enable_minimap)
mp.add_key_binding(nil, "disable-minimap", disable_minimap)
mp.add_key_binding(nil, "toggle-minimap", function() if minimap_enabled then disable_minimap() else enable_minimap() end end)
