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

local script_name = "easycrop"

-- Number of crop points currently chosen (0 to 2)
local points = {}
-- True if in cropping selection mode
local cropping = false
-- Original value of osc property
local osc_prop = false

-- Helper that converts two points to top-left and bottom-right
local swizzle_points = function (p1, p2)
    if p1.x > p2.x then p1.x, p2.x = p2.x, p1.x end
    if p1.y > p2.y then p1.y, p2.y = p2.y, p1.y end
end

local clamp = function (val, min, max)
    assert(min <= max)
    if val < min then return min end
    if val > max then return max end
    return val
end

local video_space_from_screen_space = function (ssp)
    -- Video native dimensions and screen size
    local vid_w = mp.get_property("width")
    local vid_h = mp.get_property("height")
    local osd_w = mp.get_property("osd-width")
    local osd_h = mp.get_property("osd-height")

    -- Factor by which the video is scaled to fit the screen
    local scale = math.min(osd_w/vid_w, osd_h/vid_h)

    -- Size video takes up in screen
    local vid_sw, vid_sh = scale*vid_w, scale*vid_h

    -- Video offset within screen
    local off_x = math.floor((osd_w - vid_sw)/2)
    local off_y = math.floor((osd_h - vid_sh)/2)

    local vsp = {}

    -- Move the point to within the video
    vsp.x = clamp(ssp.x, off_x, off_x + vid_sw)
    vsp.y = clamp(ssp.y, off_y, off_y + vid_sh)

    -- Convert screen-space to video-space
    vsp.x = math.floor((vsp.x - off_x) / scale)
    vsp.y = math.floor((vsp.y - off_y) / scale)

    return vsp
end

local screen_space_from_video_space = function (vsp)
    -- Video native dimensions and screen size
    local vid_w = mp.get_property("width")
    local vid_h = mp.get_property("height")
    local osd_w = mp.get_property("osd-width")
    local osd_h = mp.get_property("osd-height")

    -- Factor by which the video is scaled to fit the screen
    local scale = math.min(osd_w/vid_w, osd_h/vid_h)

    -- Size video takes up in screen
    local vid_sw, vid_sh = scale*vid_w, scale*vid_h

    -- Video offset within screen
    local off_x = math.floor((osd_w - vid_sw)/2)
    local off_y = math.floor((osd_h - vid_sh)/2)

    local ssp = {}
    ssp.x = vsp.x * scale + off_x
    ssp.y = vsp.y * scale + off_y
    return ssp
end

-- Wrapper that converts RRGGBB / RRGGBBAA to ASS format
local ass_set_color = function (idx, color)
    assert(color:len() == 8 or color:len() == 6)
    local ass = ""

    -- Set alpha value (if present)
    if color:len() == 8 then
        local alpha = 0xff - tonumber(color:sub(7, 8), 16)
        ass = ass .. string.format("\\%da&H%X&", idx, alpha)
    end

    -- Swizzle RGB to BGR and build ASS string
    color = color:sub(5, 6) .. color:sub(3, 4) .. color:sub(1, 2)
    return "{" .. ass .. string.format("\\%dc&H%s&", idx, color) .. "}"
end

local draw_rect = function (p1, p2)
    local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height")

    ass = assdraw.ass_new()

    -- Draw overlay over surrounding unselected region

    ass:draw_start()
    ass:pos(0, 0)

    ass:append(ass_set_color(1, "000000aa"))
    ass:append(ass_set_color(3, "00000000"))

    local l = math.min(p1.x, p2.x)
    local r = math.max(p1.x, p2.x)
    local u = math.min(p1.y, p2.y)
    local d = math.max(p1.y, p2.y)

    ass:rect_cw(0, 0, l, osd_h)
    ass:rect_cw(r, 0, osd_w, osd_h)
    ass:rect_cw(l, 0, r, u)
    ass:rect_cw(l, d, r, osd_h)

    ass:draw_stop()

    -- Draw border around selected region

    ass:new_event()
    ass:draw_start()
    ass:pos(0, 0)

    ass:append(ass_set_color(1, "00000000"))
    ass:append(ass_set_color(3, "000000ff"))
    ass:append("{\\bord2}")

    ass:rect_cw(p1.x, p1.y, p2.x, p2.y)

    ass:draw_stop()

    mp.set_osd_ass(osd_w, osd_h, ass.text)
end

local draw_fill = function ()
    local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height")

    ass = assdraw.ass_new()
    ass:draw_start()
    ass:pos(0, 0)

    ass:append(ass_set_color(1, "000000aa"))
    ass:append(ass_set_color(3, "00000000"))
    ass:rect_cw(0, 0, osd_w, osd_h)

    ass:draw_stop()
    mp.set_osd_ass(osd_w, osd_h, ass.text)
end

local draw_clear = function ()
    local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height")
    mp.set_osd_ass(osd_w, osd_h, "")
end

local draw_cropper = function ()
    if #points == 1 then
        local p1 = screen_space_from_video_space(points[1])
        local p2 = {}
        p2.x, p2.y = mp.get_mouse_pos()
        draw_rect(p1, p2)
    end
end

local uncrop = function ()
    mp.command("no-osd vf del @" .. script_name .. ":crop")
end

local crop = function(p1, p2)
    swizzle_points(p1, p2)

    local w = p2.x - p1.x
    local h = p2.y - p1.y
    local ok, err = mp.command(string.format(
        "no-osd vf add @%s:crop=%s:%s:%s:%s", script_name, w, h, p1.x, p1.y))

    if not ok then
        mp.osd_message("Cropping failed")
        points = {}
    end
end

local easycrop_stop = function ()
    mp.set_property("osc", osc_prop)
    cropping = false
    mp.remove_key_binding("easycrop_mouse_btn0")
    draw_clear()
end

local mouse_btn0_cb = function ()
    if not cropping then
        return
    end

    local mx, my = mp.get_mouse_pos()
    table.insert(points, video_space_from_screen_space({ x = mx, y = my }))

    if #points == 2 then
        crop(points[1], points[2])
        easycrop_stop()
    end
end

local easycrop_start = function ()
    -- Cropping requires swdec or hwdec with copy-back
    local hwdec = mp.get_property("hwdec-current")
    if hwdec == nil then
        return mp.msg.error("Cannot determine current hardware decoder mode")
    end
    -- Check whitelist of ok values
    local valid_hwdec = {
       ["no"] = true, -- software decoding
       -- Taken from mpv manual
       ["videotoolbox-co"] = true,
       ["vaapi-copy"] = true,
       ["dxva2-copy"] = true,
       ["d3d11va-copy"] = true,
       ["mediacodec"] = true
    }
    if not valid_hwdec[hwdec] then
        return mp.osd_message("Cropping requires swdec or hwdec with copy-back (see mpv manual)")
    end

    -- Just clear the current crop and return, if there is one
    if #points ~= 0 then
        uncrop()
        points = {}
        return
    end

    -- Hide OSC
    osc_prop = mp.get_property("osc")
    mp.set_property("osc", "no")

    cropping = true
    mp.add_forced_key_binding("mouse_btn0", "easycrop_mouse_btn0", mouse_btn0_cb)
    draw_fill()
end

local easycrop_activate = function ()
    if cropping then
        easycrop_stop()
    else
        easycrop_start()
    end
end

mp.add_key_binding("mouse_move", draw_cropper)
mp.observe_property("osd-width", "native", draw_cropper)
mp.observe_property("osd-height", "native", draw_cropper)

mp.add_key_binding("c", "easy_crop", easycrop_activate)
