from . import Config, Logger

def amIdjango(fileCall):
    # Used by core on init for initialaizing Django Functions if needed
    return True if 'core.library.hackingtools' == fileCall else False

if amIdjango(__name__):
    from core.library import hackingtools as ht
    from core.library.hackingtools.core.Objects import Worker as __Worker
else:
    import hackingtools as ht
    from hackingtools.core.Objects import Worker as __Worker
__config__ = Config.getConfig(parentKey='core', key='Utils')
__config_logger__ = Config.getConfig(parentKey='core', key='Logger')
__config_utils__ = Config.getConfig(parentKey='core', key='Utils', subkey='dictionaries')

__function_param_exclude__ = Config.getConfig(parentKey='core', key='import_modules', subkey='__function_param_exclude__')
__default_class_name_for_all__ = Config.getConfig(parentKey='core', key='import_modules', subkey='__default_class_name_for_all__')  

import random as __random
import requests as __requests
import base64 as __base64
import os as __os
import ast
import inspect
import json as __json
import shutil
from threading import Thread
from threading import Lock
from itertools import product
from functools import reduce
from datetime import datetime
import git
import re
from pathlib import Path

global threads
threads = {}

__path = __os.path.abspath(__os.path.split(__os.path.dirname(__file__))[0])

def getWorkers():
    global threads
    return threads

def startWorker(workerName, functionCall, args=(), timesleep=1, loop=True, run_until_ht_stops=False, log=False, timeout=None):
    t = None
    try:
        w = __Worker()
        if log:
            Logger.printMessage('Starting Worker: {w}'.format(w=workerName))
        t = Thread(target=w.run, args=(functionCall, args, int(timesleep), loop, timeout ), daemon=run_until_ht_stops)
        if loop:
            threads[workerName] = [ w, t ]
        t.start()
    except Exception as e:
        Logger.printMessage(str(e), is_error=True)

def killAllWorkers():
    global threads
    for t in threads:
        threads[t][0].terminate()
        threads[t][1].join()

def stopWorker(workerName):
    global threads
    if workerName in threads:
        try:
            threads[workerName][0].terminate()
            threads[workerName][1].join()
        except Exception as e:
            Logger.printMessage(str(e), is_error=True)
        del threads[workerName]

# File Manipulation
def getFileContentInByteArray(filePath):
    """Function that returns a ByteArray with the data from a file
    
    Arguments:
        filePath {String} -- The Path of the file and the file name
    
    Returns:
        byteArray -- The content of the file / None
    """
    try:
        Logger.printMessage(message='{methodName}'.format(methodName='getFileInByteArray'), description='{filePath}'.format(filePath=filePath), debug_core=True)
        the_file = open(filePath, "rb")
        file_data = the_file.read()
        the_file.close()
        return file_data
    except:
        pass
    return None
        
def saveToFile(content, fileName):
    """Saves a new file with a Byte Array and a fileName
    
    Arguments:
        content {byteArray} -- The content we want to write into the file
        fileName {String} -- The file name
    
    Returns:
        boolean -- If saved True, else False
    """
    try:
        Logger.printMessage(message='{methodName}'.format(methodName='saveToFile'), description='{filename}'.format(filename=fileName), debug_core=True)
        the_file = open(fileName, "w")
        the_file.write(content)
        the_file.close()
        return True
    except:
        pass
    return False

def emptyDirectory(directory):
    """
    Cleans a directory given as param
    Be carefull with the 
    
    Arguments:
        dir : str
        
            output_dir variable in modules,
            for example, ht_crypter, it has a
            path automatically created
            for this function
    Returns:
        bool

            Returns if all was OK
    """
    try:
        #Logger.printMessage(message='emptyDirectory', description='Would empty: {path}'.format(path=directory), is_warn=True)
        #if __os.path.isdir(directory):
        #    shutil.rmtree(directory)
        #    return True
        # ! Temporary ommited
        return False
    except:
        return False

def readFileAsString(filepath):
    data = ''
    try:
        f = open(filepath, 'r', encoding='utf8')
        data = f.read()
    except Exception as e:
        pass
    return data

# Data Manipulation
def getValidDictNoEmptyKeys(data):
    final_data = {}
    if isinstance(data, str):
        return data
    for d in data:
        if data[d]:
            if isinstance(data[d], str):
                final_data[d] = data[d]
            else:
                final_data[d] = getValidDictNoEmptyKeys(data[d])
    return final_data

def getFunctionFullCall(moduleName, category, functionName):
    return 'ht.modules.{category}.{modDir}.{module}.{callClass}().{function}'.format(category=category, modDir=moduleName.replace('ht_', ''), module=moduleName, callClass=__default_class_name_for_all__, function=functionName)

def getAnyFunctionParams(functionObjectStr, i_want_list=False):
    try:
        params_func = inspect.getfullargspec(eval(functionObjectStr))[0]
        params_func = [param for param in params_func if not param in __function_param_exclude__] if params_func else []

        args, _, __, defaults = inspect.getargspec(eval(functionObjectStr))
        args = [param for param in args if not param in __function_param_exclude__] if args else []

        if defaults:
            new_params_func = params_func[:-len(defaults)]
            args_defaults = dict(zip(params_func[-len(defaults):], defaults))

        if i_want_list:
            if defaults:
                # urls.py
                return new_params_func + [i for i in args_defaults.keys()]

            return {"params":params_func}

        if defaults:
            return {"params":new_params_func,"defaults": args_defaults}

        return {"params":params_func}
    except:
        pass
        #Logger.printMessage('{functionObjectStr} is not a function'.format(functionObjectStr=functionObjectStr), 'Be sure you have all your module class variables outsite the class, in the file ({functionObjectStr}.py) before the \'class StartModule:\' statement'.format(functionObjectStr='.'.join(functionObjectStr.split('.')[0:5])), is_error=True)
    return []

def getFunctionsParams(category, moduleName, functionName, i_want_list=False):
    function = getFunctionFullCall(moduleName=moduleName, category=category, functionName=functionName)
    return getAnyFunctionParams(function, i_want_list)

def getValueType(value, getResponse=False, returnLiteralType=False):
    try:
        if isinstance(value, bool) or isinstance(eval(value), bool):
            if returnLiteralType:
                return bool
            return 'checkbox'
    except:
        pass
    try:
        if isinstance(value, int) or isinstance(eval(value), int):
            if returnLiteralType:
                return int
            return 'number'
    except:
        pass
    try:
        if isinstance(value, dict) or isinstance(eval(value), dict):
            if returnLiteralType:
                return dict
            return 'data'
    except:
        pass
    try:
        if isinstance(value, datetime) or isinstance(eval(value), datetime) or datetime.strptime(value, '%Y-%m-%d %H:%M:%S') or isinstance(value, datetime):
            if returnLiteralType:
                return datetime
            return 'time'
    except:
        pass
    try:
        if (isinstance(value, list) or isinstance(eval(value), list)) and (str(value)[0] == '[' and str(value)[-1] == ']'):
            if returnLiteralType:
                return list
            return 'select'
    except:
        pass
    if returnLiteralType:
        return str
    try:
        if isinstance(value, str) or isinstance(eval(value), str):
            if returnLiteralType:
                return str
    except:
        pass
    if not getResponse:
        try:
            if isinstance(value, str) or isinstance(eval(value), str):
                if '.' in value and len(value.split('.')[1] in range(1,4)):
                    return 'file'
        except:
            pass
        try:
            if isinstance(value, str) or isinstance(eval(value), str):
                if 'path' in value or 'file' in value:
                    return 'file'
        except:
            pass
        try:
            if isinstance(value, str) or isinstance(eval(value), str):
                if 'pass' in value or 'password' in value:
                    return 'password'
        except:
            pass
    try:
        if isinstance(value, str) or isinstance(eval(value), str):
            if 'http' in value and ('.jpg' in value or '.png' in value or '.ico' in value or '.gif' in value or ('image' in value and '.php' in value)):
                return 'image'
    except:
        pass
    try:
        if isinstance(value, str) or isinstance(eval(value), str):
            if value.startswith('http://') or value.startswith('https://'):
                return 'url'
    except:
        pass
    try:
        if isinstance(value, str) or isinstance(eval(value), str):
            if len(value) > 100:
                return 'textarea'
    except:
        pass
    return 'text'

def doesFunctionContainsExplicitReturn(functionCall):
    try:
        return True if 'return' in inspect.getsource(eval(functionCall)) else False
    except:
        return False

def downloadProjectAsModuleFromGithub(moduleName, category, githubURL):
    try:
        ht.__createModule__(moduleName, category)
        moduleDir = __os.path.join( __path, 'modules', category, moduleName.replace('ht_', '') )
        if __os.path.isdir( moduleDir ):
            # Name for the new module-git
            name_git_folder = githubURL.split('/')[-1].replace('.git', '')
            # Get the dir path
            actual_git_folder = __os.path.join( moduleDir, name_git_folder )
            # Remove if exists
            if __os.path.isdir( actual_git_folder ):
                __os.remove( actual_git_folder )
            # Git clone
            git.Git( moduleDir ).clone( githubURL )
            # Create new name
            new_git_folder = __os.path.join( moduleDir, '_'.join( [ moduleName, 'git' ] ) )
            # Rename the folder
            if __os.path.isdir( new_git_folder ):
                __os.remove( new_git_folder )
                
            __os.rename( actual_git_folder, new_git_folder )
            # Get all files in directory
            entries = __os.listdir(new_git_folder)
            for ent in entries:
                # Check files for remove not interesting ones
                if ent.endswith('.md') or ent.endswith('.jpg') or ent.endswith('.png') or ent.endswith('.txt'):
                    __os.remove( __os.path.join( new_git_folder, ent ) )

            # Search for setup.py if there is
            entries = __os.listdir(new_git_folder)
            for filen in entries:
                file_to_read = __os.path.join( new_git_folder, filen )
                setup_content = readFileAsString( file_to_read )
                commands = []
                find_for = [ '--' ]
                for finding in find_for:
                    if finding in setup_content:
                        for ind in findIndexAllOccurrencesSubstring(finding, str( setup_content )):
                            interesting_line = str(setup_content)[ind:].split('\n')[0]
                            for appearances in interesting_line.split( finding ):
                                comm = ''.join( [ finding, appearances.split('\'')[0] ] ).split(' ')[0]
                                if not comm in commands and not comm == finding:
                                    commands.append( comm )
                if commands:
                    print('Found commands:', commands)
                else:
                    print('Not commands')
        else:
            Logger.printMessage('Not exists', description=moduleDir, is_error=True)
            ht.__removeModule__(moduleName, category)
    except Exception as e:
        raise
        Logger.printMessage(str(e), is_error=True)
        ht.__removeModule__(moduleName, category)

# Others
def getTime():
    return datetime.utcnow().strftime(__config_logger__['log_print_date_format'])[:-3]

def getLocationGPS():
    ip_request = __requests.get('https://get.geojs.io/v1/ip.json')
    geo_request_url = 'https://get.geojs.io/v1/ip/geo/' + ip_request.json()['ip'] + '.json'
    geo_request = __requests.get(geo_request_url)
    geo_data = geo_request.json()
    return geo_data

def getIPLocationGPS(ip, api):
    url = 'http://api.ipinfodb.com/v3/ip-city/?key={api}&ip={ip}'.format(api=api, ip=ip)
    res = __requests.get(url).content.decode()
    if len(res.split(';')) > 8:
        return {'ip' : ip, 'location' : [ res.split(';')[9], res.split(';')[8] ] }
    else:
        return {'ip' : ip, 'location' : [0, 0] }

def getIPLocationGPS_v2(ip):
    try:
        url = 'https://api.ipgeolocationapi.com/geolocate/{ip}'.format(ip=ip)
        res = __json.loads(__requests.get(url).content.decode())
        if res['geo']:
            return {'ip' : ip, 'location' : [ res['geo']['longitude'], res['geo']['latitude'] ], 'country' : res['alpha2'].lower() }
        else:
            return {'ip' : ip, 'location' : [0, 0], 'country' : 'us' }
    except:
        return {'ip' : ip, 'location' : [0, 0], 'country' : 'us' }

# Maths
def euclides(a, b):
    """Euclid's algorithm for determining the greatest common divisor
    Use iteration to make it faster for larger integers
    
    Arguments:
        a {integer} -- Value 1 to use in euclides
        b {integer} -- Value 2 to use in euclides
    
    Returns:
        integer -- Euclides of a and b
    """
    Logger.printMessage(message='{methodName}'.format(methodName='euclides'), description='{a} - {b}'.format(a=a, b=b), debug_core=True)
    while b != 0:
        a, b = b, a % b
    return a

def multiplicativeInverse(e, phi):
    """Euclid's extended algorithm for finding the multiplicative inverse of two numbers
    
    Arguments:
        e {integer} -- Euclides value
        phi {integer} -- Phi value
    
    Returns:
        integer -- The inverse multiplicative of e and phi
    """
    Logger.printMessage(message='{methodName}'.format(methodName='multiplicativeInverse'), description='{e} - {phi}'.format(e=e, phi=phi), debug_core=True)
    # See: http://en.wikipedia.org/wiki/Extended_Euclidean_algorithm
    def eea(a,b):
        if b==0:return (1,0)
        (q,r) = (a//b,a%b)
        (s,t) = eea(b,r)
        return (t, s-(q*t) )

    inv = eea(e,phi)[0]
    if inv < 1: inv += phi #we only want positive values
    return inv

def isPrime(n):
    """Tests to see if a number is prime.
    
    Arguments:
        number {integer} -- The number to test if is a prime number
    
    Returns:
        boolean -- If is prime True, else False
    """
    Logger.printMessage(message='{methodName}'.format(methodName='isPrime'), description='{n}'.format(n=n), debug_core=True)
    excepted = (0, 2, 4, 5, 6, 8)
    if not int(str(n)[-1]) in excepted:
        if (n==1):
            return False
        elif (n==2):
            return True
        else:
            for x in range(2,n):
                if(n % x==0):
                    return False
            return True
    return False

def getRandomPrimeByLength(length = 8):
    """Returns a random prime number with the length you choose
    
    Keyword Arguments:
        length {integer} -- The length of the number we want (default: {8})
    
    Returns:
        integer -- A random prime number with the length you specified / -1
    """
    try:
        length = int(length)
    except:
        length = 8
    try:
        while True:
            n = int(randomText(length=length, alphabet='numeric'))
            if len(str(n)) == length and isPrime(n):
                return n
    except:
        return -1

# Text Treatment
def findIndexAllOccurrencesSubstring(substring, content):
    return [m.start() for m in re.finditer( substring, str(content) )]

def decimalToBinary(n):
    return bin(n).replace("0b","")

def textToAscii(content):
    """Transform text in String to ASCII Array
    
    Arguments:
        content {String} -- The String content we want to transform into ASCII Array
    
    Returns:
        Array -- The content String transformed into an Array in ASCII
    """
    Logger.printMessage(message='{methodName}'.format(methodName='textToAscii'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return [ord(pal) for pal in content]

def asciiToHex(content):
    """Transform ASCII Array to Hex Array
    
    Arguments:
        content {Array} -- An Array with some content in ASCII for transforming into Hex Array
    
    Returns:
        Array -- The content ASCII Array transformed into an Hex Array
    """
    Logger.printMessage(message='{methodName}'.format(methodName='AsciiToHex'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return [hex(n)[2:] for n in content]

def hexToBase64(content):
    """Transform Hex Array to Base64 Array
    
    Arguments:
        content {Array} -- An Array with some content in Hex for transforming into Base64 Array
    
    Returns:
        Array -- The content transformed into an Base64 Array
    """
    Logger.printMessage(message='{methodName}'.format(methodName='HexToBase64'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return [__base64.b64encode(n.encode()) for n in content]

def joinBase64(content):
    """Transform Base64 Array to String
    
    Arguments:
        content {Array} -- An Array with some content in Base64 for transforming into a String
    
    Returns:
        String -- The content Base64 Array joined and transformed from Base64 decoded and encoded to String
    """
    Logger.printMessage(message='{methodName}'.format(methodName='joinBase64'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return (''.join([content[i].decode('utf-8') for i in range(0, len(content))])).encode()

def asciiToBase64(content):
    """Transform ASCII String to Base64 Array
    
    Arguments:
        content {String} -- A String with some content in ASCII for transforming into a Base64 Array
    
    Returns:
        Array -- The content in ASCII String transformed into a Base64 Array
    """
    Logger.printMessage(message='{methodName}'.format(methodName='asciiToBase64'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return [content[i:i+4] for i in range(0, len(content), 4)]

def base64ToHex(content):
    """Transform Base64 Array to Hex Array
    
    Arguments:
        content {Array} -- The content in a Base64 Array to transform into an Hex Array
    
    Returns:
        Array -- The content in Base64 Array transformed into an Hex Array
    """
    Logger.printMessage(message='{methodName}'.format(methodName='base64ToHex'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return [__base64.b64decode(b64) for b64 in content]

def hexToDecimal(content):
    """Transform Hex Array to Decimal Array
    
    Arguments:
        content {Array} -- The content in an Hex Array to transform to a Decimal Array
    
    Returns:
        Array -- The content in an Hex Array transformed into a Decimal Array
    """
    Logger.printMessage(message='{methodName}'.format(methodName='hexToDecimal'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return [int(hexa.decode("UTF-8"), 16) for hexa in content]

def decimalToAscii(content):
    """Transform Decimal Array to ASCII String
    
    Arguments:
        content {Array} -- The content in a Decimal Array to transform to an ASCII String
    
    Returns:
        String -- The content in a Decimal Array transformed to an ASCII String
    """
    Logger.printMessage(message='{methodName}'.format(methodName='decimalToAscii'), description='Length: {length} - {content} ...'.format(length=len(content), content=content[0:10]), debug_core=True)
    return ''.join([chr(dec) for dec in content])

def randomText(length=8, alphabet='lalpha', try_pattern=None, pattern_force_chars=None):
    try:
        if try_pattern:
            alphabets_patter = []
            for char in try_pattern:
                char_alphabet = fromWhatDictListIsChar(char=char)

                if char_alphabet and pattern_force_chars and not char in pattern_force_chars:
                    alphabets_patter.append(char_alphabet)
                else:
                    alphabets_patter.append(char)
                    
            return ''.join([ getRandomCharFromDict(alphabet=alp) if getRandomCharFromDict(alphabet=alp) else alp for alp in alphabets_patter ])

        return ''.join(__random.SystemRandom().choice(__config_utils__[alphabet]) for _ in range(int(length)))

    except Exception as e:
        Logger.printMessage(message=randomText, description=e, is_error=True)

def getRandomCharFromDict(alphabet='lalpha'):
    dictionaryOptions = Config.getConfig(parentKey='core', key='Utils', subkey='dictionaries')
    if alphabet in dictionaryOptions:
        return __random.choice(dictionaryOptions[alphabet])
    return None

def fromWhatDictListIsChar(char='a'):
    dictionaryOptions = Config.getConfig(parentKey='modules', key='ht_bruteforce', subkey='dictionaryOptions')
    for opt in dictionaryOptions:
        if char in __config_utils__[opt]:
            return opt
    return None

def getCombinationPosibilitiesLength(alphabet, length, generator=False):
    if generator:
        for x in product(__config_utils__[alphabet], repeat=int(length)):
            yield ''.join(x)
    else:
        return [ ''.join(x) for x in product(__config_utils__[alphabet], repeat=int(length)) ]

def getCombinationPosibilitiesByPattern(try_pattern=None, generator=False):
    try:
        alphabets_patter = []
        create_pattern = []
        for char in try_pattern:
            char_alphabet = fromWhatDictListIsChar(char=char)
            if char_alphabet:
                if len(alphabets_patter) == 0 or not char_alphabet in alphabets_patter or not alphabets_patter[-1] == char_alphabet:
                    alphabets_patter.append(char_alphabet)
                    create_pattern.append(1)
                else:
                    create_pattern[-1] += 1
            else:
                alphabets_patter.append(char)

        final_combinations = []
        for i_alp, alp in enumerate(alphabets_patter):
            if i_alp == 0:
                for comb in getCombinationPosibilitiesLength(alphabet=alp, length=create_pattern[i_alp], generator=generator):
                    final_combinations.append(comb)
            else:
                temp_final_combinations = final_combinations
                final_combinations = []
                for combb in getCombinationPosibilitiesLength(alphabet=alp, length=create_pattern[i_alp], generator=generator):
                    for comb in [r[0] + r[1] for r in product(temp_final_combinations, combb)]:
                        final_combinations.append(comb)

        if generator:
            for f in final_combinations:
                yield f
        else:
            return final_combinations

    except MemoryError:
        Logger.printMessage(message='Memory Error', description='There are so many combinations for this pattern', is_error=True)
    return []

def getDict(length=8, alphabet='lalpha', try_pattern=None, generator=False):
    if try_pattern and not try_pattern == '':
        if generator:
            for comb in getCombinationPosibilitiesByPattern(try_pattern=try_pattern, generator=generator):
                yield comb
        else:
            return getCombinationPosibilitiesByPattern(try_pattern=try_pattern, generator=generator)
    else:
        if generator:
            for comb in getCombinationPosibilitiesLength(alphabet=alphabet, length=length, generator=generator):
                yield comb
        else:
            return getCombinationPosibilitiesLength(alphabet=alphabet, length=length, generator=generator)

def getPosibleAlphabet():
    return list(__config_utils__.keys())

def get_from_dict(data_dict, map_list, default=None):
    def getitem(source, key):
        try:
            if isinstance(source, list):
                return source[int(key)]
            if isinstance(source, dict) and key not in source.keys():
                return default
            if not source:
                return default
        except IndexError:
            return default

        return source[key]

    if isinstance(map_list, str):
        map_list = map_list.split('.')

    return reduce(getitem, map_list, data_dict)

def groupListByLength(dataList, length):
    return [ dataList[i:i+length] for i in range(0, len(dataList), length) ]