import csv
import os
import sys

lines_per_function = 4

def flatten_list(lst):
    return [x for xl in lst for x in xl]

def check_validity(function_data, i):
    non_empty = [x for x in function_data if len(x) > 0]

    empty = len(non_empty) == 0
    if empty:
        return False, ""
    
    enough = len(non_empty) >= 5
    if not enough:
        print (function_data)
        print (non_empty)
        return False, "Not enough values for excel function\n"\
                    "check rows " + str(lines_per_function * i) + " to " + str(lines_per_function * (i + 1))

    consistent = (non_empty[3].count(',') + 1) == (len(non_empty) - 4)
    if not consistent:
        print (non_empty[3].count(',') + 1)
        print (len(non_empty) - 4)
        print (non_empty[3])
        print (consistent)
        return False, "Not enough values for an excel function\n"\
               "check rows " + str(lines_per_function * i) + " to " + str(lines_per_function * (i + 1) + " Ensure variable names don't contain commas or spaces")

    return True, ""


def generate_errors(errors, num_functions):

    f = open("generated_entry_points/generation_errors.txt", "w+")

    for err in errors:
        f.write(err)
        f.write("\n")

    f.write(str(num_functions) + " entry point functions have been generated")
    f.write("\n")

    f.close()


def find_functions_to_add(function_names, function_categories, logger, extension):
    assert (len(function_names) == len(function_categories))

    name_to_category = dict(zip(function_names, function_categories))

    existing_functions = []

    def check_line():
        with open("cpp_functions/" + category + extension, 'r') as f:
            textlines = f.readlines()
            for textline in textlines:
                for name in function_names:
                    if (name_to_category[name] == category) and (name[3:-1] in textline):
                        existing_functions.append(name)

    for category in set(function_categories):
        # if category doesn't exist yet, definitely don't already have function
        if not os.path.exists("cpp_functions/" + category + extension):
            continue
        check_line()

    existing_functions = set(existing_functions)

    functions_to_add = [x for x in function_names if not x in existing_functions]
    logger.write(str(len(existing_functions)) + " Functions already written, going to write "
                 + str(len(functions_to_add)))
    logger.write("\n")
    logger.write("Adding to " + extension + " file:")
    logger.write("\n")

    for fxn in existing_functions:
        logger.write(fxn[3:-1])
        logger.write("\n")

    return name_to_category, functions_to_add


"""
 @params
 function_names: list of function names (entry point style)
 all_param_types: equal sized list of lists, with each list giving types of params of one function
 all_param_names: equal sized list of lists, with each list giving params of one function
 function_categories: list of function categories which corresponds with function_names

"""
def generate_cpp_functions(function_names, all_param_types, all_param_names, function_categories):
    if not os.path.exists('cpp_functions'):
        os.makedirs('cpp_functions')

    logger = open("cpp_functions/log.txt", "w+")

    def make_function_header(xl_name, param_types, param_names):
        params = zip(param_types, param_names)
        params = ["* ".join(x) for x in params]
        params = ", ".join(params)

        func_name = "void " + xl_name[3:-1] + "("
        func_name = func_name + "outputType* output, "

        func_name_check = "bool " + xl_name[3:-1] + "Check("
        func_name_check = func_name_check + "XLOPER12* exception_output, "

        func_name = func_name + params + ");\n"
        func_name_check = func_name_check + params + ");\n"
        return func_name, func_name_check

    def make_function_cpp(xl_name, param_types, param_names):
        params = zip(param_types, param_names)
        params = ["* ".join(x) for x in params]
        params = ", ".join(params)

        func_name = "void " + category + "::" + xl_name[3:-1] + "("
        func_name = func_name + "outputType* output, "

        func_name_check = "bool " + category + "::" + xl_name[3:-1] + "Check("
        func_name_check = func_name_check + "XLOPER12* exception_output, "

        func_name = func_name + params + ")\n{\n}\n"
        func_name_check = func_name_check + params + ")\n{\n}\n"
        return func_name, func_name_check

    def write_functions(ext):
        # making a new file if first true
        first = not os.path.exists("cpp_functions/" + category + ext)
        with open("cpp_functions/" + category + ext, 'a') as f:
            for name in functions_to_add:
                if name_to_category[name] == category:
                    f.write("\n")
                    if ext == ".h":
                        if first:
                            f.write("#include \"../ExcelDoNotEdit/excel_addin_utils.h\"\n\n")
                            f.write("namespace " + category + "\n{\n")
                            with open("generated_entry_points/generated_entry_points.h", 'a') as f2:
                                f2.write("\n#include \"" + "../cpp_functions/" + category + ".h\"")
                            first = False
                        func_name, func_name_check = make_function_header(name, name_to_types[name], name_to_param_names[name])
                        f.write(func_name)
                        f.write("\n")
                        f.write(func_name_check)
                    if ext == ".cpp":
                        if first:
                            f.write("#include \"" + category + ".h\"\n\n")
                            first = False
                        func_name, func_name_check = make_function_cpp(name, name_to_types[name], name_to_param_names[name])
                        f.write(func_name)
                        f.write("\n")
                        f.write(func_name_check)

    extensions = [".h", ".cpp"]

    name_to_types = dict(zip(function_names, all_param_types))
    name_to_param_names = dict(zip(function_names, all_param_names))

    for extension in extensions:
        # map of names to categories and list of functions which aren't already there
        name_to_category, functions_to_add = find_functions_to_add(function_names, function_categories, logger, extension)

        for category in set(function_categories):
            write_functions(extension)

    logger.close()


def generate_entry_points(generated_functions, namespace):
    if not os.path.exists('generated_entry_points'):
        os.makedirs('generated_entry_points')

    f = open("generated_entry_points/generated_entry_points_" + namespace + ".cpp", "w+")

    f.write("///////////////////////////////////////////////////////")
    f.writelines("\n")
    f.write("// GENERATED FILE - MANUAL EDITS MAY BE OVERWRITTEN")
    f.writelines("\n")
    f.write("///////////////////////////////////////////////////////")
    f.writelines("\n")

    # the include should have everything we need to compile the cpp file
    f.write("#include \"generated_entry_points.h\"")
    f.writelines("\n")

    for fxn in generated_functions:
        f.writelines("\n")
        f.write(fxn)

    f.close()


def generate_function(function_data, i):
    success, msg = check_validity(function_data, i)
    
    if success:
        category = function_data[1]
        num_params = function_data[3].count(',') + 1
        created_function = "XLOPER12* WINAPI " + str(function_data[0]) + "("
        params = ["XLOPER12* xl" + str(i) for i in range(num_params)]
        sep = ', '
        params = sep.join(params)

        created_function = created_function + params + ")\n{\n#pragma EXPORT_FUNC\n\n"
        created_function = created_function + "\tstd::initializer_list< XLOPER12* > inputs{"
        params = ["xl" + str(i) for i in range(num_params)]
        sep = ', '
        params = sep.join(params)
        created_function = created_function + params + "};"

        cpp_exe_func = function_data[0][3:-1]
        cpp_check_func = cpp_exe_func + "Check"

        created_function = created_function + "\n\n\treturn bind_excel_func" \
                                              "(inputs,\n\tstdfunction_from_functionptr(&" + category + "::" + cpp_check_func + "),\n\t"\
                                              "stdfunction_from_functionptr(&" + category + "::" + cpp_exe_func + "));\n}"
    else:
        created_function = None
    return created_function, success, msg


def read_and_generate(filename):
    generated_functions = []
    messages = []
    lines = []
    i = 0
    all_num_params = []

    namespace = filename[:-4]

    with open(filename) as tsv:
        for line in csv.reader(tsv, delimiter="\t"):
            lines.append([x for x in line if len(x) > 0])

    function_info_set = [lines[i:i + lines_per_function] for i in range(0, len(lines), lines_per_function)]
    function_info_set = [x for x in function_info_set if len(x[0]) > 0]

    for func_info in function_info_set:
        xl_info = flatten_list([func_info[i] for i in range(4) if not i == 2])
        num_params = int((len(xl_info) - 3) / 2)
        all_num_params.append(num_params)
        joined_params = ",".join(xl_info[3:3 + num_params])
        xl_info = xl_info[:3] + [joined_params] + xl_info[-num_params:]
        created_function, success, msg = generate_function(xl_info, i)
        if success:
            generated_functions.append(created_function)
        else:
            messages.append(msg)
        i += 1

    generate_entry_points(generated_functions, namespace)
    generate_errors(messages, len(generated_functions))

    function_names = []
    all_param_types = []
    all_param_names = []
    function_categories = []

    for i in range(len(function_info_set)):
        num_params = all_num_params[i]
        function_names.append(function_info_set[i][0][0])
        all_param_names.append(function_info_set[i][1])
        all_param_types.append(function_info_set[i][2])
        function_categories.append(function_info_set[i][0][1])

    generate_cpp_functions(function_names, all_param_types, all_param_names, function_categories)

filename = "excel_function_data.txt"
if len(sys.argv) == 2 and len(sys.argv[1]) > 5 and ".txt" in sys.argv[1]:
    filename = sys.argv[1]

print("generating for " + filename)
read_and_generate(filename)
