'''
A utility to compositionally analyze test cases generated by KLEE 
Pre-conditions - 
    1. genearate_separate_unit.py should have been run to generate separate test cases for all functions in a C file
    2. run KLEE on separate unit tests to generate test cases
'''

from .analyze_klee_test import find_ptr_errs, find_matching_error
from clang.cindex import Index, CursorKind
import sys, os
import re
import glob
from optparse import OptionParser, OptionGroup
from collections import Counter
import numpy

def get_caller_list(dirname, func_name):
    #dirname = os.path.dirname(tests_dir)
    main_name = os.path.basename(dirname).split('_')[0]
    if not dirname.endswith('/'):
        dirname = dirname + '/'
    if not os.path.exists(dirname+main_name+'_'+func_name+'.c.caller'):
        print(dirname+main_name+'_'+func_name+'.c.caller')
        print('The caller file for the given function does not exist.\nIgnoring and moving on')
        return None
    
    caller_file = open(dirname+main_name+'_'+func_name+'.c.caller')
    caller_list = []
    for line in caller_file:
        caller_list.append(line.strip())

    return caller_list

def get_outlier_funcs(affected_funcs):
    outliers = []
    outlier_names = []

    func_names = [ f[1] for f in affected_funcs ]
    func_count = Counter(func_names)
    occurences = list(func_count.values())
    occurences = numpy.array(occurences)
    mean = numpy.mean(occurences)
    std = numpy.std(occurences)
    outlier_thres = mean + 2*std

    for f in list(func_count.keys()):
        if func_count[f]>=outlier_thres:
            outlier_names.append(f)

    for a in affected_funcs:
        if a[1] in outlier_names:
            outliers.append(a)

    return outliers

def get_container_file(dirname, caller):
    unit_dir_list = glob.glob(dirname+'/*_units/')
    for unit_dir in unit_dir_list:
        unit_files = glob.glob(unit_dir+'*_'+caller+'.c.units')
        if not unit_files==[]:
            return os.path.basename(unit_dir[:-1])[:-6]+'.c'

    print('Could not find container file for that function: ' + caller)
    return None

def check_parent_funcs(c_filename, func_name):
    if not os.path.exists(c_filename):
        print(c_filename)
        print('The given filename does not exist.')
        return [], []

    affected_funcs = []
    unaffected_funcs = []

    main_name = os.path.splitext(os.path.basename(c_filename))[0]
    tests_dir = c_filename[:-2]+'_units'+'/'+main_name+'_'+func_name
    # Sometimes regex screws up the grep of function name, while generating directories
    alt_func_name = func_name.split('_')[-1]
    alt_tests_dir = c_filename[:-2]+'_units'+'/'+main_name+'_'+alt_func_name

    if os.path.isdir(tests_dir):
        pass
    elif os.path.isdir(alt_tests_dir):
        tests_dir = alt_tests_dir
    else:
        # print 'The given function either does not exist, or was not tested by KLEE (no klee output folder found): %s in %s'%(func_name, c_filename)
        return [], []

    ptr_errs = find_ptr_errs(tests_dir)

    for pe in list(ptr_errs.keys()):
        caller_list = get_caller_list(os.path.dirname(tests_dir), pe)
        if caller_list==None:
            return [], []
        for caller in caller_list:
            container = get_container_file(os.path.dirname(c_filename), caller)
            if container==None:
                continue
            klee_out_dir = os.path.dirname(c_filename)+'/'+os.path.splitext(os.path.basename(container))[0]+'_units/'+os.path.splitext(os.path.basename(container))[0]+'_'+caller.split('_')[-1]+'/'
            if not os.path.isdir(klee_out_dir):
                # print 'Could not find klee output directory for the function: %s in directory %s'%(func_name, tests_dir)
                return [], []

            for ptr_err in ptr_errs[pe]:
                if find_matching_error(klee_out_dir, pe, ptr_err[1]):
                    affected_funcs.append((os.path.dirname(c_filename)+'/'+container, caller))
                else:
                    unaffected_funcs.append((os.path.dirname(c_filename)+'/'+container, caller))

    affected_funcs = set(affected_funcs)
    affected_funcs = [i for i in affected_funcs]

    unaffected_funcs = set(unaffected_funcs)
    unaffected_funcs = [i for i in unaffected_funcs]

    return affected_funcs, unaffected_funcs

def get_top_level_funcs(c_filename, func_name):
    unaffected_parents_clean = []
    affected_parents, unaffected_parents = check_parent_funcs(c_filename, func_name)
    affected = [(c_filename, func_name)]
    
    # This works
    while len(affected_parents)>0:
        affected_grandparents = []
        for a in affected_parents:
            rec_result, rec_result_unaffected = check_parent_funcs(a[0], a[1])
            if len(rec_result)>0:
                for r in rec_result:
                    if r not in affected and r not in affected_parents:
                        affected_grandparents.append(r)
            if len(rec_result_unaffected)>0:
                for ur in rec_result_unaffected:
                    if (a[1], ur[0], ur[1]) not in unaffected_parents_clean:
                        unaffected_parents_clean.append((a[1], ur[0], ur[1]))
        affected.extend(affected_parents)
        affected_parents = affected_grandparents

    # This, not so much
    '''
    rec_affected_funcs = [(c_filename, func_name)]

    for i in affected_funcs:
        temp_rec_affected_funcs = check_parent_funcs(i[0], i[1])
        if not temp_rec_affected_funcs == []:
            rec_affected_funcs.extend(temp_rec_affected_funcs)
    '''

    if len(affected)>0:
        sorted_affected = []
        for a in affected:
            if a not in sorted_affected:
                sorted_affected.append(a)
        affected = sorted_affected
    else:
        affected = []

    if len(unaffected_parents_clean)>0:
        sorted_unaffected_parents = []
        for ua in unaffected_parents_clean:
            if ua not in sorted_unaffected_parents:
                sorted_unaffected_parents.append(ua)
        unaffected_parents_clean = sorted_unaffected_parents
    
    return affected, unaffected_parents_clean

if __name__=='__main__':
    parser = OptionParser('Usage: %prog -d {name of the source directory}')
    parser.add_option('-d', '--dir', action='store', type='string', dest='dir', help='Source file directory path')
    parser.add_option('-n', '--n-long', action='store', type='int', dest='n_long', help='Minimum length of error chain to be reported')

    (opts, args) = parser.parse_args()
    
    src_dir = opts.dir
    if opts.n_long:
        n_long = opts.n_long
    else:
        n_long = 2

    if not src_dir.endswith('/'):
        src_dir = src_dir + '/'

    out_file = open(src_dir+'composition.test', 'w+')

    func_names = []
    affected_parent_funcs = []
    unaffected_parent_funcs = []
    for c_file in glob.glob(src_dir + '*.c'):
        re_pattern = src_dir + '(.*).c'
        re_match = re.search(re_pattern, c_file)
        main_name = re_match.group(1)

        for unit_test in glob.glob(src_dir+main_name+'_units/*.c.units'):
            unit_file_name = os.path.splitext(os.path.basename(unit_test))[0][:-2]
            func_name = unit_file_name.split('_', 1)[1]
            temp_affected_parent_funcs, temp_unaffected_parent_funcs = get_top_level_funcs(c_file, func_name)
            if temp_affected_parent_funcs:
                func_names.append(func_name)
                affected_parent_funcs.append(temp_affected_parent_funcs)
            if temp_unaffected_parent_funcs:
                unaffected_parents_funcs.extend(temp_unaffected_parent_funcs)
    
    print(unaffected_parent_funcs)
    for i, func_name in enumerate(func_names):
        out_file.write(func_name+'\n')
        out_file.write(str(affected_parent_funcs[i])+'\n\n')

    out_file.close()

