import json
import os
import random
import re
import shutil
from base64 import b64encode, b64decode

import cv2
import numpy as np
import regex as re2
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes

import string
import random


def encrypt(content):
    try:
        iv = get_random_bytes(12)
        cipher = AES.new(aesKey.encode('utf-8'), AES.MODE_GCM, nonce=iv)
        encrypt_data, tag = cipher.encrypt_and_digest(content.encode('utf-8'))
        result = iv + encrypt_data + tag
        return b64encode(result).decode('utf-8')
    except Exception as e:
        print('error: ' + str(e))
    return content


def decrypt(content):
    try:
        combined = b64decode(content.encode('utf-8'))
        iv = combined[:12]
        tag = combined[-16:]
        encrypt_data = combined[12:-16]
        cipher = AES.new(aesKey.encode('utf-8'), AES.MODE_GCM, nonce=iv)
        decrypt_data = cipher.decrypt_and_verify(encrypt_data, tag)
        return decrypt_data.decode('utf-8')
    except (ValueError, KeyError) as e:
        print(e)
    return content


def get_classes_impl(result, class_type, path='.'):
    listdir = os.listdir(path)
    for i in listdir:
        path_join = os.path.join(path, i)
        if os.path.isdir(path_join):
            if path.endswith('.') and i in ["build", "debug", "release"]:
                continue
            if path.endswith('src') and i in ["androidTest", "test"]:
                continue
            get_classes_impl(result, class_type, path_join)
        elif os.path.isfile(path_join):
            if os.path.splitext(i)[-1][1:] in class_type:
                result.append(path_join)


def get_classes(class_type=None):
    if class_type is None:
        class_type = ["kt", "java"]
    result = []
    get_classes_impl(result, class_type)
    return result


def find_text_impl(reg, result, skip_name, path, is_one):
    listdir = os.listdir(path)
    for i in listdir:
        if is_one and len(result) > 0:
            return
        path_join = os.path.join(path, i)
        if os.path.isdir(path_join):
            if path.endswith('.') and i in ["build", "debug", "release"]:
                continue
            if path.endswith('src') and i in ["androidTest", "test"]:
                continue
            find_text_impl(reg, result, skip_name, path_join, is_one)
        elif os.path.isfile(path_join):
            if path.endswith('.') and i.split('.')[-1] in ['py']:
                continue
            is_skip = False
            for j in skip_name:
                if j in i:
                    is_skip = True
                    break
            if is_skip:
                continue
            with open(path_join, 'r', encoding='utf-8') as f:
                if not f.readable():
                    continue
                try:
                    text = f.read()
                except ValueError:
                    continue
                findall = re.findall(reg, text)
                if len(findall) > 0:
                    result.extend(findall)


def find_text(reg, path='.', is_one=False, skip_name=None):
    if skip_name is None:
        skip_name = []
    result = []
    if os.path.isdir(path):
        find_text_impl(reg, result, skip_name, path, is_one)
    else:
        with open(path, 'r', encoding='utf-8') as f:
            if f.readable():
                try:
                    text = f.read()
                    findall = re.findall(reg, text)
                    if len(findall) > 0:
                        result.extend(findall)
                except ValueError:
                    pass
    if is_one:
        if len(result) > 0:
            return result[0]
        else:
            return None
    return result


def find_file_impl(reg, result, skip_name, path='.'):
    listdir = os.listdir(path)
    for i in listdir:
        if len(result) > 0:
            return
        path_join = os.path.join(path, i)
        if os.path.isdir(path_join):
            if path.endswith('.') and i in ["build", "debug", "release"]:
                continue
            if path.endswith('src') and i in ["androidTest", "test"]:
                continue
            find_file_impl(reg, result, skip_name, path_join)
        elif os.path.isfile(path_join):
            if path.endswith('.') and i not in ["pro"]:
                continue
            is_skip = False
            for j in skip_name:
                if j in i:
                    is_skip = True
                    break
            if is_skip:
                continue
            with open(path_join, 'r', encoding='utf-8') as f:
                if not f.readable():
                    continue
                try:
                    text = f.read()
                except ValueError:
                    continue
                findall = re.findall(reg, text)
                if len(findall) > 0:
                    result.append(path_join)


def find_file(reg, path='.', skip_name=None):
    if skip_name is None:
        skip_name = []
    result = []
    if os.path.isdir(path):
        find_file_impl(reg, result, skip_name, path)
    else:
        with open(path, 'r', encoding='utf-8') as f:
            if f.readable():
                try:
                    text = f.read()
                    findall = re.findall(reg, text)
                    if len(findall) > 0:
                        result.append(path)
                except ValueError:
                    pass
    return result[0] if len(result) > 0 else ""


def get_class_path(path):
    with open(path, 'r') as f:
        text = f.read()
        package = re.search('package (.*?)\\s', text).group(1)
        name = os.path.basename(path)
        name = name[:name.rfind('.')]
        return package + '.' + name


def replace_map_text(mapping, path='.'):
    listdir = os.listdir(path)
    for i in listdir:
        path_join = os.path.join(path, i)
        if os.path.isdir(path_join):
            if path.endswith('.') and i in ["build", "debug", "release"]:
                continue
            if path.endswith('src') and i in ["androidTest", "test"]:
                continue
            replace_map_text(mapping, path_join)
        elif os.path.isfile(path_join):
            if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
                continue
            with open(path_join, 'r', encoding='utf-8') as f:
                if not f.readable():
                    continue
                try:
                    text = f.read()
                except ValueError:
                    continue
            with open(path_join, 'w', encoding='utf-8') as f:
                reverse = sorted(list(mapping.keys()), key=len, reverse=True)
                for name in reverse:
                    text = text.replace(str(name), mapping[name])
                f.write(text)


def sub_map_text(mapping, is_reverse=True, path='.', skip_type=None, skip_name=None):
    if len(mapping) == 0:
        return []
    if skip_type is None:
        skip_type = []
    if skip_name is None:
        skip_name = []
    result = []
    sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse, path)
    return result


def sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse=True, path='.'):
    listdir = os.listdir(path)
    for i in listdir:
        path_join = os.path.join(path, i)
        if os.path.isdir(path_join):
            if path.endswith('.') and i in ["build", "debug", "release"]:
                continue
            if path.endswith('src') and i in ["androidTest", "test"]:
                continue
            sub_map_text_impl(mapping, result, skip_type, skip_name, is_reverse, path_join)
        elif os.path.isfile(path_join):
            if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
                continue
            if os.path.splitext(i)[-1][1:] in skip_type:
                continue
            is_skip = False
            for j in skip_name:
                if j in i:
                    is_skip = True
                    break
            if is_skip:
                continue
            with open(path_join, 'r', encoding='utf-8') as f:
                if not f.readable():
                    continue
                try:
                    text = f.read()
                except ValueError:
                    continue
            with open(path_join, 'w', encoding='utf-8') as f:
                reverse = sorted(list(mapping.keys()), key=len, reverse=is_reverse)
                text_result = text
                for name in reverse:
                    text_result = re2.sub(str(name), mapping[name], text_result)
                if text_result != text:
                    result.append(path_join)
                f.write(text_result)


def replace_text(old, new, path='.'):
    listdir = os.listdir(path)
    for i in listdir:
        path_join = os.path.join(path, i)
        if os.path.isdir(path_join):
            if path.endswith('.') and i in ["build", "debug", "release"]:
                continue
            if path.endswith('src') and i in ["androidTest", "test"]:
                continue
            replace_text(old, new, path_join)
        elif os.path.isfile(path_join):
            if path.endswith('.') and os.path.splitext(i)[-1][1:] not in ["pro"]:
                continue
            with open(path_join, 'r', encoding='utf-8') as f:
                if not f.readable():
                    continue
                try:
                    text = f.read()
                except ValueError:
                    continue
            with open(path_join, 'w', encoding='utf-8') as f:
                f.write(text.replace(old, new))


def get_random_package():
    packages = [get_random_string(4) for _ in range(get_random_int(1, 3))]
    return '.'.join(packages)


def check_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    old_mapping = loads.get('dir', {})

    packages = find_text('package (.*?)[;]?\\s', 'src')
    packages.append(applicationId)
    packages = sorted(set(packages), key=packages.index)
    packages.sort(key=len)

    mapping = {}
    for i in packages:
        if applicationId not in i:
            continue
        value = old_mapping.get(i)
        if value is not None:
            mapping[i] = value
            continue
        mapping[i] = get_random_package()
        value = mapping.get(i[:i.rfind('.')])
        if value is not None:
            mapping[i] = value + '.' + mapping[i]
    loads["dir"] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def check_class_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    old_mapping = loads.get('class', {})

    mapping = {}
    classes = get_classes()
    for i in classes:
        if '_D' in i:
            continue
        name = os.path.basename(i)
        name = name[:name.rfind('.')]
        value = old_mapping.get(name)
        if value is not None:
            mapping[name] = value
            continue
        mapping[name] = get_title(get_random_string(4), True)
    loads["class"] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def check_type_ids_mapping(ids_type):
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    old_mapping = loads.get(ids_type + "_ids", {})

    mapping = {}
    type_ids = find_text('<' + ids_type + ' name="(.*?)".*?>', get_path('.\\src\\main\\res'))
    for i in type_ids:
        if i.endswith('_D'):
            continue
        value = old_mapping.get(i)
        if value is not None:
            mapping[i] = value
            continue
        mapping[i] = get_random_string(5, False)
    type_ids = find_text('<item name="(.*?)".*? type="' + ids_type + '">', get_path('.\\src\\main\\res'))
    for i in type_ids:
        value = old_mapping.get(i)
        if value is not None:
            mapping[i] = value
            continue
        mapping[i] = get_random_string(5, False)
    if len(mapping) != 0:
        loads[ids_type + "_ids"] = mapping
        json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def check_styleable_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    old_mapping = loads.get("styleable", {})

    mapping = {}
    styleables = find_text('<declare-styleable name=".*?">[\\s\\S]*?</declare-styleable>')
    for i in styleables:
        styleable_id = re.findall('<declare-styleable name="(.*?)">', i)[0]
        mapping[styleable_id] = old_mapping.get(styleable_id, get_random_string(5, False))
        styleables_attr = re.findall('<attr name="(.*?)".*?/>', i)
        for attr in styleables_attr:
            mapping[attr + '_A'] = old_mapping.get(attr + '_A', get_random_string(5))
    if len(mapping) != 0:
        loads["styleable"] = mapping
        json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def deal_ids_type(ids_type):
    type_ids_mapping = check_type_ids_mapping(ids_type)
    if len(type_ids_mapping) == 0:
        return
    print(type_ids_mapping)

    sub_map = {}
    for i in type_ids_mapping:
        sub_map['<' + ids_type + ' name="' + i + '"'] = '<' + ids_type + ' name="' + type_ids_mapping[i] + '"'
        sub_map['<item name="' + i + '"(.*? type="' + ids_type + '")>'] = \
            '<item name="' + type_ids_mapping[i] + '"\\g<1>>'
        sub_map['(?<=[^\\.])R\\.' + ids_type + '\\.' + i + '(?=\\W)'] = 'R.' + ids_type + '.' + type_ids_mapping[i]
        sub_map['(?<=>|")@' + ids_type + '/' + i + '(?=<|")'] = '@' + ids_type + '/' + type_ids_mapping[i]
        sub_map['<' + ids_type + '(.*?) parent="' + i + '"(.*?)>'] = \
            '<' + ids_type + '\\g<1> parent="' + type_ids_mapping[i] + '"\\g<2>>'
    sub_map_text(sub_map)


def check_strings_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    mapping = loads.get('strings', {})

    string_values = find_text('<string name="(.*?)".*?>(.*?)</string>', get_path('.\\src\\main\\res\\values'))
    strings = []
    for i in string_values:
        if i[0].endswith('___') or i[0] in ['app_name', 'facebook_app_id']:
            continue
        if mapping.get(i[1]) is not None:
            continue
        strings.append(i[1])

    for i in strings:
        mapping[i] = encrypt(i)

    loads["strings"] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def check_view_ids_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    mapping = loads.get('view_ids', {})

    view_ids = find_text('"\\@\\+id/(.*?)"', get_path('.\\src\\main\\res'))
    for i in view_ids:
        if i == 'root':
            continue
        key = get_title(i)
        if mapping.get(key) is not None:
            continue
        mapping[key] = get_random_string(7)
    loads["view_ids"] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def check_layout_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    mapping = loads.get('layout', {})

    layout_path = get_path('.\\src\\main\\res\\layout')
    listdir = os.listdir(layout_path)
    for i in listdir:
        name = i[:i.rfind('.')]
        if mapping.get(name) is not None:
            continue
        mapping[name] = get_random_string(6)
    loads["layout"] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def check_res_mapping(res_type):
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    mapping = loads.get(res_type, {})

    res_path = get_path('.\\src\\main\\res')
    listdir = os.listdir(res_path)
    for i in listdir:
        if not i.startswith(res_type):
            continue
        for j in os.listdir(os.path.join(res_path, i)):
            if j.startswith('book_'):
                continue
            if j.endswith('.9.png'):
                name = j[:-len('.9.png')]
            else:
                name = j.rsplit('.', 1)[0]
            if mapping.get(name) is not None:
                continue
            mapping[name] = get_random_string(8)
    if len(mapping) != 0:
        loads[res_type] = mapping
        json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def get_random_string(length=8, is_ascii=True):
    s = ''
    for i in range(length):
        if is_ascii:
            s += chr(random.randint(0, 25) + ord('a'))
        else:
            s += chr(random.randrange(0x0780, 0x07A5 + 1))
    return s


def get_random_int(a=0, b=10000):
    return random.randint(a, b)


def get_dictionary_string(length=10):
    random_str = random.choice(string.ascii_letters)
    random_str += ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length - 1))
    return random_str


def check_obfuscation_dictionary():
    if not os.path.exists('dictionary.txt'):
        dictionary = set()
        while len(dictionary) != 5000:
            dictionary.add(get_dictionary_string())
        with open('dictionary.txt', 'w', encoding='utf-8') as f:
            f.write('\n'.join(dictionary))
    with open('proguard-rules.pro', 'r', encoding='utf-8') as f:
        text = f.read()
    if '-obfuscationdictionary' in text:
        return
    text += '\n-obfuscationdictionary dictionary.txt'
    text += '\n-classobfuscationdictionary dictionary.txt'
    text += '\n-packageobfuscationdictionary dictionary.txt'
    with open('proguard-rules.pro', 'w', encoding='utf-8') as f:
        f.write(text)


def mkdir(dir_path):
    try:
        os.makedirs(dir_path)
    except OSError:
        pass


def replace_click_method():
    try:
        path = get_path('.\\src\\main\\java')
        file = find_file('fun View.setTrackedOnClickListener', path)
        if file:
            class_path = get_class_path(file) + '.setTrackedOnClickListener'
            # java类不能直接使用扩展函数
            sub_map_text({
                '([.@])setOnClickListener': '\\g<1>setTrackedOnClickListener',
            }, path=path, skip_type=['java'])
            add_import(class_path, get_classes(['kt']))
    except IndexError:
        return


def deal_code():
    # 替换点击事件
    replace_click_method()

    # 生成包映射文件
    mapping = check_mapping()
    packages = list(mapping.keys())
    print(mapping)

    # 移动文件
    root_path = get_path('.\\src\\main\\java')
    for key in packages:
        key = str(key)

        old_path = get_path(root_path + '\\' + key.replace('.', '\\'))
        new_path = get_path(root_path + '\\' + mapping[key].replace('.', '\\'))

        if not os.path.exists(old_path):
            mkdir(old_path)

        if not os.path.exists(new_path):
            mkdir(new_path)

        listdir = os.listdir(old_path)
        for i in listdir:
            path_join = os.path.join(old_path, i)
            if os.path.isdir(path_join):
                continue
            shutil.move(path_join, os.path.join(new_path, i))
    shutil.rmtree(get_path(root_path + '\\' + applicationId[:applicationId.find('.')]))

    # 修改manifest
    with open(get_path('.\\src\\main\\AndroidManifest.xml'), 'r', encoding='utf-8') as f:
        text = f.read()

    findall = re.findall('android:name=(".*?")', text)
    for i in findall:
        if i.startswith('".'):
            text = text.replace(i, '"' + applicationId + i[1:-1] + '"')

    with open(get_path('.\\src\\main\\AndroidManifest.xml'), 'w', encoding='utf-8') as f:
        f.write(text)

    # 修改依赖
    sub_map = {}
    for i in mapping:
        sub_map['(?<=\\W)' + i + '(?=\\W)'] = mapping[i]

    # 过滤修改
    sub_map['(?<=\\W)' + mapping[applicationId] + '.R(?=\\W)'] = applicationId + '.R'
    sub_map['(?<=\\W)' + mapping[applicationId] + '.databinding(?=\\W)'] = applicationId + '.databinding'
    sub_map['(?<=\\W)' + mapping[applicationId] + '.BuildConfig(?=\\W)'] = applicationId + '.BuildConfig'
    sub_map_text(sub_map)

    # 根包名下 R 单独处理
    new_path = get_path(root_path + '\\' + mapping[packages[0]].replace('.', '\\'))
    listdir = os.listdir(new_path)
    result_path = []
    for i in listdir:
        if os.path.isdir(os.path.join(new_path, i)):
            continue
        result_path.append(os.path.join(new_path, i))
    add_import(applicationId + '.R', result_path)
    add_import(applicationId + '.BuildConfig', result_path)

    # 类名
    class_mapping = check_class_mapping()
    classes = get_classes()
    print(class_mapping)
    for i in classes:
        if '_D' in i:
            continue
        name = os.path.basename(i)
        name = name[:name.rfind('.')]
        shutil.move(i, i.replace(name, class_mapping[name]))
    sub_map = {}
    for i in class_mapping:
        sub_map['(?<=\\W)' + i + '(?=\\W)'] = class_mapping[i]
    sub_map_text(sub_map)

    # 混淆字典
    check_obfuscation_dictionary()


def deal_res_type(res_type):
    type_mapping = check_res_mapping(res_type)
    if len(type_mapping) == 0:
        return
    print(type_mapping)

    res_path = get_path('.\\src\\main\\res')
    listdir = os.listdir(res_path)
    for i in listdir:
        if not i.startswith(res_type):
            continue
        path_join = os.path.join(res_path, i)
        for j in os.listdir(path_join):
            if j.startswith('book_'):
                continue
            if j.endswith('.9.png'):
                name = j[:-len('.9.png')]
            else:
                name = j.rsplit('.', 1)[0]
            path = os.path.join(path_join, j)
            if '.DS_Store' not in path:
                shutil.move(path, path.replace(name, type_mapping[name]))

    sub_map = {}
    for i in type_mapping:
        sub_map['R\\.' + res_type + '\\.' + i + '(\\W)'] = 'R.' + res_type + '.' + type_mapping[i] + '\\g<1>'
        sub_map['(>|")@' + res_type + '/' + i + '(<|")'] = '\\g<1>@' + res_type + '/' + type_mapping[i] + '\\g<2>'
    sub_map_text(sub_map)


def add_image_noise(path):
    try:
        image = cv2.imread(path, cv2.IMREAD_UNCHANGED)
        if path.endswith('.9.png'):
            return
        # 添加随机噪声
        noise = np.random.randint(0, 2, image.shape, np.uint8)
        image = cv2.add(image, noise)
        new_path = path.rsplit('.', 1)[0] + '.webp'
        cv2.imwrite(new_path, image, [cv2.IMWRITE_WEBP_QUALITY, 75])
        if new_path != path:
            os.remove(path)
    except AttributeError:
        return


def deal_image():
    res_path = get_path('.\\src\\main\\res')
    listdir = os.listdir(res_path)
    for i in listdir:
        path_join = os.path.join(res_path, i)
        if os.path.isdir(path_join):
            for j in os.listdir(path_join):
                add_image_noise(os.path.join(path_join, j))


def get_title(string, is_all_upper=False):
    splits = string.split('_')
    s = ''
    for i in range(len(splits)):
        if i == 0 and not is_all_upper:
            s = splits[i]
        elif len(splits[i]) > 0:
            s += splits[i][0].upper() + splits[i][1:]
    return s


def add_import(path, listfile):
    for i in listfile:
        with open(i, 'r', encoding='utf-8') as f:
            text = f.read()
            if len(re.findall('import ' + path, text)) != 0:
                continue
            if i.endswith('kt'):
                text = re.sub('(package \\S+?\\s+?)(?=\\S)', '\\g<1>import ' + path + '\n', text, flags=re.S)
            elif i.endswith('java'):
                text = re.sub('(package \\S+?\\s+?)(?=\\S)', '\\g<1>import ' + path + ';\n', text, flags=re.S)
        with open(i, 'w', encoding='utf-8') as f:
            f.write(text)


def check_class_string_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    mapping = loads.get('class_string', {})

    classes = get_classes(['kt'])
    for path in classes:
        if os.path.basename(path).startswith('AESHelper'):
            continue
        with open(path, 'r', encoding='utf-8') as f:
            text = f.read()
        text = re.sub('@SuppressLint.*?\n', '\n', text)
        text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
        text = re.sub(' +\n', '\n', text)
        text = re.sub('=\\s+', '= ', text)
        text = re.sub(':\\s+', ': ', text)
        text = re.sub(',\\s+', ', ', text)
        text = re.sub('\n{2,}', '\n\n', text)
        result_text = text
        strings = re.findall('".*?[^\\\\]"', re.sub(r'@[^)]*', lambda x: x.group().replace('"', "'"), result_text))
        last_string = ''
        for string in strings:
            if len(string) <= 4 or string.isspace() or \
                    string[1:-1] in ['\\\\n', '\\\\r', '\\\\\'', '\\\\\\"', '\\\\?', '&amp;', 'UTF-8']:
                continue
            if string in last_string:
                continue
            start = text.find(string)
            if start == -1:
                continue
            index = start + 1
            sign_stack = ['"']
            while len(sign_stack) != 0:
                if text[index] == '\\':
                    index += 1
                elif text[index] == '"' and sign_stack[-1] == '"':
                    sign_stack.pop()
                elif text[index] == '"':
                    sign_stack.append('"')
                elif text[index] == '}' and sign_stack[-1] == '${':
                    sign_stack.pop()
                elif text[index - 1:index + 1] == '${':
                    sign_stack.append('${')
                index += 1
            string = text[start:index]
            last_string = string
            params = []
            result = string
            if '$' in string:
                count = 0
                last = -1
                res = []
                for i in range(len(string)):
                    if last == -1 and string[i] == '$':
                        if count == 0:
                            last = i
                    elif last != -1:
                        if string[i] == '{':
                            count += 1
                        elif string[i] == '}':
                            count -= 1
                            if count == 0:
                                res.append((last, i + 1))
                                last = -1
                        elif count == 0:
                            last = -1
                for i in re.findall('\\$\\w*', string):
                    if i == '$':
                        continue
                    find = string.find(i)
                    res.append((find, find + len(i)))
                res = sorted(res, key=lambda l: l[0])
                last_res = None
                for i in res:
                    if last_res is not None and i[0] < last_res[1]:
                        continue
                    params.append(string[i[0]:i[1]])
                    last_res = i
                for i in params:
                    result = result.replace(i, "[str]")
            result = result[1:-1].replace('%', '%%').replace('\\\\', '\\').replace('[str]', '%s')
            if result == aesKey:
                continue
            if mapping.get(result) is not None:
                continue
            mapping[result] = get_random_string(5, False) + '_D'
    loads['class_string'] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def deal_class_string():
    # 获取映射
    string_mapping = check_class_string_mapping()
    print(string_mapping)

    with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'r', encoding='utf-8') as f:
        text = f.read()
    rfind = text.rfind('\n')
    text_result = text[:rfind]
    for i in string_mapping:
        string = i
        string = string.replace('&', '&amp;')
        string = string.replace('?', '\\?')
        string = string.replace('\'', '\\\'')
        text_result += '\n    <string name="' + string_mapping[i] + '" translatable="false">' + string + '</string>'
    text_result += text[rfind:]
    with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'w', encoding='utf-8') as f:
        f.write(text_result)

    # 搜索 Int.string() 路径
    file_path = find_file('fun Int.string', get_path('.\\src\\main\\java'))
    class_path = get_class_path(file_path) + '.string' if file_path else ""
    classes = get_classes(['kt'])
    for path in classes:
        # 跳过 AESHelper 类中的字符串，避免嵌套
        if os.path.basename(path).startswith('AESHelper'):
            continue
        with open(path, 'r', encoding='utf-8') as f:
            text = f.read()
        text = re.sub('@SuppressLint.*?\n', '\n', text)
        text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
        text = re.sub(' +\n', '\n', text)
        text = re.sub('=\\s+', '= ', text)
        text = re.sub(':\\s+', ': ', text)
        text = re.sub(',\\s+', ', ', text)
        text = re.sub('\n{2,}', '\n\n', text)
        result_text = text
        strings = re.findall('".*?[^\\\\]"', re.sub(r'@[^)]*', lambda x: x.group().replace('"', "'"), result_text))
        last_string = ''
        for string in strings:
            if len(string) <= 4 or string.isspace() or \
                    string[1:-1] in ['\\\\n', '\\\\r', '\\\\\'', '\\\\\\"', '\\\\?', '&amp;', 'UTF-8']:
                continue
            if string in last_string:
                continue
            start = text.find(string)
            if start == -1:
                continue
            index = start + 1
            sign_stack = ['"']
            while len(sign_stack) != 0:
                if text[index] == '\\':
                    index += 1
                elif text[index] == '"' and sign_stack[-1] == '"':
                    sign_stack.pop()
                elif text[index] == '"':
                    sign_stack.append('"')
                elif text[index] == '}' and sign_stack[-1] == '${':
                    sign_stack.pop()
                elif text[index - 1:index + 1] == '${':
                    sign_stack.append('${')
                index += 1
            # 得到完整字符串
            string = text[start:index]
            last_string = string
            # 参数
            params = []
            result = string
            # 处理字符串拼接
            if '$' in string:
                count = 0
                last = -1
                res = []
                for i in range(len(string)):
                    if last == -1 and string[i] == '$':
                        if count == 0:
                            last = i
                    elif last != -1:
                        if string[i] == '{':
                            count += 1
                        elif string[i] == '}':
                            count -= 1
                            if count == 0:
                                res.append((last, i + 1))
                                last = -1
                        elif count == 0:
                            last = -1
                for i in re.findall('\\$\\w*', string):
                    if i == '$':
                        continue
                    find = string.find(i)
                    res.append((find, find + len(i)))
                res = sorted(res, key=lambda l: l[0])
                last_res = None
                for i in res:
                    if last_res is not None and i[0] < last_res[1]:
                        continue
                    params.append(string[i[0]:i[1]])
                    last_res = i
                for i in params:
                    result = result.replace(i, "[str]")
                for i in range(len(params)):
                    if params[i][1] == '{':
                        params[i] = params[i][2:-1]
                    else:
                        params[i] = params[i][1:]
                    # 每个参数加上 toString
                    params[i] = '(' + params[i] + ').toString()'
            # 拼接部分替换为 %s
            result = result[1:-1].replace('%', '%%').replace('\\\\', '\\').replace('[str]', '%s')
            # aesKey 密钥不加密
            if result == aesKey:
                continue
            result_text = result_text.replace(text[start:index], 'R.string.' + string_mapping[result] +
                                              '.string(' + ', '.join(params) + ')')

        # 添加 Int.string() 方法引用
        if class_path:
            if len(re.findall('import ' + class_path, text)) == 0:
                result_text = re.sub('(package \\S+?\\s+?)(?=\\S)',
                                     '\\g<1>import ' + class_path + '\n', result_text, flags=re.S)

        # 添加资源 R 引用
        if len(re.findall('import ' + applicationId + '.R', text)) == 0:
            result_text = re.sub('(package \\S+?\\s+?)(?=\\S)',
                                 '\\g<1>import ' + applicationId + '.R' + '\n', result_text, flags=re.S)

        # 去除 const
        result_text = re.sub('const (val.*?R\\.string\\.\\S+?\\.string\\(\\))', '\\g<1>', result_text)
        with open(path, 'w', encoding='utf-8') as f:
            f.write(result_text)


def check_xml_string_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    old_mapping = loads.get('xml_string', {})
    strings = find_text('android:text="(?<!@string/)([^@"]+?)"', get_path('.\\src\\main\\res\\layout'),
                        skip_name=['notify', 'notity'])

    mapping = {}
    for i in strings:
        value = old_mapping.get(i)
        if value is not None:
            mapping[i] = value
            continue
        # 添加 '_D' 结尾，资源混淆时过滤不用再次改名
        mapping[i] = get_random_string(5, False) + '_D'
    loads['xml_string'] = mapping
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return mapping


def deal_xml_string():
    # 获取映射
    string_mapping = check_xml_string_mapping()
    print(string_mapping)

    with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'r', encoding='utf-8') as f:
        text = f.read()
    rfind = text.rfind('\n')
    text_result = text[:rfind]
    for i in string_mapping:
        string = i
        string = string.replace('&', '&amp;')
        string = string.replace('?', '\\?')
        string = string.replace('\'', '\\\'')
        text_result += '\n    <string name="' + string_mapping[i] + '" translatable="false">' + string + '</string>'
    text_result += text[rfind:]
    with open(get_path('.\\src\\main\\res\\values\\strings.xml'), 'w', encoding='utf-8') as f:
        f.write(text_result)

    sub_map = {}
    for i in string_mapping:
        sub_map['(?<=android:text=")' + re.escape(i) + '(?=")'] = '@string/' + re.escape(string_mapping[i])
    # 替换时跳过通知布局
    sub_map_text(sub_map, path=get_path('.\\src\\main\\res\\layout'), skip_name=['notify', 'notity'])


def deal_code_string():
    # 处理布局文件中的明文字符串
    deal_xml_string()
    # 处理代码文件中的明文字符串
    deal_class_string()


def deal_res():
    # 添加字符串为 string 资源
    deal_code_string()

    # 替换 TextView 为 AESTextView
    aes_text_view = get_class_path(find_file('class AESTextView', get_path('.\\src\\main\\java')))
    sub_map = {
        '(?<=<)TextView': aes_text_view,
        '(?<=<)com.noober.background.view.BLTextView': aes_text_view
    }
    sub_map_text(sub_map, path=get_path('.\\src\\main\\res\\layout'), skip_name=['notify', 'notity'])

    # 加密 string
    strings_mapping = check_strings_mapping()
    print(strings_mapping)
    sub_map = {}
    for i in strings_mapping:
        sub_map['<string(.*?)>' + re.escape(i) + '</string>'] = \
            '<string\\g<1>>' + re.escape(strings_mapping[i]) + '</string>'
    sub_map_text(sub_map, path=get_path('.\\src\\main\\res\\values'))

    # 改 string id
    deal_ids_type('string')

    # 改 color id
    deal_ids_type('color')

    # 改 dimen id
    deal_ids_type('dimen')

    # 改 style id
    deal_ids_type('style')

    # 改 declare-styleable
    styleable_mapping = check_styleable_mapping()
    print(styleable_mapping)
    sub_map = {}
    styleables = find_text('<declare-styleable name=".*?">[\\s\\S]*?</declare-styleable>')
    for i in styleables:
        result = i
        styleable_id = re.findall('<declare-styleable name="(.*?)">', i)[0]
        result = result.replace('<declare-styleable name="' + styleable_id + '">',
                                '<declare-styleable name="' + styleable_mapping[styleable_id] + '">')
        sub_map['R\\.styleable\\.' + styleable_id + '(\\W)'] = \
            'R.styleable.' + styleable_mapping[styleable_id] + '\\g<1>'
        styleables_attr = re.findall('<attr name="(.*?)".*?/>', i)
        for attr in styleables_attr:
            result = result.replace('<attr name="' + attr + '"', '<attr name="' + styleable_mapping[attr + '_A'] + '"')
            sub_map['R\\.styleable\\.' + styleable_id + '_' + attr + '(\\W)'] = \
                'R.styleable.' + styleable_mapping[styleable_id] + '_' + styleable_mapping[attr + '_A'] + '\\g<1>'
            sub_map['app:' + attr + '='] = 'app:' + styleable_mapping[attr + '_A'] + '='
        sub_map[re.escape(i)] = result
    sub_map_text(sub_map)

    # 改 view id
    view_ids = find_text('"\\@\\+id/(.*?)"', get_path('.\\src\\main\\res'))
    view_ids_mapping = check_view_ids_mapping()
    print(view_ids_mapping)
    sub_map = {}
    for i in view_ids:
        if i == 'root':
            continue
        key = get_title(i)
        sub_map['(>|")@(\\+)?id/' + i + '(<|")'] = '\\g<1>@\\g<2>id/' + view_ids_mapping[key] + '\\g<3>'
        sub_map['R\\.id\\.' + i + '(\\W)'] = 'R.id.' + view_ids_mapping[key] + '\\g<1>'
        sub_map['([bB]inding\\??(?:\\.\\w+)?\\??)\\.' + key + '(\\W)'] = '\\g<1>.' + view_ids_mapping[key] + '\\g<2>'
        sub_map['(?<=app:constraint_referenced_ids=".*?)' + i + '(?=.*?")'] = view_ids_mapping[key]
    sub_map_text(sub_map)

    # 改 layout 文件名
    layout_mapping = check_layout_mapping()
    print(layout_mapping)

    layout_path = get_path('.\\src\\main\\res\\layout')
    listdir = os.listdir(layout_path)
    for i in listdir:
        name = i[:i.rfind('.')]
        if layout_mapping.get(name) is None:
            continue
        path_join = os.path.join(layout_path, i)
        shutil.move(path_join, path_join.replace(name, layout_mapping[name]))

    sub_map = {}
    for i in layout_mapping:
        sub_map['R\\.layout\\.' + i + '(\\W)'] = 'R.layout.' + layout_mapping[i] + '\\g<1>'
        # ViewBinding 同步修改
        sub_map[get_title(i, True) + 'Binding'] = get_title(layout_mapping[i], True) + 'Binding'
        sub_map['(>|")@layout/' + i + '(<|")'] = '\\g<1>@layout/' + layout_mapping[i] + '\\g<2>'
    sub_map_text(sub_map)

    # 改 drawable 文件名
    deal_res_type('drawable')

    # 改 mipmap 文件名
    deal_res_type('mipmap')

    # 改 raw 文件名
    deal_res_type('raw')

    # 改 raw 文件名
    deal_res_type('xml')

    # 改图片内容
    deal_image()


def check_activity_mapping():
    try:
        loads = json.load(open('mapping.json', 'r', encoding='utf-8'))
    except (ValueError, IOError):
        loads = {}
    old_list = loads.get('activity', [])
    old_paths = []
    for i in old_list:
        old_paths.append(i[:i.rfind('.')])
    old_paths = sorted(set(old_paths), key=old_paths.index)
    old_paths.sort(key=len)

    classes = get_classes()
    paths = []
    root_path = get_path('.\\src\\main\\java')
    for i in classes:
        paths.append(i[len(root_path) + 1:i.rfind(os.sep)].replace(os.sep, '.'))
    paths = sorted(set(paths), key=paths.index)
    paths.sort(key=len)
    new_list = []
    for i in old_list:
        if i[:i.rfind('.')] in paths:
            new_list.append(i)
    for i in paths:
        if i in old_paths:
            continue
        # 每个路径下产生随机个垃圾类
        for _ in range(random.randint(0, 3)):
            name = get_title(get_random_string(4), True) + '_D'
            new_list.append(i + '.' + name)
    loads["activity"] = new_list
    json.dump(loads, open('mapping.json', 'w', encoding='utf-8'), indent=4)
    return new_list


def add_junk_activity():
    root_path = get_path('.\\src\\main\\java')
    new_list = check_activity_mapping()
    count = len(new_list)
    random.shuffle(new_list)
    for i in range(count):
        class_path = get_path(root_path + '\\' + new_list[i].replace('.', '\\') + '.kt')
        # 获取垃圾类代码
        code = get_random_activity_code(new_list[i], new_list[(i + 1) % count])
        with open(class_path, 'w', encoding='utf-8') as f:
            f.write(code)
        print(class_path)
    with open(get_path('.\\src\\main\\AndroidManifest.xml'), 'r', encoding='utf-8') as f:
        text = f.read()
    replace_string = '\n'
    for i in new_list:
        replace_string += '        <activity android:name="' + i + '" />\n'
    text = text.replace('   </application>', replace_string + '''    </application>''')
    with open(get_path('.\\src\\main\\AndroidManifest.xml'), 'w', encoding='utf-8') as f:
        f.write(text)


def get_random_view():
    return random.choice(
        ['TextView', 'Button', 'EditText', 'ImageView', 'FrameLayout', 'LinearLayout', 'CalendarView', 'CheckBox',
         'CheckedTextView', 'DatePicker', 'ExpandableListView', 'GridLayout', 'GridView', 'HorizontalScrollView',
         'ImageButton', 'ListView', 'RelativeLayout', 'ScrollView', 'SearchView', 'SeekBar', 'TableLayout'])


def get_random_activity_code(class1, class2):
    # 代码中，class1 跳转 class2，确保垃圾类被引用到
    package1 = class1[:class1.rfind('.')]
    name1 = class1[len(package1) + 1:]
    package2 = class2[:class2.rfind('.')]
    name2 = class2[len(package2) + 1:]
    view = get_random_view()
    if package1 == package2:
        import_code = ''
    else:
        import_code = '\nimport ' + class2
    code = '''package ''' + package1 + '''

import android.content.Intent
import android.os.Bundle
import android.widget.''' + view + '''
import androidx.appcompat.app.AppCompatActivity''' + import_code + '''

class ''' + name1 + ''' : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(''' + view + '''(this))
        startActivity(Intent(this, ''' + name2 + '''::class.java))
    }
}'''
    return code


def add_junk_method():
    classes = get_classes(['kt'])
    for path in classes:
        with open(path, 'r', encoding='utf-8') as f:
            text = f.read()
        if text.find('data class') != -1:
            continue
        text = re.sub('@SuppressLint.*?\n', '\n', text)
        text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
        text = re.sub(' +\n', '\n', text)
        text = re.sub('=\\s+', '= ', text)
        text = re.sub(':\\s+', ': ', text)
        text = re.sub(',\\s+', ', ', text)
        text = re.sub('/\\*[\\s\\S]*?\\*/', '', text)
        text = re.sub('\n{2,}', '\n\n', text)
        result_text = text
        interfaces = re.findall('interface .*?{.*?}', text, flags=re.S)
        for interface in interfaces:
            start = text.find(interface)
            start = text[:start].rfind('\n') + 1
            if start == -1:
                continue
            index = start
            count = 0
            isStart = False
            while (not isStart or count > 0) and index < len(text):
                if text[index] == '{':
                    count += 1
                    if count == 1:
                        isStart = True
                elif text[index] == '}':
                    count -= 1
                index += 1
            # 过滤接口及其内容
            text = text.replace(text[start:index], '')
        functions = re.findall('fun (?!interface ).*?\\(.*?\\)', text, flags=re.S)
        for function in functions:
            start = text.find(function)
            start = text[:start].rfind('\n') + 1
            if start == -1:
                continue
            index = start
            count = 0
            isStart = False
            while (not isStart or count > 0) and index < len(text):
                if text[index] == '(':
                    count += 1
                    if count == 1:
                        isStart = True
                elif text[index] == ')':
                    count -= 1
                index += 1
            if 'abstract' in text[start:index]:
                continue
            count = 0
            isStart = False
            param_end = index
            while (not isStart or count > 0) and index < len(text):
                if not isStart and text[index] == '=':
                    break
                if text[index] == '{':
                    count += 1
                    if count == 1:
                        isStart = True
                        param_end = index
                elif text[index] == '}':
                    count -= 1
                index += 1
            if not isStart:
                continue
            fun_text = text[start:index]
            header = text[start:param_end]
            param_start = start + header.find('(')
            context = re.search('fun (.*?\\.)?\\w*?', text[start:param_start])[1]
            if context is None:
                context = ''
            param_string = text[param_start:param_end]
            if 'override fun ' in header and '\n    ' + \
                    re.sub('(final )?override fun ', 'super.', header[:header.index(param_string)]) in fun_text:
                methods = []
                for i in range(random.randint(0, 5)):
                    result = add_one_method(fun_text, param_string, header, context)
                    if result is None or len(result) != 3:
                        continue
                    fun_text, method_text1, method_text2 = result
                    methods.insert(0, method_text2)
                    methods.insert(0, method_text1)
                methods.insert(0, fun_text)
                fun_text = '\n\n'.join(methods)
            else:
                methods = []
                for i in range(random.randint(0, 5)):
                    result = add_one_method(fun_text, param_string, header, context)
                    if result is None or len(result) != 2:
                        continue
                    fun_text, method_text = result
                    methods.insert(0, method_text)
                methods.insert(0, fun_text)
                fun_text = '\n\n'.join(methods)
            result_text = result_text.replace(text[start:index], fun_text)
        with open(path, 'w', encoding='utf-8') as f:
            f.write(result_text)


def add_one_method(fun_text, param_string, header, context):
    if 'suspend ' in header:
        suspend = 'suspend '
    else:
        suspend = ''
    body = fun_text[len(header):]
    intent = len(fun_text) - len(fun_text.lstrip())
    param = param_string[1:param_string.rfind(')')]
    param = re.sub('<.*?>', '', param)
    param = re.sub('\\([^(]*?\\)', '()', param)
    param = [i.strip().split(':')[0] for i in param.split(',')]
    for i in range(len(param)):
        if param[i].startswith('vararg'):
            param[i] = '*' + param[i].split(' ')[-1]
        elif param[i].startswith('@'):
            param[i] = param[i].split(' ')[-1]
    if 'override fun ' in header and '\n    ' + \
            re.sub('(final )?override fun ', 'super.', header[:header.index(param_string)]) in fun_text:
        if ':' in param_string[param_string.rfind(')'):]:
            return
        method_split = [i.rstrip() for i in fun_text.split('\n')]
        if method_split[0].rstrip()[-1] != '{' or method_split[-1].lstrip()[0] != '}':
            return
        method_body = method_split[1:-1]
        super_index = -1
        for i in range(len(method_body)):
            if 'super.' in method_body[i]:
                if super_index >= 0:
                    super_index = -1
                    break
                super_index = i
        if super_index < 0:
            return

        method_text1 = method_split.copy()
        method_name1 = get_random_string(is_ascii=False)
        method_text1[0] = method_text1[0].replace(header, ' ' * intent + 'private ' + suspend + 'fun ' +
                                                  context + method_name1 + param_string)
        method_text1[1:-1] = method_body[:super_index]

        method_text2 = method_split.copy()
        method_name2 = get_random_string(is_ascii=False)
        method_text2[0] = method_text2[0].replace(header, ' ' * intent + 'private ' + suspend + 'fun ' +
                                                  context + method_name2 + param_string)
        method_text2[1:-1] = method_body[super_index + 1:]

        method_body[super_index + 1:] = [' ' * (intent + 4) + method_name2 + '(' + ', '.join(param) + ')']
        method_body[:super_index] = [' ' * (intent + 4) + method_name1 + '(' + ', '.join(param) + ')']
        method_split[1:-1] = method_body

        method_text1 = '\n'.join(method_text1)
        method_text2 = '\n'.join(method_text2)
        fun_text = '\n'.join(method_split)
        return fun_text, method_text1, method_text2
    else:
        method_name = get_random_string(is_ascii=False)
        method_text = fun_text
        if ':' in param_string[param_string.rfind(')'):]:
            method_text = method_text.replace(header, ' ' * intent + 'private ' + suspend + 'fun ' + context +
                                              method_name + param_string)
            fun_text = fun_text.replace(body, '{\n' + ' ' * (intent + 4) + 'return ' +
                                        method_name + '(' + ', '.join(param) + ')\n' + ' ' * intent + '}')
        else:
            method_text = method_text.replace(header, ' ' * intent + 'private ' + suspend + 'fun ' + context +
                                              method_name + param_string)
            fun_text = fun_text.replace(body, '{\n' + ' ' * (intent + 4) +
                                        method_name + '(' + ', '.join(param) + ')\n' + ' ' * intent + '}')
        return fun_text, method_text


def add_junk_code():
    classes = get_classes(['kt'])
    for path in classes:
        with open(path, 'r', encoding='utf-8') as f:
            text = f.read()
        if text.find('data class') != -1:
            continue
        text = re.sub('@SuppressLint.*?\n', '\n', text)
        text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
        text = re.sub(' +\n', '\n', text)
        text = re.sub('=\\s+', '= ', text)
        text = re.sub(':\\s+', ': ', text)
        text = re.sub(',\\s+', ', ', text)
        text = re.sub('/\\*[\\s\\S]*?\\*/', '', text)
        text = re.sub('\n{2,}', '\n\n', text)
        functions = re.findall('((?=fun .*?\\([^}]*?\\)|init |val.*?by lazy)[^}]*?\\{)', text)
        result_text = text
        for function in functions:
            start = text.find(function)
            start = text[:start].rfind('\n') + 1
            if start == -1:
                continue
            index = start
            count = 0
            isStart = False
            while (not isStart or count > 0) and index < len(text):
                if text[index] == '(':
                    count += 1
                    if count == 1:
                        isStart = True
                elif text[index] == ')':
                    count -= 1
                if not isStart and text[index] == '{':
                    break
                index += 1
            count = 0
            isStart = False
            while (not isStart or count > 0) and index < len(text):
                if text[index] == '{':
                    count += 1
                    if count == 1:
                        isStart = True
                elif text[index] == '}':
                    count -= 1
                index += 1
            fun_text = text[start:index]
            fun_text = fun_text.split('\n')
            intent_stack = []
            last_intent = 0
            log_indexes = []
            skip_text = [False for _ in fun_text]
            i = 0
            while i < len(fun_text):
                if 'object : ' in fun_text[i] and fun_text[i].rstrip().endswith('{'):
                    intent = len(fun_text[i]) - len(fun_text[i].lstrip())
                    while i < len(fun_text) - 1:
                        i += 1
                        skip_text[i] = True
                        if fun_text[i].lstrip().startswith('}') and \
                                len(fun_text[i]) - len(fun_text[i].lstrip()) == intent:
                            break
                i += 1
            for i in range(len(fun_text)):
                if skip_text[i]:
                    continue
                intent = len(fun_text[i]) - len(fun_text[i].lstrip())
                if intent == 0 or intent % 4 != 0:
                    continue
                if intent > last_intent and i > 0:
                    j = i - 1
                    while len(fun_text[j]) - len(fun_text[j].lstrip()) == 0 and j >= 0:
                        j -= 1
                    if j >= 0:
                        for k in range(int((intent - last_intent) / 4)):
                            intent_stack.append(j)
                elif intent < last_intent:
                    for k in range(int((last_intent - intent) / 4)):
                        intent_stack.pop()
                if len(intent_stack) > 0 and intent >= last_intent \
                        and len(fun_text[i].strip()) != 0 and fun_text[i].lstrip()[0] != ')':
                    last_text = fun_text[intent_stack[-1]]
                    if last_text[-1] != '{':
                        continue
                    if last_text.lstrip()[0] == ')':
                        last_text_intent = len(last_text) - len(last_text.lstrip())
                        j = intent_stack[-1] - 1
                        while j >= 0:
                            temp_text = fun_text[j]
                            if len(temp_text) - len(temp_text.lstrip()) == last_text_intent:
                                break
                            j -= 1
                        if j >= 0:
                            last_text = fun_text[j] + last_text.strip()
                    if 'when' not in last_text and '$' not in last_text and \
                            'object' not in last_text and 'class ' not in last_text and 'interface ' not in last_text:
                        log_indexes.append((i, intent))
                last_intent = intent
            log_indexes.reverse()
            for i in log_indexes:
                if random.random() > 0.35:
                    continue
                junk_code = get_random_junk_code()
                junk_code = junk_code.replace('\n', '\n' + ' ' * i[1])[1:]
                fun_text.insert(i[0], junk_code)
            result_text = result_text.replace(text[start:index], '\n'.join(fun_text))
        with open(path, 'w', encoding='utf-8') as f:
            f.write(result_text)
    add_import('android.util.Log', classes)


def get_random_junk_code():
    junk_code = random.choice(junk_codes)
    count = 1
    while True:
        random_str = get_random_string(is_ascii=False)
        replace = junk_code.replace('[str' + str(count) + ']', random_str)
        if junk_code == replace:
            break
        junk_code = replace
        count += 1
    count = 1
    while True:
        random_int = get_random_int()
        replace = junk_code.replace('[int' + str(count) + ']', str(random_int))
        if junk_code == replace:
            break
        junk_code = replace
        count += 1
    return junk_code


junk_codes = [
    '''
if (System.currentTimeMillis() < [int1]) {
    println(System.currentTimeMillis())
}''',
    '''
Log.d("[str1]", "[str1]")''',
    '''
val [str1] = [int1]..[int1] + 10
println([str1].random())''',
    '''
val [str1]: (Int, Int) -> Int = { [str2], [str3] -> [str2] + [str3] }
println([str1].invoke([int1], [int2]))''',
    '''
class [str1](val [str2]: String, val [str3]: Int)
println([str1]("[str4]", [int1]))''',
    '''
open class [str1] {
    open fun [str2]() {}
}
class [str3] : [str1]() {
    override fun [str2]() {
        println("[str5]")
    }
}
val [str4] = [str3]()
println([str4])''',
    '''
fun String.[str1](): Boolean {
    return this == this.reversed()
}
val [str2] = "[str4]"
val [str3] = [str2].[str1]()
println([str3])''',
    '''
fun [str1]([str2]: Int, [str3]: Int): Int {
    return [str2] + [str3]
}
println([str1]([int1], [int2]))''',
    '''
val [str1] = { [str2]: String -> println([str2]) }
[str1]("[str3]!")''',
    '''
fun [str1]([str2]: String) {
    println("tag, $[str2]!")
}
[str1]("[str3]")''',
    '''
fun [str1]() {
    fun [str2]([str3]: Int) {
        println([str3])
    }
    [str2]([int1])
    [str2]([int2])
    [str2]([int3])
}
[str1]()''',
    '''
class [str1] {
    inner class [str2] {
        fun [str3]() {
            println("[str5]")
        }
    }
}
val [str4] = [str1]().[str2]()
[str4].[str3]()''',
    '''
class [str1] {
    fun [str2]() {
        println("[str3]")
    }
}
[str1]().[str2]()''',
    '''
abstract class [str1] {
    abstract fun [str2]()
}
class [str3] : [str1]() {
    override fun [str2]() {
        println("[str5]")
    }
}
val [str4] = [str3]()
[str4].[str2]()''',
    '''
class [str1]<T>(val [str2]: T)
val [str3] = [str1]([int1])
val [str4] = [str1]("[str5]")
println([str3])
println([str4])''',
    '''
fun <T> [str1]([str2]: T) {
    println([str2])
}
[str1]([int1])
[str1]("[str3]")''',
    '''
class [str1]<T, K>(val [str3]: T, val [str4]: K) {
    init {
        println("[str5]")
    }
}
val [str2] = [str1]("[str6]", [int1])
println([str2])''',
    '''
class [str1]<T>(val [str2]: T, val [str3]: Int) {
    constructor([str4]: T) : this([str4], [int1]) {
        println("[str5]")
    }
}
println([str1]("[str6]"))''',
    '''
val [str1] = [int1]
val [str2] = [int2]
val [str3] = [str1] * [str2] * [str2]
println([str3])''',
    '''
fun [str1]([str3]: Int, [str4]: Int): Int {
    return [str3] + [str4]
}
val [str2] = [str1]([int1], [int2])
println([str2])''',
    '''
fun [str1]([str4]: Int, [str5]: Int): Int {
    fun [str2]([str6]: Int, [str7]: Int): Int {
        return [str6] + [str7]
    }
    return [str2]([str4], [str5])
}
val [str3] = [str1]([int1], [int2])
println([str3])''',
    '''
class [str1] {
    val [str2] by lazy {
        println("[str4]")
        "[str5]!"
    }
}
val [str3] = [str1]()
println([str3].[str2])''',
    '''
class [str1]<T>([str3]: T) {
    init {
        println("[str4]: $[str3]")
    }
}
println([str1]("[str2]"))''',
    '''
class [str1]<T>(val [str3]: T, val [str4]: Int) {
    constructor([str3]: T) : this([str3], [int1]) {
        println("[str4]")
    }
}
println([str1]("[str2]"))''',
    '''
class [str1]<T>(val [str4]: T)
val [str2] = [str1]([int1])
val [str3] = [str1]("[str5]")
println([str2].[str4])
println([str3].[str4])'''
]


def class_disorder():
    classes = get_classes(['kt'])
    for path in classes:
        with open(path, 'r', encoding='utf-8') as f:
            text = f.read()
        if text.find('data class') != -1:
            continue
        text = re.sub('@SuppressLint.*?\n', '\n', text)
        text = re.sub('//[^"]*?(("[^"]*?){2})*?\n', '\n', text)
        text = re.sub(' +\n', '\n', text)
        text = re.sub('=\\s+', '= ', text)
        text = re.sub(':\\s+', ': ', text)
        text = re.sub(',\\s+', ', ', text)
        text = re.sub('/\\*[\\s\\S]*?\\*/', '', text)
        text = re.sub('\n{2,}', '\n\n', text)
        lines = text.split('\n')
        count = 0
        begin = 0
        result = []
        start = -1
        end = 0
        for index in range(len(lines)):
            if len(lines[index].strip()) == 0:
                continue
            if start == -1:
                if lines[index][-1] == '{':
                    start = index
                    count += 1
                continue
            for char in lines[index]:
                if char in ['{', '(']:
                    count += 1
                elif char in ['}', ')']:
                    if count == 1:
                        end = index
                    count -= 1
            if count == 1:
                if lines[index][-1] in ['}', ')'] and begin != 0:
                    if 'get()' in lines[begin] or 'set(value)' in lines[begin]:
                        for i in range(len(result)):
                            if result[i][1] == begin:
                                result[i] = (result[i][0], index + 1)
                                break
                    else:
                        result.append((begin, index + 1))
                    begin = 0
                elif lines[index][-1] not in ['{', '(']:
                    strip = lines[index].lstrip()
                    if strip[0] == '.' or strip.startswith('get() =') or strip.startswith('set(value) = '):
                        for i in range(len(result)):
                            if result[i][1] == index:
                                result[i] = (result[i][0], index + 1)
                                break
                    else:
                        result.append((index, index + 1))
            elif count == 2 and (lines[index][-1] in ['{', '('] or lines[index].endswith('->')) \
                    and lines[begin][-1] not in ['{', '(']:
                begin = index
        result_temp = []
        i = 0
        while i < len(result):
            begin = result[i][0]
            while lines[result[i][0]].lstrip().startswith('@'):
                i += 1
            result_temp.append([begin, result[i][1]])
            i += 1
        result = result_temp
        random.shuffle(result)
        result_sort = result.copy()
        var_weight = {}
        for i in re.findall('va[lr] (\\w+)', text):
            var_weight[i] = len(re.findall('\\W' + re.escape(i) + '\\W', text))
        for var in sorted(var_weight, key=lambda k: var_weight[k]):
            for i in range(len(result)):
                findall = re.findall('va[lr] (' + re.escape(var) + ')', lines[result[i][0]])
                if len(findall) == 0:
                    continue
                result_sort.remove(result[i])
                result_sort.insert(0, result[i])
                break
        result = result_sort
        text = '\n'.join(lines[:start + 1]) + '\n'
        for i in result:
            text += '\n' + '\n'.join(lines[i[0]:i[1]]) + '\n'
        text += '\n'.join(lines[end:])
        with open(path, 'w', encoding='utf-8') as f:
            f.write(text)


def deal_junk():
    # 添加垃圾类
    add_junk_activity()
    # 添加方法嵌套
    add_junk_method()
    # 添加垃圾代码
    add_junk_code()
    # 类成员乱序
    class_disorder()


def get_path(path):
    paths = path.split('\\')
    return str(os.path.join(*paths))


def main():
    if not os.path.exists(get_path('.\\src\\main\\java\\' + applicationId[:applicationId.find('.')])):
        return

    # 资源混淆
    deal_res()
    # 垃圾代码
    deal_junk()
    # 代码处理
    deal_code()


if __name__ == '__main__':
    if os.path.exists('build.gradle'):
        gradle_path = 'build.gradle'
    elif os.path.exists('build.gradle.kts'):
        gradle_path = 'build.gradle.kts'
    else:
        exit('找不到 build.gradle 文件')
    applicationId = re.search('applicationId .*?["\'](.*?)["\']', open(gradle_path, 'r', encoding='utf-8').read())[1]
    aesKey = find_text(' aesKey ?= ?"(.*?)"', get_path('.\\src\\main\\java'))[0]
    print('包名: ' + applicationId, 'aesKey: ' + aesKey)

    if find_text('release ?{\\s*?(?:isM|m)inifyEnabled(?: ?= ?| )true', is_one=True) is None:
        exit('官方混淆未开启')

    if find_text('\\s*?ndk ?{', is_one=True) is None:
        exit('ndk未配置')

    main()
