import os
import fnmatch
import xml.etree.ElementTree as ET
import random
import string
import exception as e
import logging
import fileinput
import base64
import md5
from Crypto.Cipher import DES


def logger(log_info):  # Log
    logging.debug('[' + base_dir().split('/')[-2] + ']:' + log_info)


def shuffle_list(data_list):  # Shuffle a list
    random.shuffle(data_list)


def open_file_input(file_name):  # Open a file for inline editing
    try:
        return fileinput.input(file_name, inplace=1)
    except IOError as ex:
        raise e.LoadFileException(str(ex)+'\nUnable to edit inplace file ' + file_name)


def random_nop_interval():  # Randomize the number of nop(s)
    return random.sample(xrange(3), 1)[0]+1


def get_random_int(min_int, max_int):  # Get a random integer
    return random.randint(min_int, max_int)


def get_random(mixed_type, str_len):  # Get a random string of strLen lenght
    type_random = string.letters  # Mixedcase Letter
    if not mixed_type:
        type_random = string.lowercase  # Lowercase letter
    return ''.join(random.sample(type_random, str_len))


def base_name(file_name):  # Return the filename in a path
    try:
        return os.path.basename(file_name)
    except OSError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to load path for ' + file_name)


def rename(file_name, new_file_name):  # Rename a file
    try:
        os.rename(file_name, new_file_name)
    except OSError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to rename ' + file_name + ' to ' + new_file_name)


def get_valid_block_code():  # Get the block opcode
    return get_text_as_list('codeBlockValidOpCode.txt')


def get_valid_debug_code():  # Get the debug opcode
    return get_text_as_list('debugValidOpCode.txt')


def get_valid_op_code():  # Get the valid opcode list
    return get_text_as_list('nopValidOpCode.txt')


def move_decrypt_method():  # Move the decription routine into the apk class tree
    try:
        method_name = 'nvlEStringManager.smali'
        os.system('cp -R '+ob_dir() + '/' + method_name + ' ' + base_dir() + '/smali/' + method_name)
    except OSError as ex:
        raise e.OpenToolException(str(ex) + '\nUnable to move Decrypytion Method')


def get_android_method_names():  # Return the list of the method of the SDK
    return get_text_as_list('androidLibMethodsTable.txt')


def get_defunct_method():  # Return the defunct method
    return get_text_file('defunctMethod.txt')


def get_text_file(file_name):  # Get a text from a file
    try:
        with open(ob_dir() + '/' + file_name) as file_list:
            return file_list.read()
    except IOError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to load ' + file_name)


def get_text_as_list(file_name):  # Get a text file as line-per-line list
    return get_text_file(file_name).splitlines()

global_dir = ''  # The base directory
obfuscator_dir = ''  # Obfuscator resource directory


def ob_dir():  # Return the base directory
    return obfuscator_dir


def base_dir():  # Return the base directory
    return global_dir


def load_smali_file():  # Load all the smali files
    return load_files('', '*.smali', '')


def load_xml_file():  # Load all the xml files
    return load_files('', '*.xml', '')


def load_resource_file():  # Load all the resource files
    return load_files('/res/', '*', 'values')


def load_res_repository():  # Load the public resource repository
    return load_xml('/res/values/public.xml')


def save_res_repository(xml_file):  # Save the public resource repository
    save_xml('/res/values/public.xml', xml_file)


def load_manifest():  # Load the apk manifest file
    return load_xml('/AndroidManifest.xml')


def save_manifest(xml_file):  # Save the apk manifest file
    save_xml('/AndroidManifest.xml', xml_file)


def load_xml(file_name):  # Load an XML file
    try:
        parser = ET.XMLParser(encoding="utf-8")
        return ET.parse(base_dir() + file_name, parser=parser)
    except IOError as ex:
        if ex.errno == 2:
            raise e.FileNotFound
        else:
            raise e.LoadFileException(str(ex)+'\nUnable to load XML ' + base_dir() + file_name)


def save_xml(file_name, xml_file):  # Save an XML file
    try:
        xml_file.write(base_dir() + file_name)
    except IOError as ex:
        raise e.LoadFileException(str(ex)+'\nUnable to save XML ' + base_dir() + file_name)


def load_files(path_add, pattern, exclude_dir_prefix):  # Load all the files from a directory whic respect a pattern and does not start with a given prefix
    return set(find_files(base_dir() + path_add, pattern, exclude_dir_prefix))


def get_file_name(file_path):  # Extract the last dir of a path
    return file_path.split('/')[-1]


def get_file_info(file_name):  # Extract some file information from a path
    file_base = base_name(file_name)
    res_name = file_base.split('.')[0]
    dir_name = file_name[:-len(file_base)]
    file_ext = file_base[len(res_name):]
    return dir_name, res_name, file_ext


def find_files(top_dir, pattern, exclude_dir_prefix):
    '''Walk trought the directory tree and return all the files that respect the pattern and do not are in the exclude_dir_prefix folder'''
    try:
        for dir_path, dir_names, file_names in os.walk(top_dir):
                for file_name in file_names:
                    if not exclude_dir_prefix or not get_file_name(dir_path).startswith(exclude_dir_prefix):
                        if fnmatch.fnmatch(file_name, pattern):
                            yield os.path.join(dir_path, file_name)
    except (IOError, OSError) as ex:
        raise e.LoadFileException(str(ex)+'\nUnable to load ' + top_dir + ' with pattern ' + pattern + ' excluding ' + exclude_dir_prefix)


def rename_dir(source_dir, dest_dir):  # Rename a directory
    try:
        os.renames(source_dir, dest_dir)
    except OSError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to rename ' + source_dir + ' to ' + dest_dir)


def load_smali_dirs():  # Load all the class directory
    return load_dirs('/smali', '/' + get_main_exec_dir())


def load_dirs(path_add, exclude_dir_prefix):  # Load all the subdirectories from a directory
    return list(reversed(sorted(list(find_dir(base_dir() + path_add, exclude_dir_prefix)), key=len)))


def find_dir(top_dir, exclude_dir_prefix):
    '''Walk trought the directory tree and return all the directory except for the base directory and the exclude_dir_prefix folder'''
    try:
        for dir_path, dir_names, file_names in os.walk(top_dir):
            if not exclude_dir_prefix or (dir_path != top_dir and dir_path != top_dir + exclude_dir_prefix):
                yield dir_path
    except (IOError, OSError) as ex:
        raise e.LoadFileException(str(ex)+'\nUnable to load ' + top_dir + ' excluding ' + exclude_dir_prefix)


def load_all_smali_dirs():  # Load all the class directory
    smali_dir_list = load_smali_dirs()
    smali_dir_list.append(base_dir() + '/smali' + '/' + get_main_exec_dir())
    smali_dir_list.append(base_dir() + '/smali')
    return smali_dir_list


def get_defunct_class():  # Return the defunct class
    return get_text_file('defunctClass.txt')


def write_text_file(file_name, file_data):  # Write a text to a file
    try:
        with open(file_name, 'w') as file_list:
            return file_list.write(file_data)
    except IOError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to write ' + file_name)


def pad_pkcs5(file_data):
    return file_data + (8 - len(file_data) % 8) * chr(8 - len(file_data) % 8)


def get_raw_file(file_name):
    return get_binary_file(file_name)


def write_raw_file(file_name, file_data):
    write_binary_file(file_name, file_data)


def write_binary_file(file_name, file_data):
    try:
        with open(file_name, "wb+") as file_bin:
            file_bin.write(file_data)
    except IOError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to binary write ' + file_name)


def get_binary_file(file_name):
    try:
        with open(file_name, "rb+") as file_bin:
            return file_bin.read()
    except IOError as ex:
        raise e.LoadFileException(str(ex) + '\nUnable to binary load ' + file_name)


def random_res_interval():  # Randomize the number of new resource(s)
    return random.sample(xrange(10), 1)[0]+10


def save_ids_repository(xml_file):  # Save the id resources repository
    save_xml('/res/values/ids.xml', xml_file)


def move_ids_xml():  # Move the id resources index into the value resource dir
    try:
        os.system('cp -R ' + ob_dir() + '/ids.xml ' + base_dir() + '/res/values/ids.xml')
    except OSError as ex:
        raise e.OpenToolException(str(ex) + '\nUnable to move Ids xml file')


def load_res_id_repository():  # Load the ids resource repository
    try:
        return load_xml('/res/values/ids.xml')
    except e.LoadFileException:
        return None
    except e.FileNotFound:
        return None


def load_asset_file():  # Load all the resource files
    return load_files('/assets/', '*', '')


def get_asset_file(file_name):
    return get_binary_file(file_name)


def write_asset_file(file_name, file_data):
    write_binary_file(file_name, file_data)


def load_asset_dirs():  # Load all the class directory
    return load_dirs('/assets', '')


def is_number(s):
    try:
        float(s)
        return True
    except ValueError:
        return False


def load_raw_file():  # Load all the resource files
    return load_files('/res/raw/', '*', '')


main_exec_dir = ''


def get_main_exec_dir():
    return main_exec_dir


def crypt_identifier(id_value):
    return 'a' + md5_data(id_value).hexdigest().lower()[:8]


def crypt_string(string_const):
    return base64.b16encode(crypt_data(string_const, '*StrGhy*', True)).lower()


def crypt_raw(file_data):
    return crypt_data(file_data, '*RawKey*')


def crypt_data(file_data, des_key, is_string=False):
    if is_string:
        file_data = file_data.encode('utf-8')
    return DES.new(des_key, DES.MODE_ECB).encrypt(pad_pkcs5(file_data))


def md5_data(file_data, is_string=False):
    if is_string:
        file_data = file_data.encode('utf-8')
    return md5.new(file_data)


def move_res_manager():
    os.system('mkdir -p ' + base_dir() + '/smali/android/app/')
    os.system('cp -rf ' + ob_dir() + '/android/app/ActivityOb.smali ' + base_dir() + '/smali/android/app/ActivityOb.smali')
    os.system('cp -rf  ' + ob_dir() + '/android/app/ServiceOb.smali ' + base_dir() + '/smali/android/app/ServiceOb.smali')
    os.system('mkdir -p ' + base_dir() + '/smali/android/content/res')
    os.system('cp -rf ' + ob_dir() + '/android/content/ContextWrapperOb.smali ' + base_dir() + '/smali/android/content/ContextWrapperOb.smali')
    os.system('cp -rf ' + ob_dir() + '/android/content/res/Base16.smali ' + base_dir() + '/smali/android/content/res/Base16.smali')
    os.system('cp -rf ' + ob_dir() + '/android/content/res/ResourcesOb.smali ' + base_dir() + '/smali/android/content/res/ResourcesOb.smali')
    os.system('cp -rf ' + ob_dir() + '/android/content/res/StringUnescape.smali ' + base_dir() + '/smali/android/content/res/StringUnescape.smali')
    os.system('cp -rf ' + ob_dir() + '/android/content/res/RawIdList.smali ' + base_dir() + '/smali/android/content/res/RawIdList.smali')
    os.system('cp -rf ' + ob_dir() + '/android/content/res/LibOb.smali ' + base_dir() + '/smali/android/content/res/LibOb.smali')


def get_string_class():
    os.system('mkdir -p ' + base_dir() + '/smali/android/content/res')
    return get_text_file('android/content/res/StringManagerOb.smali')


def get_asset_class():
    os.system('mkdir -p ' + base_dir() + '/smali/android/content/res')
    return get_text_file('android/content/res/AssetManagerOb.smali')


def get_setter_resource_flag():
    return open_file_input(list(load_files('', 'RawIdList.smali', ''))[0])


def load_lib_file():  # Load all the resource files
    return load_files('/lib/', 'lib*.so', '')


def write_lib_file(file_name, file_data):
    write_binary_file(file_name, file_data)