#!/usr/bin/env python

"""

This script will create several auto-generated files:

1. Forward functions and curried functions:
    "${PROJECT_SOURCE_DIR}/include/fplus/fwd_instances.autogenerated_defines"
    "${PROJECT_SOURCE_DIR}/include/fplus/curry_instances.autogenerated_defines"

    (Those files are included by fplus.hpp)

2. Amalgamated library (all in one header library)
    "${PROJECT_SOURCE_DIR}/include_all_in_one/include/fplus/fplus.hpp"

"""

import os
from sys import version_info

# Global settings
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FPLUS_INCLUDE_DIR = os.path.realpath(THIS_DIR + "/../include/fplus")
ALL_IN_ONE_INCLUDE_DIR = os.path.realpath(THIS_DIR + "/../include_all_in_one/include/fplus")
AUTOGENERATED_HEADER = "// THIS FILE WAS GENERATED AUTOMATICALLY. DO NOT EDIT."


def fread_lines(filename):
    """
    Python 2 & 3 agnostic fopen + readlines
    """
    if version_info[0] >= 3:
        f = open(filename, "r", encoding='utf-8', errors='ignore')
    else:
        f = open(filename, "r")
    return f.readlines()


def fread_content(filename):
    """
    Python 2 & 3 agnostic fopen + readlines
    """
    if version_info[0] >= 3:
        f = open(filename, "r", encoding='utf-8', errors='ignore')
    else:
        f = open(filename, "r")
    return f.read()


def fwrite_content(filename, content):
    """
    Python 2 & 3 agnostic fopen + write
    This function will not overwrite the file (and thus not update its modification date) 
    if the new content is unchanged
    """
    old_content = fread_content(filename)
    if old_content == content:
        return

    if version_info[0] >= 3:
        f = open(filename, "w", encoding='utf-8', errors='ignore')
    else:
        f = open(filename, "w")
    f.write(content)
    f.close()


def is_fplus_include_line(code_line):
    """
    Tests whether or not a C++ code line is a include statement that concerns a fplus header,
    (i.e this will *exclude* lines like "#include <vector>")
    """
    return (   code_line.startswith("#include <fplus/")
            or code_line.startswith("#include \""))


def extract_fplus_include_file(code_line):
    """
    Extracts the included file path from an include statement
    """
    return (
         code_line
        .replace("#include <fplus/", "")
        .replace("#include \"", "")
        .replace("\"", "")
        .replace(">", "")[:-1]
    )


def amalgamate_one_file(filename, already_included_files):
    """
    Recursive function that will create an amalgamation for a given header file.
    """
    if filename in already_included_files:
        return ""
    already_included_files.append(filename)

    parsed_result = "\n//\n" + "// " + filename + "\n//\n\n"
    
    lines = fread_lines(FPLUS_INCLUDE_DIR + "/" + filename)
    for code_line in lines:
        if is_fplus_include_line(code_line):
            new_file = extract_fplus_include_file(code_line)
            parsed_result = parsed_result + amalgamate_one_file(new_file, already_included_files)
        else:
            if not "#pragma once" in code_line:
                parsed_result = parsed_result + code_line
    return parsed_result


def amalgamation_content():
    """
    Returns the content that should be written to the amalgamation file
    """
    content = "#pragma once\n" + amalgamate_one_file("fplus.hpp", [])
    return content


def write_amalgamation():
    """
    Writes the amalgamation file
    """
    fwrite_content(ALL_IN_ONE_INCLUDE_DIR + "/fplus.hpp", amalgamation_content())


class FunctionNameAndBindCount:
    """
    Utility struct that groups a function name + its bind count
    """
    def __init__(self, function_name, bind_count):
        self.function_name = function_name
        self.bind_count = bind_count


def gather_function_names_and_bind_counts():
    """
    Looks for pair of lines that look like this in the amalgamation content:
    
        API search type: flatten_tree_breadth_first : Tree a -> [a]
        fwd bind count: 0
    
    and returns a list  [(function_name1, bind_count1), (function_name2, bind_count2), ...]    
    """
    all_function_and_bind_counts = []
    amalgamated_content = amalgamation_content()
    amalgamated_lines = amalgamated_content.split("\n") 
    for line_number, line_content in enumerate(amalgamated_lines):
        if "fwd bind count" in line_content:
            bind_count = int(line_content[-1:])  # The bind count is represented by the last char (which limits us to 9)
            previous_line = amalgamated_lines[line_number - 1]
            function_name = previous_line.split(":")[1].strip() # The function name is the second item in ":" splitted items
            all_function_and_bind_counts.append(FunctionNameAndBindCount(function_name, bind_count))
    return all_function_and_bind_counts


def write_fwd_defines(all_function_and_bind_counts):
    """
    Writes the include/fplus/fwd_instances.autogenerated_defines file    
    """
    def make_fplus_fwd_define_fn_line(function_and_bind_count):
        return "fplus_fwd_define_fn_" + str(function_and_bind_count.bind_count) + "(" + function_and_bind_count.function_name + ")"
    def make_fplus_fwd_flip_define_fn_line(function_and_bind_count):
        return "fplus_fwd_flip_define_fn_1(" + function_and_bind_count.function_name + ")"

    fwd_lines = map(make_fplus_fwd_define_fn_line, all_function_and_bind_counts)

    all_functions_with_bind_1 = filter(lambda v: v.bind_count == 1, all_function_and_bind_counts)
    flip_lines = map(make_fplus_fwd_flip_define_fn_line, all_functions_with_bind_1)

    code = (
               AUTOGENERATED_HEADER + "\n" 
             + "\n".join(fwd_lines)  + "\n"
             + "\n".join(flip_lines) + "\n"
           )

    fwrite_content(FPLUS_INCLUDE_DIR + "/fwd_instances.autogenerated_defines", code)


def write_curry_defines(all_function_and_bind_counts):
    """
    Writes the include/fplus/curry_instances.autogenerated_defines file    
    """
    def make_curry_define_fn_line(function_and_bind_count):
        return "fplus_curry_define_fn_" + str(function_and_bind_count.bind_count) + "(" + function_and_bind_count.function_name + ")"
    curry_lines = map(make_curry_define_fn_line, all_function_and_bind_counts)

    code = (
               AUTOGENERATED_HEADER   + "\n" 
             + "\n".join(curry_lines) + "\n"
           )

    fwrite_content(FPLUS_INCLUDE_DIR + "/curry_instances.autogenerated_defines", code)


def auto_generate():
    """
    Generate all auto-generated files
    """
    all_function_and_bind_counts = gather_function_names_and_bind_counts()
    write_fwd_defines(all_function_and_bind_counts)
    write_curry_defines(all_function_and_bind_counts)
    write_amalgamation()


if __name__ == "__main__":
    auto_generate()
