#
# \file generator.py
#
# \brief Generates the CUTLASS Library's instances
#

import enum
import os.path
import shutil

from conv2d_operation import *
from gemm_operation import *
from library import *

###################################################################################################


class EmitOperationKindLibrary:
    def __init__(self, generated_path, kind, args):
        self.generated_path = generated_path
        self.kind = kind
        self.args = args

        self.emitters = {
            OperationKind.Gemm: EmitGemmConfigurationLibrary,
            OperationKind.Conv2d: EmitConv2dConfigurationLibrary,
        }

        self.configurations = []

        self.header_template = """
/*
 Generated by manifest.py - Do not edit.
*/

#include "cutlass/cutlass.h"
#include "cutlass/library/library.h"
#include "cutlass/library/manifest.h"

namespace cutlass {
namespace library {

///////////////////////////////////////////////////////////////////////////////////////////////////

"""
        self.entry_template = """

//
// Entry point to construct operations
//
void initialize_all_${operation_name}_operations(Manifest &manifest) {
"""
        self.configuration_prototype_template = (
            "void initialize_${configuration_name}(Manifest &manifest);\n"
        )
        self.configuration_template = "  initialize_${configuration_name}(manifest);\n"

        self.epilogue_template = """

}

///////////////////////////////////////////////////////////////////////////////////////////////////

} // namespace library
} // namespace cutlass

"""

    #
    def __enter__(self):
        self.operation_path = os.path.join(
            self.generated_path, OperationKindNames[self.kind]
        )
        os.mkdir(self.operation_path)

        self.top_level_path = os.path.join(
            self.operation_path, "all_%s_operations.cu" % OperationKindNames[self.kind]
        )

        self.top_level_file = open(self.top_level_path, "w")
        self.top_level_file.write(self.header_template)

        self.source_files = [self.top_level_path]

        return self

    #
    def emit(self, configuration_name, operations):

        with self.emitters[self.kind](
            self.operation_path, configuration_name
        ) as configuration_emitter:
            for operation in operations:
                configuration_emitter.emit(operation)

            self.source_files.append(configuration_emitter.configuration_path)

        self.configurations.append(configuration_name)
        self.top_level_file.write(
            SubstituteTemplate(
                self.configuration_prototype_template,
                {"configuration_name": configuration_name},
            )
        )

    #
    def __exit__(self, exception_type, exception_value, traceback):
        self.top_level_file.write(
            SubstituteTemplate(
                self.entry_template, {"operation_name": OperationKindNames[self.kind]}
            )
        )

        for configuration_name in self.configurations:
            self.top_level_file.write(
                SubstituteTemplate(
                    self.configuration_template,
                    {"configuration_name": configuration_name},
                )
            )

        self.top_level_file.write(self.epilogue_template)
        self.top_level_file.close()


###################################################################################################
###################################################################################################


class Options:
    def __init__(self):
        pass


###################################################################################################

#
class Manifest:

    #
    def __init__(self, args):
        self.operations = {}
        self.args = args

        architectures = (
            args.architectures.split(";") if len(args.architectures) else ["50"]
        )
        self.compute_capabilities = [int(x) for x in architectures]

        self.selected_kernels = []

        if args.operations == "all":
            self.operations_enabled = []
        else:

            operations_list = [OperationKind.Gemm, OperationKind.Conv2d]

            self.operations_enabled = [
                x
                for x in operations_list
                if OperationKindNames[x] in args.operations.split(",")
            ]

        if args.kernels == "all":
            self.kernel_names = []
        else:
            self.kernel_names = [x for x in args.kernels.split(",") if x != ""]

        self.ignore_kernel_names = [
            x for x in args.ignore_kernels.split(",") if x != ""
        ]

        if args.kernel_filter_file is None:
            self.kernel_filter_list = []
        else:
            self.kernel_filter_list = self.get_kernel_filters(args.kernel_filter_file)

        self.operation_count = 0
        self.operations_by_name = {}
        self.top_level_prologue = """

#include "cutlass/library/library.h"
#include "cutlass/library/manifest.h"

namespace cutlass {
namespace library {

${prototypes}

void initialize_all(Manifest &manifest) {

"""
        self.top_level_reserve = "  manifest.reserve(${operation_count});\n\n"
        self.top_level_epilogue = """
}

} // namespace library
} // namespace cutlass

"""

    def get_kernel_filters(self, kernelListFile):
        if os.path.isfile(kernelListFile):
            with open(kernelListFile, "r") as fileReader:
                lines = [
                    line.rstrip() for line in fileReader if not line.startswith("#")
                ]

            lines = [re.compile(line) for line in lines if line]
            return lines
        else:
            return []

    def filter_out_kernels(self, kernel_name, kernel_filter_list):

        for kernel_filter_re in kernel_filter_list:
            if kernel_filter_re.search(kernel_name) is not None:
                return True

        return False

    #
    def _filter_string_matches(self, filter_string, haystack):
        """ Returns true if all substrings appear in the haystack in order"""
        substrings = filter_string.split("*")
        for sub in substrings:
            idx = haystack.find(sub)
            if idx < 0:
                return False
            haystack = haystack[idx + len(sub) :]
        return True

    #
    def filter(self, operation):
        """ Filtering operations based on various criteria"""

        # filter based on compute capability
        enabled = False
        for cc in self.compute_capabilities:
            if (
                cc >= operation.tile_description.minimum_compute_capability
                and cc <= operation.tile_description.maximum_compute_capability
            ):

                enabled = True
                break

        if not enabled:
            return False

        if (
            len(self.operations_enabled)
            and not operation.operation_kind in self.operations_enabled
        ):
            return False

        # eliminate duplicates
        if operation.procedural_name() in self.operations_by_name.keys():
            return False

        # Filter based on list of valid substrings
        if len(self.kernel_names):
            name = operation.procedural_name()
            enabled = False

            # compare against the include list
            for name_substr in self.kernel_names:
                if self._filter_string_matches(name_substr, name):
                    enabled = True
                    break

            # compare against the exclude list
            for name_substr in self.ignore_kernel_names:
                if self._filter_string_matches(name_substr, name):
                    enabled = False
                    break

        if len(self.kernel_filter_list) > 0:
            enabled = False
            if self.filter_out_kernels(
                operation.procedural_name(), self.kernel_filter_list
            ):
                enabled = True

        # todo: filter based on compute data type
        return enabled

    #

    #
    def append(self, operation):
        """ 
      Inserts the operation.

      operation_kind -> configuration_name -> []
    """

        if self.filter(operation):

            self.selected_kernels.append(operation.procedural_name())

            self.operations_by_name[operation.procedural_name()] = operation

            # add the configuration
            configuration_name = operation.configuration_name()

            if operation.operation_kind not in self.operations.keys():
                self.operations[operation.operation_kind] = {}

            if (
                configuration_name
                not in self.operations[operation.operation_kind].keys()
            ):
                self.operations[operation.operation_kind][configuration_name] = []

            self.operations[operation.operation_kind][configuration_name].append(
                operation
            )
            self.operation_count += 1

    #

    #
    def emit(self, target=GeneratorTarget.Library):

        operation_emitters = {GeneratorTarget.Library: EmitOperationKindLibrary}

        generated_path = os.path.join(self.args.curr_build_dir, "generated")

        # create generated/
        if os.path.exists(generated_path):
            shutil.rmtree(generated_path)

        os.mkdir(generated_path)

        source_files = []

        top_level_path = os.path.join(generated_path, "initialize_all.cpp")
        with open(top_level_path, "w") as top_level_file:

            if target == GeneratorTarget.Library:
                source_files.append(top_level_path)

            prototypes = []
            for operation_kind, configurations in self.operations.items():
                prototypes.append(
                    SubstituteTemplate(
                        "void initialize_all_${operation_kind}_operations(Manifest &manifest);",
                        {"operation_kind": OperationKindNames[operation_kind]},
                    )
                )

            top_level_file.write(
                SubstituteTemplate(
                    self.top_level_prologue, {"prototypes": "\n".join(prototypes)}
                )
            )

            top_level_file.write(
                SubstituteTemplate(
                    self.top_level_reserve,
                    {"operation_count": str(self.operation_count)},
                )
            )

            # for each operation kind, emit initializer for all configurations
            for operation_kind, configurations in self.operations.items():

                with operation_emitters[target](
                    generated_path, operation_kind, self.args
                ) as operation_kind_emitter:
                    for configuration_name, operations in configurations.items():
                        operation_kind_emitter.emit(configuration_name, operations)

                    source_files += operation_kind_emitter.source_files

                top_level_file.write(
                    SubstituteTemplate(
                        "  initialize_all_${operation_kind}_operations(manifest);\n",
                        {"operation_kind": OperationKindNames[operation_kind]},
                    )
                )

            top_level_file.write(self.top_level_epilogue)

        # write the manifest.cmake file containing paths from all targets
        manifest_path = os.path.join(generated_path, "manifest.cmake")
        with open(manifest_path, "w") as manifest_file:

            target_name = "cutlass_library_objs"

            target_text = SubstituteTemplate(
                """cutlass_target_sources(
  ${target_name}
  BATCH_SOURCES ON
  PRIVATE
""",
                {"target_name": target_name},
            )

            manifest_file.write(target_text)

            for source_file in source_files:
                manifest_file.write("    %s\n" % str(source_file.replace("\\", "/")))
            manifest_file.write(")")

    #


###################################################################################################


def GenerateManifest(args, operations, output_dir):
    assert isinstance(operations, list)
    if len(operations) == 0:
        return
    op = operations[0]
    required_cuda_ver_major = op.required_cuda_ver_major
    required_cuda_ver_minor = op.required_cuda_ver_minor

    manifest_path = os.path.join(
        output_dir, "all_%s_%s_operations.cu" % (args.operations, args.type)
    )
    f = open(manifest_path, "w")
    f.write(
        """
/*
 Generated by generator.py - Do not edit.
*/

#if __CUDACC_VER_MAJOR__ > %s || (__CUDACC_VER_MAJOR__ == %s && __CUDACC_VER_MINOR__ >= %s)

#include "cutlass/cutlass.h"
#include "src/cuda/cutlass/library.h"
#include "src/cuda/cutlass/manifest.h"

namespace cutlass {
namespace library {

"""
        % (
            str(required_cuda_ver_major),
            str(required_cuda_ver_major),
            str(required_cuda_ver_minor),
        )
    )

    for op in operations:
        f.write("void initialize_%s(Manifest &manifest);\n" % op.procedural_name())

    f.write(
        """
void initialize_all_%s_%s_operations(Manifest &manifest) {
"""
        % (args.operations, args.type)
    )

    for op in operations:
        f.write("    initialize_%s(manifest);\n" % op.procedural_name())

    f.write(
        """
}

}  // namespace library
}  // namespace cutlass

#endif
"""
    )
    f.close()
