# 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

from library import *
from matmul import ReferenceMatmulOp
from batch_matmul import ReferenceBatchMatmulOp
from pathlib import Path
import subprocess


class IreeToolsLauncher:
    """Launcher for IREE tools."""

    def __init__(self, args, operation):
        self.operation = operation

        self.generated_path = Path(args.generated_dir, "generated", args.mlir_dialect)

        self.args = args
        self.benchmark_dispatch_repeat_count = args.batch_size
        self.batch_size = args.batch_size

        # paths to source dispatch mlir, compiled vmfb, and logs.
        self.operation_path = self.generated_path.joinpath(
            OperationKindNames[operation.operation_kind], operation.name()
        )

        self.source_mlir_file = self.operation_path.joinpath(
            operation.name()
        ).with_suffix(".mlir")

        # path to cached numpy refernece input and expected output files.
        self.op_reference_cache_path = Path(
            args.generated_dir, "generated", "reference_cache", operation.name()
        )

        if not self.op_reference_cache_path.exists():
            self.op_reference_cache_path.mkdir(parents=True, exist_ok=True)

        # path to iree-compile tool. (for compiling the input mlir file to vmfb)
        self.iree_compile_path = Path(args.iree_bin_dir, "iree-compile")

        # path to iree-benchmark-module tool. (for performance benchmarking and profiling)
        self.iree_benchmark_module_path = Path(
            args.iree_bin_dir, "iree-benchmark-module"
        )

        # path to iree-run-module tool. (for verification)
        self.iree_run_module_path = Path(args.iree_bin_dir, "iree-run-module")

        # output vmfb files for verification and profiling.
        vmfb_filename = f"{operation.name()}"

        if operation.operation_kind == OperationKind.SplitkMatmul:
            split_k_suffix = "_".join(["split_k_slice", str(operation.split_k_slices)])
            vmfb_filename = f"{vmfb_filename}_{split_k_suffix}"

        self.vmfb_verify_filepath = self.operation_path.joinpath(
            self.operation.name()
        ).with_name(f"{vmfb_filename}_verify.vmfb")
        self.vmfb_profile_filepath = self.operation_path.joinpath(
            self.operation.name()
        ).with_name(f"{vmfb_filename}_profile.vmfb")

        # reference implementation for the operation_kind.
        self.reference_impl_map = {
            OperationKind.Matmul: ReferenceMatmulOp,
            OperationKind.SplitkMatmul: ReferenceMatmulOp,
            OperationKind.BatchMatmul: ReferenceBatchMatmulOp,
        }

    def iree_compile(self, compilation_mode):
        """Compiles the input mlir file to vmfb file."""

        benchmark_dispatch_repeat_count = (
            self.benchmark_dispatch_repeat_count
            if compilation_mode == CompilationMode.Profile
            else 1
        )
        vmfb_filepath = (
            self.vmfb_profile_filepath
            if compilation_mode == CompilationMode.Profile
            else self.vmfb_verify_filepath
        )

        # Base iree-compile commandline
        cmd = [
            f"{self.iree_compile_path}",
            f"{self.source_mlir_file}",
            "-o",
            f"{vmfb_filepath}",
        ]

        # General compilation options
        cmd += [f"--iree-hal-target-backends={self.args.device}"]

        if self.args.device == "cuda":
            cmd += [f"--iree-hal-cuda-llvm-target-arch={self.args.cuda_arch}"]
        if self.operation.operation_kind == OperationKind.SplitkMatmul:
            cmd += [
                f"--iree-flow-split-matmul-reduction={self.operation.split_k_slices}"
            ]
        if self.args.use_mma_sync:
            cmd += [f"--iree-codegen-llvmgpu-use-mma-sync"]
        if self.args.use_wmma:
            cmd += [f"--iree-codegen-llvmgpu-use-wmma"]

        # Compilation options for profiling
        cmd += [
            f"--iree-hal-benchmark-dispatch-repeat-count={benchmark_dispatch_repeat_count}"
        ]

        # Appends print ir options at the end of the command line.
        if self.args.mlir_print_ir_after_all:
            cmd += [f"--mlir-print-ir-after-all"]

        if not vmfb_filepath.exists() or self.args.force_compile:
            complie_mode_str = CompilationModeNames[compilation_mode]

            print(f"[Compiling ({complie_mode_str})] {' '.join(cmd)}")

            iree_compile_stdout_filepath = self.operation_path.joinpath(
                "iree_compile_cmd_stdout.mlir"
            )

            with open(iree_compile_stdout_filepath, "w") as fp:
                subprocess.run(cmd, stderr=fp)

        elif self.args.verbose:
            print(
                f"Skipping compilation of operation: {vmfb_filepath} since it already exists."
            )

    def verify(self, configuration):
        """Verifies the operation with a given configuration."""
        # First compile the operation to a vmfb file.
        self.iree_compile(CompilationMode.Verify)

        # Verify using random data distribution.
        reference_run = self.reference_impl_map[self.operation.operation_kind](
            self.operation,
            self.op_reference_cache_path,
            Distribution.Random,
            Distribution.Random,
        )

        if not reference_run.is_cached():
            reference_run()

        # Commandline `iree-run-module` for verification.
        cmd = [
            f"{self.iree_run_module_path}",
            f"--module={self.vmfb_verify_filepath}",
            f"--device={self.args.device}",
        ]

        # Operation-specific verification command-line.
        cmd.append(f"--function={self.operation.name()}_{configuration.name()}")
        for input_file_path in reference_run.get_input_filepaths():
            cmd.append(f"--input=@{input_file_path}")
        for output_file_path in reference_run.get_output_filepaths():
            cmd.append(f"--expected_output=@{output_file_path}")

        # Print the command if verbose.
        if self.args.verbose:
            print(f"[Verification] {' '.join(cmd)}")

        # Launch verification.
        cmd_output = subprocess.check_output(cmd, text=True)

        # Save the verification command and the output, only if requested
        # (file writing could slow down the verification).
        if self.args.save_cmds:
            filepath = self.operation_path.joinpath("iree_run_module.stdout")
            with open(filepath, "w") as fp:
                fp.write(f"[Command] $ {' '.join(cmd)}\n")
                fp.write(cmd_output)

        # Parse the verification output.
        m = re.search(r"\[(?P<verification_result>[a-zA-Z]+)\]", cmd_output)
        if m is None:
            raise ValueError(
                f"Failed to parse verification output by iree-run-module: {cmd_output}"
            )
        verification_result = m.group("verification_result")

        if self.args.verbose or verification_result != "SUCCESS":
            print(cmd_output)

        return verification_result

    def profile(self, configuration):
        """Profiles the operation with a given configuration."""
        # First compile the operation to a vmfb file.
        self.iree_compile(CompilationMode.Profile)

        # Commandline `iree-benchmark-module` for profiling.
        cmd = [
            f"{self.iree_benchmark_module_path}",
            f"--module={self.vmfb_profile_filepath}",
            f"--device={self.args.device}",
        ]

        # Profiling specific flags.
        cmd += [f"--benchmark_repetitions={self.args.benchmark_repetitions}"]
        cmd += [f"--batch_size={self.batch_size}"]

        # Operation-specific profiling command-line.
        cmd += [f"--function={self.operation.name()}_{configuration.name()}"]
        cmd += [f"--input={self.operation.lhs_npy_shape()}"]
        cmd += [f"--input={self.operation.rhs_npy_shape()}"]

        # Print the command if verbose.
        if self.args.verbose:
            print(f"[Profiling] {' '.join(cmd)}")

        # Launch profiling.
        cmd_output = subprocess.check_output(cmd, text=True, stderr=subprocess.STDOUT)

        # Save the profiling command and the output, only if requested
        # (file writing could slow down the profiling).
        if self.args.save_cmds:
            filepath = self.operation_path.joinpath("iree_benchmark_module.stdout")
            with open(filepath, "w") as fp:
                fp.write(f"[Command] $ {' '.join(cmd)}\n")
                fp.write(cmd_output)

        # Parse the profiling output.
        m = re.search(r"real_time_median\s+(?P<runtime>\d+.\d+)\s+ms", cmd_output)
        if m is None:
            raise ValueError(
                f"Failed to parse runtime from benchmark result: {cmd_output}"
            )
        runtime_in_ms = float(m.group("runtime"))
        return runtime_in_ms
