'''
Function:
    Implementation of KuwoMusicClient Utils
Author:
    Zhenchao Jin
WeChat Official Account (微信公众号):
    Charles的皮卡丘
'''
import re
import math
import zlib
import base64


'''settings'''
MASK32 = (1 << 32) - 1
MASK64 = (1 << 64) - 1


'''HelperFunctions'''
class HelperFunctions():
    @staticmethod
    def u64(x: int) -> int: return x & MASK64
    @staticmethod
    def u32(x: int) -> int: return x & MASK32
    @staticmethod
    def rangen(n: int): return range(n)
    @staticmethod
    def power2(n: int) -> int: return 1 << n
    @staticmethod
    def longarray(*arr): return list(arr)


'''settings'''
SECRET_KEY_SONG, SECRET_KEY_LYRIC = b"ylzsxkwm", b'yeelion'
ARRAYLS = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
ARRAYLSMASK = HelperFunctions.longarray(0, 0x100001, 0x300003)
ARRAYE = HelperFunctions.longarray(31, 0, 1, 2, 3, 4, -1, -1, 3, 4, 5, 6, 7, 8, -1, -1, 7, 8, 9, 10, 11, 12, -1, -1, 11, 12, 13, 14, 15, 16, -1, -1, 15, 16, 17, 18, 19, 20, -1, -1, 19, 20, 21, 22, 23, 24, -1, -1, 23, 24, 25, 26, 27, 28, -1, -1, 27, 28, 29, 30, 31, 30, -1, -1)
ARRAYIP1 = HelperFunctions.longarray(39, 7, 47, 15, 55, 23, 63, 31, 38, 6, 46, 14, 54, 22, 62, 30, 37, 5, 45, 13, 53, 21, 61, 29, 36, 4, 44, 12, 52, 20, 60, 28, 35, 3, 43, 11, 51, 19, 59, 27, 34, 2, 42, 10, 50, 18, 58, 26, 33, 1, 41, 9, 49, 17, 57, 25, 32, 0, 40, 8, 48, 16, 56, 24)
ARRAYIP2 = HelperFunctions.longarray(57, 49, 41, 33, 25, 17, 9, 1, 59, 51, 43, 35, 27, 19, 11, 3, 61, 53, 45, 37, 29, 21, 13, 5, 63, 55, 47, 39, 31, 23, 15, 7, 56, 48, 40, 32, 24, 16, 8, 0, 58, 50, 42, 34, 26, 18, 10, 2, 60, 52, 44, 36, 28, 20, 12, 4, 62, 54, 46, 38, 30, 22, 14, 6)
ARRAYMASK = [HelperFunctions.power2(n) for n in HelperFunctions.rangen(64)]
ARRAYMASK[-1] = -ARRAYMASK[-1]
ARRAYP = HelperFunctions.longarray(15, 6, 19, 20, 28, 11, 27, 16, 0, 14, 22, 25, 4, 17, 30, 9, 1, 7, 23, 13, 31, 26, 2, 8, 18, 12, 29, 5, 21, 10, 3, 24)
ARRAYPC1 = HelperFunctions.longarray(56, 48, 40, 32, 24, 16, 8, 0, 57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 60, 52, 44, 36, 28, 20, 12, 4, 27, 19, 11, 3)
ARRAYPC2 = HelperFunctions.longarray(13, 16, 10, 23, 0, 4, -1, -1, 2, 27, 14, 5, 20, 9, -1, -1, 22, 18, 11, 3, 25, 7, -1, -1, 15, 6, 26, 19, 12, 1, -1, -1, 40, 51, 30, 36, 46, 54, -1, -1, 29, 39, 50, 44, 32, 47, -1, -1, 43, 48, 38, 55, 33, 52, -1, -1, 45, 41, 49, 35, 28, 31, -1, -1)
MATRIXNSBOX = [
    [14,4,3,15,2,13,5,3,13,14,6,9,11,2,0,5,4,1,10,12,15,6,9,10,1,8,12,7,8,11,7,0,0,15,10,5,14,4,9,10,7,8,12,3,13,1,3,6,15,12,6,11,2,9,5,0,4,2,11,14,1,7,8,13],
    [15,0,9,5,6,10,12,9,8,7,2,12,3,13,5,2,1,14,7,8,11,4,0,3,14,11,13,6,4,1,10,15,3,13,12,11,15,3,6,0,4,10,1,7,8,4,11,14,13,8,0,6,2,15,9,5,7,1,10,12,14,2,5,9],
    [10,13,1,11,6,8,11,5,9,4,12,2,15,3,2,14,0,6,13,1,3,15,4,10,14,9,7,12,5,0,8,7,13,1,2,4,3,6,12,11,0,13,5,14,6,8,15,2,7,10,8,15,4,9,11,5,9,0,14,3,10,7,1,12],
    [7,10,1,15,0,12,11,5,14,9,8,3,9,7,4,8,13,6,2,1,6,11,12,2,3,0,5,14,10,13,15,4,13,3,4,9,6,10,1,12,11,0,2,5,0,13,14,2,8,15,7,4,15,1,10,7,5,6,12,11,3,8,9,14],
    [2,4,8,15,7,10,13,6,4,1,3,12,11,7,14,0,12,2,5,9,10,13,0,3,1,11,15,5,6,8,9,14,14,11,5,6,4,1,3,10,2,12,15,0,13,2,8,5,11,8,0,15,7,14,9,4,12,7,10,9,1,13,6,3],
    [12,9,0,7,9,2,14,1,10,15,3,4,6,12,5,11,1,14,13,0,2,8,7,13,15,5,4,10,8,3,11,6,10,4,6,11,7,9,0,6,4,2,13,1,9,15,3,8,15,3,1,14,12,5,11,0,2,12,14,7,5,10,8,13],
    [4,1,3,10,15,12,5,0,2,11,9,6,8,7,6,9,11,4,12,15,0,3,10,5,14,13,7,8,13,14,1,2,13,6,14,9,4,1,2,14,11,13,5,0,1,10,8,3,0,11,3,5,9,4,15,2,7,8,12,15,10,7,6,12],
    [13,7,10,0,6,9,5,15,8,4,3,10,11,14,12,5,2,11,9,6,15,12,0,3,4,1,14,13,1,2,7,8,1,2,12,15,10,4,0,3,13,14,6,9,7,8,9,6,15,1,5,12,3,10,14,5,8,7,11,0,4,13,2,11],
]


'''KuwoMusicClientUtils'''
class KuwoMusicClientUtils:
    '''bittransform'''
    @staticmethod
    def bittransform(arr_int, n, l):
        l2 = 0
        for i in HelperFunctions.rangen(n):
            idx = arr_int[i]
            if idx < 0: continue
            if (l & ARRAYMASK[idx]) == 0: continue
            l2 |= ARRAYMASK[i]
        return HelperFunctions.u64(l2)
    '''des64'''
    @staticmethod
    def des64(longs, l):
        p_r, p_source, out = [0] * 8, [0, 0], KuwoMusicClientUtils.bittransform(ARRAYIP2, 64, l)
        p_source[0], p_source[1] = HelperFunctions.u32(out), HelperFunctions.u32((out & 0xFFFFFFFF00000000) >> 32)
        for i in HelperFunctions.rangen(16):
            s_out, R = 0, KuwoMusicClientUtils.bittransform(ARRAYE, 64, p_source[1])
            R ^= longs[i]
            for j in HelperFunctions.rangen(8): p_r[j] = (R >> (j * 8)) & 0xFF
            for sbi in reversed(HelperFunctions.rangen(8)): s_out = (s_out << 4) | (MATRIXNSBOX[sbi][p_r[sbi]] & 0xF)
            R, L = KuwoMusicClientUtils.bittransform(ARRAYP, 32, s_out), p_source[0]
            p_source[0] = p_source[1]
            p_source[1] = HelperFunctions.u32(L ^ R)
        p_source.reverse()
        out = ((p_source[1] << 32) & 0xFFFFFFFF00000000) | (p_source[0] & 0xFFFFFFFF)
        out = KuwoMusicClientUtils.bittransform(ARRAYIP1, 64, out)
        return HelperFunctions.u64(out)
    '''subkeys'''
    @staticmethod
    def subkeys(l, longs, mode):
        l2 = KuwoMusicClientUtils.bittransform(ARRAYPC1, 56, l)
        for i in HelperFunctions.rangen(16):
            r = ARRAYLS[i]
            mask = ARRAYLSMASK[r]
            not_mask = HelperFunctions.u64(~mask)
            part1, part2 = HelperFunctions.u64((l2 & mask) << (28 - r)), (l2 & not_mask) >> r
            l2 = HelperFunctions.u64(part1 | part2)
            longs[i] = KuwoMusicClientUtils.bittransform(ARRAYPC2, 64, l2)
        if mode == 1:
            for j in HelperFunctions.rangen(8): longs[j], longs[15 - j] = longs[15 - j], longs[j]
    '''crypt'''
    @staticmethod
    def crypt(msg: bytes, key: bytes, mode: int) -> bytes:
        l = 0
        for i in HelperFunctions.rangen(8): l |= (key[i] & 0xFF) << (i * 8)
        l, j, arr_long1 = HelperFunctions.u64(l), len(msg) // 8, [0] * 16
        KuwoMusicClientUtils.subkeys(l, arr_long1, mode)
        arr_long2 = [0] * j
        for m in HelperFunctions.rangen(j):
            v = 0
            for n in HelperFunctions.rangen(8): v |= (msg[n + m * 8] & 0xFF) << (n * 8)
            arr_long2[m] = HelperFunctions.u64(v)
        arr_long3 = [0] * ((1 + 8 * (j + 1)) // 8)
        for i1 in HelperFunctions.rangen(j): arr_long3[i1] = KuwoMusicClientUtils.des64(arr_long1, arr_long2[i1])
        arr_byte1, l2 = msg[j * 8:], 0
        for i1 in HelperFunctions.rangen(len(msg) % 8): l2 |= (arr_byte1[i1] & 0xFF) << (i1 * 8)
        l2 = HelperFunctions.u64(l2)
        if len(arr_byte1) != 0 or mode == 0: arr_long3[j] = KuwoMusicClientUtils.des64(arr_long1, l2)
        out_bytes, i4 = bytearray(8 * len(arr_long3)), 0
        for l3 in arr_long3:
            for i6 in HelperFunctions.rangen(8): out_bytes[i4] = (l3 >> (i6 * 8)) & 0xFF; i4 += 1
        return bytes(out_bytes)
    '''encrypt'''
    @staticmethod
    def encrypt(msg: bytes) -> bytes:
        return KuwoMusicClientUtils.crypt(msg, SECRET_KEY_SONG, 0)
    '''decrypt'''
    @staticmethod
    def decrypt(msg: bytes) -> bytes:
        return KuwoMusicClientUtils.crypt(msg, SECRET_KEY_SONG, 1)
    '''encryptquery'''
    @staticmethod
    def encryptquery(query: str) -> str:
        return base64.b64encode(KuwoMusicClientUtils.encrypt(query.encode("utf-8"))).decode("ascii")
    '''xorencrypt'''
    @staticmethod
    def xorencrypt(data: bytes, key: bytes) -> bytes:
        key_len = len(key)
        output = bytearray(len(data))
        for i in range(len(data)): output[i] = data[i] ^ key[i % key_len]
        return bytes(output)
    '''buildlyricsparams'''
    @staticmethod
    def buildlyricsparams(music_id, is_get_lyricx: bool = True):
        params_str = f"user=12345,web,web,web&requester=localhost&req=1&rid=MUSIC_{music_id}"
        if is_get_lyricx: params_str += '&lrcx=1'
        buf_str = params_str.encode('utf-8')
        encrypted_bytes = KuwoMusicClientUtils.xorencrypt(buf_str, SECRET_KEY_LYRIC)
        final_params = base64.b64encode(encrypted_bytes).decode('utf-8')
        return final_params
    '''decodelyrics'''
    @staticmethod
    def decodelyrics(buf: bytes, is_get_lyricx: bool):
        if buf[:10] != b'tp=content': return ''
        try: split_index = buf.index(b'\r\n\r\n') + 4; compressed_data = buf[split_index:]
        except ValueError: return ''
        try: lrc_data = zlib.decompress(compressed_data)
        except zlib.error: return ''
        if not is_get_lyricx: return lrc_data.decode('gb18030', errors='ignore')
        base64_str = lrc_data.decode('utf-8')
        buf_str = base64.b64decode(base64_str)
        decrypted_buffer = KuwoMusicClientUtils.xorencrypt(buf_str, SECRET_KEY_LYRIC)
        final_lrc = decrypted_buffer.decode('gb18030', errors='ignore')
        return final_lrc
    '''formatlyricstime'''
    @staticmethod
    def formatlyricstime(ms):
        if math.isnan(ms) or ms < 0: ms = 0
        total_seconds = ms / 1000
        minutes = math.floor(total_seconds / 60)
        seconds = math.floor(total_seconds % 60)
        milliseconds = round((ms % 1000))
        return f"[{minutes:02}:{seconds:02}.{milliseconds:03}]"
    '''convertrawlrc'''
    @staticmethod
    def convertrawlrc(raw_lrc: str) -> str:
        out, i = [], 0
        lines, rx_line, rx_word, rx_zh = re.split(r"\r\n|\r|\n", raw_lrc), re.compile(r"^\[(\d{2}:\d{2}\.\d{3})\](.*)$"), re.compile(r"<(-?\d+),(-?\d+)>([^<]*)"), re.compile(r"[\u4e00-\u9fa5]")
        while i < len(lines):
            line = lines[i]
            m = rx_line.match(line)
            if not m: out.append(line); i += 1; continue
            ts, payload = m.group(1), m.group(2)
            if not payload.replace("<0,0>", "").strip(): i += 1; continue
            if payload.startswith("<0,0>") and rx_zh.search(payload): i += 1; continue
            words = list(rx_word.finditer(payload))
            lyric = "".join(w.group(3) for w in words) if words else payload.replace("<0,0>", "").strip(); trans = ""
            if i + 1 < len(lines) and (nm := rx_line.match(lines[i + 1])):
                next_payload = nm.group(2)
                if next_payload.startswith("<0,0>") and rx_zh.search(next_payload): trans = next_payload.replace("<0,0>", "").strip(); i += 1
            out.append(f"[{ts}]{lyric}")
            if trans: out.append(f"[{ts}]{trans}")
            i += 1
        return "\n".join(out)