local utils = require("mp.utils")

-- from http://lua-users.org/wiki/LuaUnicode
local UTF8_PATTERN = '[%z\1-\127\194-\244][\128-\191]*'

-- return a substring based on utf8 characters
-- like string.sub, but negative index is not supported
function utf8_sub(s, i, j)
    if i > j then
        return s
    end
    local t, idx = {}, 1
    for char in s:gmatch(UTF8_PATTERN) do
        if idx >= i and idx <= j then
            t[#t + 1] = char
        end
        idx = idx + 1
    end
    return table.concat(t)
end

function utf8_len(s)
    local count = 0
    for _ in s:gmatch(UTF8_PATTERN) do
        count = count + 1
    end
    return count
end

function utf8_iter(s)
    local iter = s:gmatch(UTF8_PATTERN)
    return function()
        return iter()
    end
end

function utf8_to_table(s)
    local t = {}
    for ch in utf8_iter(s) do
        t[#t + 1] = ch
    end
    return t
end

-- abbreviate string if it's too long
function abbr_str(str, length)
    if not str or str == '' then return '' end
    local str_clip = utf8_sub(str, 1, length)
    if str ~= str_clip then
        return str_clip .. '...'
    end
    return str
end

function get_str_width(text, font_size)
    local width = 0
    for i = 1, #text do
        local byte = string.byte(text, i)
        if byte > 127 then
            width = width + 2
        else
            width = width + 1
        end
    end

    local unicode_width = 0
    local i = 1
    while i <= #text do
        local byte = string.byte(text, i)
        local char_len
        if byte < 128 then char_len = 1; unicode_width = unicode_width + 1
        elseif byte >= 192 and byte < 224 then char_len = 2; unicode_width = unicode_width + 2
        elseif byte >= 224 and byte < 240 then char_len = 3; unicode_width = unicode_width + 2
        elseif byte >= 240 and byte < 248 then char_len = 4; unicode_width = unicode_width + 2
        else char_len = 1; unicode_width = unicode_width + 1
        end
        i = i + char_len
    end
    return unicode_width * (font_size / 2)
end

function unicode_to_utf8(unicode)
    if unicode < 0x80 then
        return string.char(unicode)
    else
        local byte_count
        if unicode < 0x800 then
            byte_count = 2
        elseif unicode < 0x10000 then
            byte_count = 3
        elseif unicode < 0x110000 then
            byte_count = 4
        else
            return
        end

        local res = {}
        local shift = 2 ^ 6
        local after_shift = unicode
        for _ = byte_count, 2, -1 do
            local before_shift = after_shift
            after_shift = math.floor(before_shift / shift)
            table.insert(res, 1, before_shift - after_shift * shift + 0x80)
        end
        shift = 2 ^ (8 - byte_count)
        table.insert(res, 1, after_shift + math.floor(0xFF / shift) * shift)
        ---@diagnostic disable-next-line: deprecated
        return string.char(unpack(res))
    end
end

function jaro(s1, s2)
    local match_window = math.floor(math.max(#s1, #s2) / 2.0) - 1
    local matches1 = {}
    local matches2 = {}

    local m = 0;
    local t = 0;

    for i = 0, #s1, 1 do
        local start = math.max(0, i - match_window)
        local final = math.min(i + match_window + 1, #s2)

        for k = start, final, 1 do
            if not (matches2[k] or s1[i] ~= s2[k]) then
                matches1[i] = true
                matches2[k] = true
                m = m + 1
                break
            end
        end
    end

    if m == 0 then
        return 0.0
    end

    local k = 0
    for i = 0, #s1, 1 do
        if matches1[i] then
            while not matches2[k] do
                k = k + 1
            end

            if s1[i] ~= s2[k] then
                t = t + 1
            end

            k = k + 1
        end
    end

    t = t / 2.0

    return (m / #s1 + m / #s2 + (m - t) / m) / 3.0
end

function jaro_winkler(s1, s2)
    if #s1 + #s2 == 0 then
        return 0.0
    end

    if s1 == s2 then
        return 1.0
    end

    s1 = utf8_to_table(s1)
    s2 = utf8_to_table(s2)

    local d = jaro(s1, s2)
    local p = 0.1
    local l = 0;
    while (s1[l] == s2[l] and l < 4) do
        l = l + 1
    end

    return d + l * p * (1 - d)
end

-- 从时间字符串转换为秒数
function time_to_seconds(time_str)
    local h, m, s = time_str:match("(%d+):(%d+):([%d%.]+)")
    return tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s)
end

-- 从秒数转换为时间字符串
function seconds_to_time(seconds)
    local hours = math.floor(seconds / 3600)
    local minutes = math.floor((seconds % 3600) / 60)
    local secs = math.floor(seconds % 60)
    local centiseconds = math.floor((seconds - math.floor(seconds)) * 100)
    return string.format("%d:%02d:%02d.%02d", hours, minutes, secs, centiseconds)
end

function is_chinese(str)
    return string.match(str, "[\228-\233][\128-\191]") ~= nil
end

function is_protocol(path)
    return type(path) == 'string' and (path:find('^%a[%w.+-]-://') ~= nil or path:find('^%a[%w.+-]-:%?') ~= nil)
end

function hex_to_bin(hexstr)
    return (hexstr:gsub('..', function (cc)
        return string.char(tonumber(cc, 16))
    end))
end

function hex_to_char(x)
    return string.char(tonumber(x, 16))
end

-- url编码转换
function url_encode(str)
    -- 将非安全字符转换为百分号编码
    if str then
        str = str:gsub("([^%w%-%.%_%~])", function(c)
            return string.format("%%%02X", string.byte(c))
        end)
    end
    return str
end

-- url解码转换
function url_decode(str)
    if str ~= nil then
        str = str:gsub('^%a[%a%d-_]+://', '')
              :gsub('^%a[%a%d-_]+:\\?', '')
              :gsub('%%(%x%x)', hex_to_char)
        if str:find('://localhost:?') then
            str = str:gsub('^.*/', '')
        end
        str = str:gsub('%?.+', '')
              :gsub('%+', ' ')
        return str
    end
end

-- Utility function to split a string by a delimiter
function split(str, delim)
    local result = {}
    for match in (str .. delim):gmatch("(.-)" .. delim) do
        table.insert(result, match)
    end
    return result
end

function table_to_zero_indexed(tbl)
    for i = #tbl, 1, -1 do
        tbl[i - 1] = tbl[i]
    end
    tbl[#tbl] = nil
    return tbl
end

function itable_index_of(itable, value)
    for index = 1, #itable do
        if itable[index] == value then
            return index
        end
    end
end

function is_nested_table(t)
    if type(t) ~= "table" then
        return false
    end
    for _, v in pairs(t) do
        if type(v) == "table" then
            return true
        end
    end
    return false
end

function shallow_copy(original)
    if type(original) ~= "table" then
        return original
    end
    local copy = {}
    for k, v in pairs(original) do
        copy[k] = v
    end
    return copy
end

function deep_copy(obj, seen)
    if type(obj) ~= "table" then
        return obj
    end
    if seen and seen[obj] then
        return seen[obj]
    end
    local s = seen or {}
    local copy = {}
    s[obj] = copy
    for k, v in pairs(obj) do
        copy[deep_copy(k, s)] = deep_copy(v, s)
    end
    setmetatable(copy, getmetatable(obj))
    return copy
end

function remove_query(url)
    local qpos = string.find(url, "?", 1, true)
    if qpos then
        return string.sub(url, 1, qpos - 1)
    else
        return url
    end
end

function file_exists(path)
    if path then
        local meta = utils.file_info(path)
        return meta and meta.is_file
    end
    return false
end

function is_writable(path)
    local file = io.open(path, "w")
    if file then
        file:close()
        os.remove(path)
        return true
    end
    return false
end

function contains_any(tab, val)
    for _, element in pairs(tab) do
        if string.find(val, element) then
            return true
        end
    end
    return false
end

--读history 和 写history
function read_file(file_path)
    local file = io.open(file_path, "r") -- 打开文件，"r"表示只读模式
    if not file then
        return nil
    end
    local content = file:read("*all") -- 读取文件所有内容
    file:close()                      -- 关闭文件
    return content
end

-- 应用额外的自定义标题替换规则
function title_replace(title)
    local title_replace = utils.parse_json(options.title_replace)
    if not title_replace then
        return title
    end
    for _, v in pairs(title_replace) do
        for _, indexrules in pairs(v['rules']) do
            for rule, override in pairs(indexrules) do
                title = title:gsub(rule, override)
                        :gsub("[_%.]", " ")
                        :gsub("^%s*(.-)%s*$", "%1")
                        :gsub("[@#%.%+%-%%&*_=,/~`]+$", "")
            end
        end
    end
    return title
end

function write_json_file(file_path, data)
    local file = io.open(file_path, "w")
    if not file then
        return
    end
    file:write(utils.format_json(data)) -- 将 Lua 表转换为 JSON 并写入
    file:close()
end

-- 拆分字符串中的字符和数字
local function split_by_numbers(filename)
    local parts = {}
    local pattern = "([^%d]*)(%d+)([^%d]*)"
    for pre, num, post in string.gmatch(filename, pattern) do
        table.insert(parts, {pre = pre, num = tonumber(num), post = post})
    end
    return parts
end

-- 识别并匹配前后剧集
local function compare_filenames(fname1, fname2)
    local parts1 = split_by_numbers(fname1)
    local parts2 = split_by_numbers(fname2)

    local min_len = math.min(#parts1, #parts2)

    -- 逐个部分进行比较
    for i = 1, min_len do
        local part1 = parts1[i]
        local part2 = parts2[i]

        -- 比较数字前的字符是否相同
        if part1.pre ~= part2.pre then
            return false
        end

        -- 比较数字部分
        if part1.num ~= part2.num then
            return part1.num, part2.num
        end

        -- 比较数字后的字符是否相同
        if part1.post ~= part2.post then
            return false
        end
    end

    return false
end

-- 规范化路径
function normalize(path)
    if normalize_path ~= nil then
        if normalize_path then
            path = mp.command_native({"normalize-path", path})
        else
            local directory = mp.get_property("working-directory", "")
            path = utils.join_path(directory, path:gsub('^%.[\\/]',''))
            if PLATFORM == "windows" then path = path:gsub("\\", "/") end
        end
        return path
    end

    normalize_path = false

    local commands = mp.get_property_native("command-list", {})
    for _, command in ipairs(commands) do
        if command.name == "normalize-path" then
            normalize_path = true
            break
        end
    end
    return normalize(path)
end

-- 获取父目录路径
function get_parent_directory(path)
    local dir = nil
    if path and not is_protocol(path) then
        path = normalize(path)
        dir = utils.split_path(path)
    end
    return dir
end

-- 获取播放文件标题信息
function parse_title()
    local path = mp.get_property("path")
    local filename = mp.get_property("filename/no-ext")

    if not filename then
        return
    end
    local thin_space = string.char(0xE2, 0x80, 0x89)
    filename = filename:gsub(thin_space, " ")
    local media_title, season, episode = nil, nil, nil
    if path and not is_protocol(path) then
        local title = format_filename(filename)
        if title then
            media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
            if season then
                return title_replace(media_title), season, episode
            else
                media_title, episode = title:match("^(.-)%s*[eE](%d+)")
                if episode then
                    return title_replace(media_title), season, episode
                end
            end
            return title_replace(title)
        end

        local directory = get_parent_directory(path)
        local dir, title = utils.split_path(directory:sub(1, -2))
        if title:lower():match("^%s*seasons?%s*%d+%s*$") or title:lower():match("^%s*specials?%s*$") or title:match("^%s*SPs?%s*$")
        or title:match("^%s*O[VAD]+s?%s*$") or title:match("^%s*第.-[季部]+%s*$") then
            directory, title = utils.split_path(dir:sub(1, -2))
        end
        title = title
                :gsub(thin_space, " ")
                :gsub("%[.-%]", "")
                :gsub("^%s*%(%d+.?%d*.?%d*%)", "")
                :gsub("%(%d+.?%d*.?%d*%)%s*$", "")
                :gsub("[%._]", " ")
                :gsub("^%s*(.-)%s*$", "%1")
        return title_replace(title)
    end

    local title = mp.get_property("media-title")
    if title then
        title = title:gsub(thin_space, " ")
        local ftitle = url_decode(title)
        local name, class = ftitle:match("^(.-)%s*|%s*(.-)%s*$")
        if name then ftitle = name end
        local format_title = format_filename(ftitle)
        if format_title then
            media_title, season, episode = format_title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
            if season then
                title = media_title
            else
                media_title, episode = format_title:match("^(.-)%s*[eE](%d+)")
                if episode then
                    season = 1
                    title = media_title
                else
                    title = format_title
                end
            end
        end
    end

    return title_replace(title), season, episode
end

-- 获取当前文件名所包含的集数
function get_episode_number(filename, fname)
    -- 尝试对比记录文件名来获取当前集数
    if fname then
        local episode_num1, episode_num2 = compare_filenames(fname, filename)
        if episode_num1 and episode_num2 then
            return episode_num1, episode_num2
        else
            return nil, nil
        end
    end

    local thin_space = string.char(0xE2, 0x80, 0x89)
    filename = filename:gsub(thin_space, " ")

    local title = format_filename(filename)
    if title then
        local media_title, season, episode = title:match("^(.-)%s*[sS](%d+)[eE](%d+)")
        if season then
            return tonumber(episode)
        else
            local media_title, episode = title:match("^(.-)%s*[eE](%d+)")
            if episode then
                return tonumber(episode)
            end
        end
    end
    return nil
end

local CHINESE_NUM_MAP = {
    ["零"] = 0, ["一"] = 1, ["二"] = 2, ["三"] = 3, ["四"] = 4,
    ["五"] = 5, ["六"] = 6, ["七"] = 7, ["八"] = 8, ["九"] = 9,
    ["十"] = 10, ["百"] = 100, ["千"] = 1000, ["万"] = 10000,
}

function chinese_to_number(cn)
    local total = 0
    local num = 0
    local unit = 1

    local chars = {}
    for uchar in cn:gmatch(UTF8_PATTERN) do
        table.insert(chars, 1, uchar)
    end

    for _, char in ipairs(chars) do
        local val = CHINESE_NUM_MAP[char]
        if val then
            if val >= 10 then
                if num == 0 then
                    num = 1
                end
                unit = val
            else
                total = total + val * unit
                unit = 1
                num = 0
            end
        end
    end

    if unit > 1 then
        total = total + num * unit
    end

    if total > 0 then
        return total
    else
        return num
    end
end

local CHINESE_NUM = {"零", "一", "二", "三", "四", "五", "六", "七", "八", "九"}
local CHINESE_UNIT = {"", "十", "百", "千"}
local CHINESE_BIG_UNIT = {"", "万", "亿"}

function number_to_chinese(num)
    if num == 0 then return "零" end

    local str = tostring(num)
    local len = #str
    local result = ""
    local zero_flag = false

    for i = 1, len do
        local digit = tonumber(str:sub(i, i))
        local pos = len - i + 1
        local small_unit_index = (pos - 1) % 4 + 1
        local small_unit = CHINESE_UNIT[small_unit_index]

        if digit == 0 then
            zero_flag = true
        else
            if zero_flag then
                result = result .. "零"
                zero_flag = false
            end
            if digit == 1 and small_unit_index == 2 and i == 1 then
                result = result .. small_unit
            else
                result = result .. CHINESE_NUM[digit + 1] .. small_unit
            end
        end

        if pos % 4 == 1 and pos > 1 then
            local big_unit_index = math.floor((pos - 1) / 4)
            result = result .. CHINESE_BIG_UNIT[big_unit_index + 1]
        end
    end

    result = result:gsub("零+$", "")

    return result
end

-- 异步执行命令
-- 同时返回 abort 函数，用于取消异步命令
function call_cmd_async(args, callback)
    async_running = true
    local abort_signal = mp.command_native_async({
        name = 'subprocess',
        capture_stderr = true,
        capture_stdout = true,
        playback_only = false,
        args = args,
    }, function(success, result, error)
        if not success or not result or result.status ~= 0 then
            local exit_code = (result and result.status or 'unknown')
            local message = error or (result and result.stdout .. result.stderr) or ''
            callback('Calling failed. Exit code: ' .. exit_code .. ' Error: ' .. message, {})
            return
        end

        local json = result and type(result.stdout) == 'string' and result.stdout or ''
        return callback(nil, json)
    end)

    return function()
        mp.abort_async_command(abort_signal)
    end
end