--[[
This file is part of mpv.

mpv is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

mpv is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
]]

local utils = require "mp.utils"
local input = require "mp.input"

local options = {
    history_date_format = "%Y-%m-%d %H:%M:%S",
    hide_history_duplicates = true,
}

require "mp.options".read_options(options, nil, function () end)

local function show_warning(message)
    mp.msg.warn(message)
    if mp.get_property_native("vo-configured") then
        mp.osd_message(message)
    end
end

local function show_error(message)
    mp.msg.error(message)
    if mp.get_property_native("vo-configured") then
        mp.osd_message(message)
    end
end

mp.add_key_binding(nil, "select-playlist", function ()
    local playlist = {}
    local default_item
    local show = mp.get_property_native("osd-playlist-entry")
    local trailing_slash_pattern = mp.get_property("platform") == "windows"
                                   and "[/\\]+$" or "/+$"

    for i, entry in ipairs(mp.get_property_native("playlist")) do
        playlist[i] = entry.title
        if not playlist[i] or show ~= "title" then
            playlist[i] = entry.filename
            if not playlist[i]:find("://") then
                playlist[i] = select(2, utils.split_path(
                    playlist[i]:gsub(trailing_slash_pattern, "")))
            end
        end
        if entry.title and show == "both" then
            playlist[i] = string.format("%s (%s)", entry.title, playlist[i])
        end

        if entry.playing then
            default_item = i
        end
    end

    if #playlist == 0 then
        show_warning("The playlist is empty.")
        return
    end

    input.select({
        prompt = "Select a playlist entry:",
        items = playlist,
        default_item = default_item,
        submit = function (index)
            mp.commandv("playlist-play-index", index - 1)
        end,
    })
end)

local function format_flags(track)
    local flags = ""

    for _, flag in ipairs({
        "default", "forced", "dependent", "visual-impaired", "hearing-impaired",
        "image", "external"
    }) do
        if track[flag] then
            flags = flags .. flag .. " "
        end
    end

    if flags == "" then
        return ""
    end

    return " [" .. flags:sub(1, -2) .. "]"
end

local function format_track(track)
    local bitrate = track["demux-bitrate"] or track["hls-bitrate"]

    return (track.selected and "●" or "○") ..
        (track.title and " " .. track.title or "") ..
        " (" .. (
            (track.lang and track.lang .. " " or "") ..
            (track.codec and track.codec .. " " or "") ..
            (track["demux-w"] and track["demux-w"] .. "x" .. track["demux-h"]
             .. " " or "") ..
            (track["demux-fps"] and not track.image
             and string.format("%.4f", track["demux-fps"]):gsub("%.?0*$", "") ..
             " fps " or "") ..
            (track["demux-channel-count"] and track["demux-channel-count"] ..
             "ch " or "") ..
            (track["codec-profile"] and track.type == "audio"
             and track["codec-profile"] .. " " or "") ..
            (track["demux-samplerate"] and track["demux-samplerate"] / 1000 ..
             " kHz " or "") ..
            (bitrate and string.format("%.0f", bitrate / 1000) ..
             " kbps " or "")
        ):sub(1, -2) .. ")" .. format_flags(track)
end

mp.add_key_binding(nil, "select-track", function ()
    local tracks = {}

    for i, track in ipairs(mp.get_property_native("track-list")) do
        tracks[i] = (track.image and "Image" or
                     track.type:sub(1, 1):upper() .. track.type:sub(2)) .. ": " ..
                    format_track(track)
    end

    if #tracks == 0 then
        show_warning("No available tracks.")
        return
    end

    input.select({
        prompt = "Select a track:",
        items = tracks,
        submit = function (id)
            local track = mp.get_property_native("track-list/" .. id - 1)
            if track then
                mp.set_property(track.type, track.selected and "no" or track.id)
            end
        end,
    })
end)

local function select_track(property, type, prompt, warning)
    local tracks = {}
    local items = {}
    local default_item
    local track_id = mp.get_property_native(property)

    for _, track in ipairs(mp.get_property_native("track-list")) do
        if track.type == type then
            tracks[#tracks + 1] = track
            items[#items + 1] = format_track(track)

            if track.id == track_id then
                default_item = #items
            end
        end
    end

    if #items == 0 then
        show_warning(warning)
        return
    end

    input.select({
        prompt = prompt,
        items = items,
        default_item = default_item,
        submit = function (id)
            mp.set_property(property, tracks[id].selected and "no" or tracks[id].id)
        end,
    })
end

mp.add_key_binding(nil, "select-sid", function ()
    select_track("sid", "sub", "Select a subtitle:", "No available subtitles.")
end)

mp.add_key_binding(nil, "select-secondary-sid", function ()
    select_track("secondary-sid", "sub", "Select a secondary subtitle:",
                 "No available subtitles.")
end)

mp.add_key_binding(nil, "select-aid", function ()
    select_track("aid", "audio", "Select an audio track:",
                 "No available audio tracks.")
end)

mp.add_key_binding(nil, "select-vid", function ()
    select_track("vid", "video", "Select a video track:",
                 "No available video tracks.")
end)

local function format_time(t, duration)
    local fmt = math.max(t, duration) >= 60 * 60 and "%H:%M:%S" or "%M:%S"
    return mp.format_time(t, fmt)
end

mp.add_key_binding(nil, "select-chapter", function ()
    local chapters = {}
    local default_item = mp.get_property_native("chapter")

    if default_item == nil then
        show_warning("No available chapters.")
        return
    end

    local duration = mp.get_property_native("duration", math.huge)

    for i, chapter in ipairs(mp.get_property_native("chapter-list")) do
        chapters[i] = format_time(chapter.time, duration) .. " " .. chapter.title
    end

    input.select({
        prompt = "Select a chapter:",
        items = chapters,
        default_item = default_item > -1 and default_item + 1,
        submit = function (chapter)
            mp.set_property("chapter", chapter - 1)
        end,
    })
end)

mp.add_key_binding(nil, "select-edition", function ()
    local edition_list = mp.get_property_native("edition-list")

    if edition_list == nil or #edition_list < 2 then
        show_warning("No available editions.")
        return
    end

    local editions = {}
    local default_item = mp.get_property_native("current-edition")

    for i, edition in ipairs(edition_list) do
        editions[i] = edition.title or ("Edition " .. edition.id + 1)
    end

    input.select({
        prompt = "Select an edition:",
        items = editions,
        default_item = default_item > -1 and default_item + 1,
        submit = function (edition)
            mp.set_property("edition", edition - 1)
        end,
    })
end)

mp.add_key_binding(nil, "select-subtitle-line", function ()
    local sub = mp.get_property_native("current-tracks/sub")

    if sub == nil then
        show_warning("No subtitle is loaded.")
        return
    end

    if sub.external and sub["external-filename"]:find("^edl://") then
        sub["external-filename"] = sub["external-filename"]:match('https?://.*')
                                   or sub["external-filename"]
    end

    local r = mp.command_native({
        name = "subprocess",
        capture_stdout = true,
        args = sub.external
            and {"ffmpeg", "-loglevel", "error", "-i", sub["external-filename"],
                 "-f", "lrc", "-map_metadata", "-1", "-fflags", "+bitexact", "-"}
            or {"ffmpeg", "-loglevel", "error", "-i", mp.get_property("path"),
                "-map", "s:" .. sub["id"] - 1, "-f", "lrc", "-map_metadata",
                "-1", "-fflags", "+bitexact", "-"}
    })

    if r.error_string == "init" then
        show_error("Failed to extract subtitles: ffmpeg not found.")
        return
    elseif r.status ~= 0 then
        show_error("Failed to extract subtitles.")
        return
    end

    local sub_lines = {}
    local sub_times = {}
    local default_item
    local delay = mp.get_property_native("sub-delay")
    local time_pos = mp.get_property_native("time-pos") - delay
    local duration = mp.get_property_native("duration", math.huge)
    local sub_content = {}

    -- Strip HTML and ASS tags and process subtitles
    for line in r.stdout:gmatch("[^\n]+") do
        -- Clean up tags
        local sub_line = line:gsub("<.->", "")                -- Strip HTML tags
                             :gsub("\\h+", " ")               -- Replace '\h' tag
                             :gsub("{[\\=].-}", "")           -- Remove ASS formatting
                             :gsub(".-]", "", 1)              -- Remove time info prefix
                             :gsub("^%s*(.-)%s*$", "%1")      -- Strip whitespace
                             :gsub("^m%s[mbl%s%-%d%.]+$", "") -- Remove graphics code

        if sub.codec == "subrip" or (sub_line ~= "" and sub_line:match("^%s+$") == nil) then
            local sub_time = line:match("%d+") * 60 + line:match(":([%d%.]*)")
            local time_seconds = math.floor(sub_time)
            sub_content[time_seconds] = sub_content[time_seconds] or {}
            sub_content[time_seconds][sub_line] = true
        end
    end

    -- Process all timestamps and content into selectable subtitle list
    for time_seconds, contents in pairs(sub_content) do
        for sub_line in pairs(contents) do
            sub_times[#sub_times + 1] = time_seconds
            sub_lines[#sub_lines + 1] = format_time(time_seconds, duration) .. " " .. sub_line
        end
    end

    -- Generate time -> subtitle mapping
    local time_to_lines = {}
    for i = 1, #sub_times do
        local time = sub_times[i]
        local line = sub_lines[i]

        if not time_to_lines[time] then
            time_to_lines[time] = {}
        end
        table.insert(time_to_lines[time], line)
    end

    -- Sort by timestamp
    local sorted_sub_times = {}
    for i = 1, #sub_times do
        sorted_sub_times[i] = sub_times[i]
    end
    table.sort(sorted_sub_times)

    -- Use a helper table to avoid duplicates
    local added_times = {}

    -- Rebuild sub_lines and sub_times based on the sorted timestamps
    local sorted_sub_lines = {}
    for _, sub_time in ipairs(sorted_sub_times) do
        -- Iterate over all subtitle content for this timestamp
        if not added_times[sub_time] then
            added_times[sub_time] = true
            for _, line in ipairs(time_to_lines[sub_time]) do
                table.insert(sorted_sub_lines, line)
            end
        end
    end

    -- Use the sorted subtitle list
    sub_lines = sorted_sub_lines
    sub_times = sorted_sub_times

    -- Get the default item (last subtitle before current time position)
    for i, sub_time in ipairs(sub_times) do
        if sub_time <= time_pos then
            default_item = i
        end
    end

    input.select({
        prompt = "Select a line to seek to:",
        items = sub_lines,
        default_item = default_item,
        submit = function (index)
            -- Add an offset to seek to the correct line while paused without a
            -- video track.
            if mp.get_property_native("current-tracks/video/image") ~= false then
                delay = delay + 0.1
            end

            mp.commandv("seek", sub_times[index] + delay, "absolute")
        end,
    })
end)

mp.add_key_binding(nil, "select-audio-device", function ()
    local devices = mp.get_property_native("audio-device-list")
    local items = {}
    -- This is only useful if an --audio-device has been explicitly set,
    -- otherwise its value is just auto and there is no current-audio-device
    -- property.
    local selected_device = mp.get_property("audio-device")
    local default_item

    if #devices == 0 then
        show_warning("No available audio devices.")
        return
    end

    for i, device in ipairs(devices) do
        items[i] = device.name .. " (" .. device.description .. ")"

        if device.name == selected_device then
            default_item = i
        end
    end

    input.select({
        prompt = "Select an audio device:",
        items = items,
        default_item = default_item,
        submit = function (id)
            mp.set_property("audio-device", devices[id].name)
        end,
    })
end)

local function format_history_entry(entry)
    local status
    status, entry.time = pcall(os.date, options.history_date_format, entry.time)

    if not status then
        mp.msg.warn(entry.time)
    end

    local item = "(" .. entry.time .. ") "

    if entry.title then
        return item .. entry.title .. " (" .. entry.path .. ")"
    end

    if entry.path:find("://") then
        return item .. entry.path
    end

    local directory, filename = utils.split_path(entry.path)

    return item .. filename .. " (" .. directory .. ")"
end

mp.add_key_binding(nil, "select-watch-history", function ()
    local history_file_path = mp.command_native(
        {"expand-path", mp.get_property("watch-history-path")})
    local history_file, error_message = io.open(history_file_path)
    if not history_file then
        show_warning(mp.get_property_native("save-watch-history")
                     and error_message
                     or "Enable --save-watch-history to jump to recently played files.")
        return
    end

    local all_entries = {}
    local line_num = 1
    for line in history_file:lines() do
        local entry = utils.parse_json(line)
        if entry and entry.path then
            all_entries[#all_entries + 1] = entry
        else
            mp.msg.warn(history_file_path .. ": Parse error at line " .. line_num)
        end
        line_num = line_num + 1
    end
    history_file:close()

    local entries = {}
    local items = {}
    local seen = {}

    for i = #all_entries, 1, -1 do
        local entry = all_entries[i]
        if not seen[entry.path] or not options.hide_history_duplicates then
            seen[entry.path] = true
            entries[#entries + 1] = entry
            items[#items + 1] = format_history_entry(entry)
        end
    end

    items[#items+1] = "Clear history"

    input.select({
        prompt = "Select a file:",
        items = items,
        submit = function (i)
            if entries[i] then
                mp.commandv("loadfile", entries[i].path)
                return
            end

            error_message = select(2, os.remove(history_file_path))
            if error_message then
                show_error(error_message)
            else
                mp.osd_message("History cleared.")
            end
        end,
    })
end)

mp.add_key_binding(nil, "select-watch-later", function ()
    local watch_later_dir = mp.get_property("current-watch-later-dir")

    if not watch_later_dir then
        show_warning("No watch later files found.")
        return
    end

    local watch_later_files = {}

    for i, file in ipairs(utils.readdir(watch_later_dir, "files") or {}) do
        watch_later_files[i] = watch_later_dir .. "/" .. file
    end

    if #watch_later_files == 0 then
        show_warning("No watch later files found.")
        return
    end

    local files = {}
    for _, watch_later_file in pairs(watch_later_files) do
        local file_handle = io.open(watch_later_file)
        if file_handle then
            local line = file_handle:read()
            if line and line ~= "# redirect entry" and line:find("^#") then
                files[#files + 1] = {line:sub(3), utils.file_info(watch_later_file).mtime}
            end
            file_handle:close()
        end
    end

    if #files == 0 then
        show_warning(mp.get_property_native("write-filename-in-watch-later-config")
            and "No watch later files found."
            or "Enable --write-filename-in-watch-later-config to select recent files.")
        return
    end

    table.sort(files, function (i, j)
        return i[2] > j[2]
    end)

    local items = {}
    for i, file in ipairs(files) do
        items[i] = os.date("(%Y-%m-%d) ", file[2]) .. file[1]
    end

    input.select({
        prompt = "Select a file:",
        items = items,
        submit = function (i)
            mp.commandv("loadfile", files[i][1])
        end,
    })
end)

mp.add_key_binding(nil, "select-binding", function ()
    local bindings = {}

    for _, binding in pairs(mp.get_property_native("input-bindings")) do
        if binding.priority >= 0 and (
               bindings[binding.key] == nil or
               (bindings[binding.key].is_weak and not binding.is_weak) or
               (binding.is_weak == bindings[binding.key].is_weak and
                binding.priority > bindings[binding.key].priority)
        ) then
            bindings[binding.key] = binding
        end
    end

    local items = {}
    for _, binding in pairs(bindings) do
        if binding.cmd ~= "ignore" then
            items[#items + 1] = binding.key .. " " .. binding.cmd
        end
    end

    table.sort(items)

    input.select({
        prompt = "Select a binding:",
        items = items,
        submit = function (i)
            mp.command(items[i]:gsub("^.- ", ""))
        end,
    })
end)

local properties = {}

local function add_property(property, value)
    value = value or mp.get_property_native(property)

    if type(value) == "table" and next(value) then
        for key, val in pairs(value) do
            add_property(property .. "/" .. key, val)
        end
    else
        properties[#properties + 1] = property .. ": " .. utils.to_string(value)
    end
end

mp.add_key_binding(nil, "show-properties", function ()
    properties = {}

    -- Don't log errors for renamed and removed properties.
    local msg_level_backup = mp.get_property("msg-level")
    mp.set_property("msg-level", msg_level_backup == "" and "cplayer=no"
                                 or msg_level_backup .. ",cplayer=no")

    for _, property in pairs(mp.get_property_native("property-list")) do
        add_property(property)
    end

    mp.set_property("msg-level", msg_level_backup)

    add_property("current-tracks/audio")
    add_property("current-tracks/video")
    add_property("current-tracks/sub")
    add_property("current-tracks/sub2")

    table.sort(properties)

    input.select({
        prompt = "Inspect a property:",
        items = properties,
        submit = function (i)
            if mp.get_property_native("vo-configured") then
                mp.commandv("expand-properties", "show-text",
                            (#properties[i] > 100 and
                             "${osd-ass-cc/0}{\\fs9}${osd-ass-cc/1}" or "") ..
                            "$>" .. properties[i], 20000)
            else
                mp.msg.info(properties[i])
            end
        end,
    })
end)

local function system_open(path)
    local platform = mp.get_property("platform")
    local args
    if platform == "windows" then
        args = {"rundll32", "url.dll,FileProtocolHandler", path}
    elseif platform == "darwin" then
        args = {"open", path}
    else
        args = {"gio", "open", path}
    end

    mp.commandv("run", unpack(args))
end

local function edit_config_file(filename)
    if not mp.get_property_bool("config") then
        show_warning("Editing config files with --no-config is not supported.")
        return
    end

    local path = mp.find_config_file(filename)

    if not path then
        path = mp.command_native({"expand-path", "~~/" .. filename})
        local file_handle, error_message = io.open(path, "w")

        if not file_handle then
            show_error(error_message)
            return
        end

        file_handle:close()
    end

    system_open(path)
end

mp.add_key_binding(nil, "edit-config-file", function ()
    edit_config_file("mpv.conf")
end)

mp.add_key_binding(nil, "edit-input-conf", function ()
    edit_config_file("input.conf")
end)

mp.add_key_binding(nil, "open-docs", function ()
    system_open("https://mpv.io/manual/")
end)

mp.add_key_binding(nil, "menu", function ()
    local sub_track_count = 0
    local audio_track_count = 0
    local video_track_count = 0
    local text_sub_selected = false
    local is_disc = mp.get_property("current-demuxer") == "disc"

    local image_sub_codecs = {["dvd_subtitle"] = true, ["hdmv_pgs_subtitle"] = true}

    for _, track in pairs(mp.get_property_native("track-list")) do
        if track.type == "sub" then
            sub_track_count = sub_track_count + 1

            if track["main-selection"] == 0 and not image_sub_codecs[track.codec] then
                text_sub_selected = true
            end
        elseif track.type == "audio" then
            audio_track_count = audio_track_count + 1
        elseif track.type == "video" then
            video_track_count = video_track_count + 1
        end
    end

    local menu = {
        {"Subtitles", "script-binding select/select-sid", sub_track_count > 0},
        {"Secondary subtitles", "script-binding select/select-secondary-sid", sub_track_count > 1},
        {"Subtitle lines", "script-binding select/select-subtitle-line", text_sub_selected},
        {"Audio tracks", "script-binding select/select-aid", audio_track_count > 1},
        {"Video tracks", "script-binding select/select-vid", video_track_count > 1},
        {"Playlist", "script-binding select/select-playlist",
         mp.get_property_native("playlist-count") > 1},
        {"Chapters", "script-binding select/select-chapter", mp.get_property("chapter")},
        {is_disc and "Titles" or "Editions", "script-binding select/select-edition",
         mp.get_property_native("edition-list/count", 0) > 1},
        {"Audio devices", "script-binding select/select-audio-device", audio_track_count > 0},
        {"Key bindings", "script-binding select/select-binding", true},
        {"History", "script-binding select/select-watch-history", true},
        {"Watch later", "script-binding select/select-watch-later", true},
        {"Playback statistics", "script-binding stats/display-page-1-toggle", true},
        {"File information", "script-binding stats/display-page-5-toggle",
         mp.get_property("filename")},
        {"Edit config file", "script-binding select/edit-config-file", true},
        {"Edit key bindings", "script-binding select/edit-input-conf", true},
        {"Help", "script-binding stats/display-page-4-toggle", true},
        {"Online documentation", "script-binding select/open-docs", true},
    }

    local labels = {}
    local commands = {}

    for _, entry in ipairs(menu) do
        if entry[3] then
            labels[#labels + 1] = entry[1]
            commands[#commands + 1] = entry[2]
        end
    end

    input.select({
        prompt = "",
        items = labels,
        keep_open = true,
        submit = function (i)
            mp.command(commands[i])

            if not commands[i]:find("^script%-binding select/select") then
                input.terminate()
            end
        end,
    })
end)
