# Copyright 2023 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

import enum, re
from enum import auto
import numpy as np
from abc import ABC, abstractmethod
from collections import namedtuple

###################################################################################################
# This file contains library of enumerations and classes used to build operation descritpions.
# The operation descriptions are used to generate MLIR source files, performance tuning configuration,
# reference implementations, and numpy input/output files.

# The file is organized as follows:
# 1. Enumerated `Type`s grouped together for categories, For e.g. [Arch]Type, [Data]Type etc.
# 2. Dictonaries `Names` mapping the enumeration values to their string names.
#    For e.g. [Arch]TypeNames, [Data]TypeNames etc.
# 3. `Tags` for each enumeration value to be used in the generated MLIR source files.
#    For e.g. [TranslationInfo]Tags
###################################################################################################


# Architecure types
###################################################################################################
class ArchType(enum.Enum):
    Cpu = auto()
    Gpu = auto()


ArchTypeNames = {
    ArchType.Cpu: "cpu",
    ArchType.Gpu: "gpu",
}


class GpuArchType(enum.Enum):
    nvptx = auto()
    rocm = auto()
    spirv = auto()


GpuArchTypeNames = {
    GpuArchType.nvptx: "nvptx",
    GpuArchType.rocm: "rocm",
    GpuArchType.spirv: "spirv",
}


# Operation kinds
###################################################################################################
class OperationKind(enum.Enum):
    Matmul = auto()
    BatchMatmul = auto()
    SplitkMatmul = auto()
    Conv2d = auto()


OperationKindNames = {
    OperationKind.Matmul: "matmul",
    OperationKind.SplitkMatmul: "matmul_splitk",
    OperationKind.BatchMatmul: "batch_matmul",
    OperationKind.Conv2d: "conv2d",
}


# MLIR dialects
###################################################################################################
class MlirDialect(enum.Enum):
    Linalg = auto()
    Mhlo = auto()


MlirDialectNames = {
    MlirDialect.Linalg: "linalg",
    MlirDialect.Mhlo: "mhlo",
}


# Compilation modes (verification or benchmarking/profiling)
###################################################################################################
class CompilationMode(enum.Enum):
    Verify = auto()
    Profile = auto()


CompilationModeNames = {
    CompilationMode.Verify: "verify",
    CompilationMode.Profile: "profile",
}


class CompilationConfigType(enum.Enum):
    Default = auto()
    Custom = auto()


CompilationConfigTypeName = {
    CompilationConfigType.Default: "default",
    CompilationConfigType.Custom: "custom",
}


# Enumerations for data types and layouts
###################################################################################################
class DataType(enum.Enum):
    b1 = auto()
    u4 = auto()
    u8 = auto()
    u16 = auto()
    u32 = auto()
    u64 = auto()
    s4 = auto()
    s8 = auto()
    s16 = auto()
    s32 = auto()
    s64 = auto()
    e4m3 = auto()
    e5m2 = auto()
    f16 = auto()
    bf16 = auto()
    f32 = auto()
    tf32 = auto()
    f64 = auto()
    invalid = auto()


DataTypeName = {
    DataType.b1: "b1",
    DataType.u4: "u4",
    DataType.u8: "u8",
    DataType.u16: "u16",
    DataType.u32: "u32",
    DataType.u64: "u64",
    DataType.s4: "s4",
    DataType.s8: "s8",
    DataType.s16: "s16",
    DataType.s32: "s32",
    DataType.s64: "s64",
    DataType.e4m3: "e4m3",
    DataType.e5m2: "e5m2",
    DataType.f16: "f16",
    DataType.bf16: "bf16",
    DataType.f32: "f32",
    DataType.tf32: "tf32",
    DataType.f64: "f64",
}

DataTypeNumPyTag = {
    DataType.f16: np.float16,
    DataType.f32: np.float32,
}

DataTypeSizeInBits = {
    DataType.b1: 1,
    DataType.u4: 4,
    DataType.u8: 8,
    DataType.u16: 16,
    DataType.u32: 32,
    DataType.u64: 64,
    DataType.s4: 4,
    DataType.s8: 8,
    DataType.s16: 16,
    DataType.s32: 32,
    DataType.s64: 64,
    DataType.e4m3: 8,
    DataType.e5m2: 8,
    DataType.f16: 16,
    DataType.bf16: 16,
    DataType.f32: 32,
    DataType.tf32: 32,
    DataType.f64: 64,
}


class LayoutType(enum.Enum):
    ColumnMajor = auto()
    RowMajor = auto()
    NHWC = auto()
    NCWH = auto()


# cuBLAS/cuDNN layout type names convention is followed for the layout names.
# https://docs.nvidia.com/cuda/cublas/index.html#cublasoperation-t
ShortLayoutTypeName = {
    LayoutType.ColumnMajor: "n",
    LayoutType.RowMajor: "t",
    LayoutType.NHWC: "nhwc",
    LayoutType.NCWH: "ncwh",
}


# Compilation pipelines/translation info.
###################################################################################################
class TranslationInfo(enum.Enum):
    LLVMGPUMatmulSIMT = auto()
    LLVMGPUMatmulTensorCore = auto()
    LLVMGPUMatmulTensorCoreMmaSync = auto()


TranslationInfoTag = {
    TranslationInfo.LLVMGPUMatmulSIMT: "LLVMGPUMatmulSIMT",
    TranslationInfo.LLVMGPUMatmulTensorCore: "LLVMGPUMatmulTensorCore",
    TranslationInfo.LLVMGPUMatmulTensorCoreMmaSync: "LLVMGPUMatmulTensorCoreMmaSync",
}

TranslationInfoName = {
    TranslationInfo.LLVMGPUMatmulSIMT: "simt_ffma",
    TranslationInfo.LLVMGPUMatmulTensorCore: "tensorcore_wmma",
    TranslationInfo.LLVMGPUMatmulTensorCoreMmaSync: "tensorcore_mmasync",
}


# Distribution of values in a tensor.
###################################################################################################
class Distribution(enum.Enum):
    Empty = auto()
    Zeros = auto()
    Ones = auto()
    Sequential = auto()
    Identity = auto()
    Random = auto()


DistributionName = {
    Distribution.Empty: "empty",
    Distribution.Zeros: "zeros",
    Distribution.Ones: "ones",
    Distribution.Sequential: "seq",
    Distribution.Identity: "identity",
    Distribution.Random: "random",
}

###################################################################################################
# The next part of this file contains the data structures for describing a tensor, tiles etc that
# are built using the above enumerations. These data structures are used to create compose bigger
# data structures that describe an operation or a sequence of operations, along with compilation
# pipeling to form a collection of dispatches to profiled.
###################################################################################################


class TensorDescription:
    """A class for tensor description."""

    def __init__(self, datatype, layout):
        self.datatype = datatype
        self.layout = layout

    def name(self):
        return "%s%s" % (DataTypeName[self.datatype], ShortLayoutTypeName[self.layout])


class TileDescription:
    """A class for tile description."""

    def __init__(self, threadblock_shape, stages, block_dim):
        self.threadblock_shape = threadblock_shape  # in number of elements in M, N, K
        self.stages = stages  # number of shared memory stages in tile K
        self.block_dim = block_dim  # block dimension in number of threads in x, y, z

    def name(self):
        return "%dx%d_%dx%d" % (
            self.threadblock_shape[0],
            self.threadblock_shape[1],
            self.threadblock_shape[2],
            self.stages,
        )


###################################################################################################
# The following part contains utility functions for which are used by the profiler tool.
# These function may be moved as the need for create a proper structure for the
# functionality they provide becomes apparent and necessary as we move forward.
###################################################################################################
def get_np_array(tensor_description, shape, dist):
    """Returns a numpy array based on the distribution and shape."""
    # Fix the seed for reproducibility.
    np.random.seed(42)

    # Generate the numpy array based on the distribution.
    if dist == Distribution.Empty:
        return np.empty(shape)
    elif dist == Distribution.Zeros:
        return np.zeros(shape)
    elif dist == Distribution.Ones:
        return np.ones(shape)
    elif dist == Distribution.Sequential:
        return np.arange(np.prod(shape)).reshape(shape)
    elif dist == Distribution.Identity:
        return np.eye(shape[0], shape[1])
    elif dist == Distribution.Random:
        if tensor_description.datatype == DataType.s8:
            return np.random.randint(-2, 3, shape)
        elif tensor_description.datatype == DataType.u8:
            return np.random.randint(0, 4, shape)
        elif (
            tensor_description.datatype == DataType.f16
            or tensor_description.datatype == DataType.bf16
        ):
            return np.random.randint(-3, 4, shape)
        elif tensor_description.datatype == DataType.f32:
            return np.random.randint(-7, 8, shape)


###################################################################################################
def SubstituteTemplate(template, values):
    """Substitutes values into a template string."""
    text = template
    for key, value in values.items():
        regex = "\\$\\{%s\\}" % key
        newtext = re.sub(regex, value, text)
        text = newtext
    return text


###################################################################################################
class ReferenceOpInterface(ABC):
    """Interface for reference implementations."""

    @abstractmethod
    def get_input_filepaths(self):
        """Returns the list of inputs."""
        pass

    @abstractmethod
    def get_output_filepaths(self):
        """Returns the list of outputs/."""
        pass

    @abstractmethod
    def __call__(self):
        """Runs the reference implementation."""
        pass

    def is_cached(self):
        """Returns whether the reference run is cached."""

        # Returns False if any of the reference input are missing.
        for input_filepath in self.get_input_filepaths():
            if not input_filepath.exists():
                return False

        # Returns False if any of the reference output are missing.
        for output_filepath in self.get_output_filepaths():
            if not output_filepath.exists():
                return False

        # Returns True if all the reference inputs and outputs are cached.
        return True

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