# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Pre-defined pipelines.

oRelax enables flexible pipeline optimizations before min build.
This namespace offers a pre-defined collection that can be used
as it is or serves as a basis to do further composition.
"""

# pylint: disable=unused-argument

import tvm
from tvm.s_tir import meta_schedule as ms

from . import backend, transform
from .backend.utils import BackendDispatcher


def zero_pipeline(*, enable_warning: bool = False):
    """Wrapper function that returns the zero pipeline.

    Parameters
    ----------
    enable_warning : bool
        A boolean value indicating if to print warnings
        * in LegalizeOps pass, for CallNode whose op's legalization function is
        not registered,
        * in MetaScheduleApplyDatabase pass, for TIR functions now showing up in
        the database. By default we don't print warning.
    """

    @tvm.transform.module_pass(opt_level=0)
    def f_zero_pipeline(mod: tvm.ir.IRModule, ctx: tvm.transform.PassContext) -> tvm.ir.IRModule:
        """Pipeline that applies pre-tuned logs.

        Parameters
        ----------
        mod : tvm.ir.IRModule
            Input IRModule.

        ctx : tvm.transform.PassContext
            The pass context

        Returns
        -------
        mod: tvm.ir.IRModule
            The result transformed module.
        """
        seq = tvm.transform.Sequential(
            [
                transform.LegalizeOps(enable_warning=enable_warning),
                transform.AnnotateTIROpPattern(),
                transform.FoldConstant(),
                transform.FuseOps(),
                transform.FuseTIR(),
            ]
        )
        mod = seq(mod)
        if ms.Database.current():
            mod = transform.MetaScheduleApplyDatabase(enable_warning=enable_warning)(mod)
        return mod

    return f_zero_pipeline


def default_build_pipeline():
    """The default compilation pipeline used in tvm.compile"""

    @tvm.transform.module_pass(opt_level=0)
    def _pipeline(mod: tvm.ir.IRModule, _ctx: tvm.transform.PassContext) -> tvm.ir.IRModule:
        seq = tvm.transform.Sequential(
            [
                backend.DispatchSampling(),
                backend.DispatchSortScan(),
                transform.LegalizeOps(),
                transform.RewriteDataflowReshape(),
                transform.ToNonDataflow(),
                transform.RemovePurityChecking(),
                transform.CallTIRRewrite(),
                transform.StaticPlanBlockMemory(),
                transform.RewriteCUDAGraph(),
                transform.LowerAllocTensor(),
                transform.KillAfterLastUse(),
                transform.LowerRuntimeBuiltin(),
                transform.ComputePrimValue(),
                transform.VMShapeLower(),
                transform.AttachGlobalSymbol(),
            ],
        )
        mod = seq(mod)
        return mod

    return _pipeline


def static_shape_tuning_pipeline(
    total_trials: int,
    target: str | tvm.target.Target,
    work_dir: str = "tuning_logs",
    cpu_weight_prepack: bool = False,
    max_trials_per_task: int | None = None,
):
    """Tune the static shape model and store the log to database.

    Parameters
    ----------
    total_trials : int
        Total number of trials to run.

    target : Union[str, tvm.target.Target]
        The target device to tune the model.

    work_dir : str
        The directory to store the tuning logs.

    cpu_weight_prepack : bool
        Whether to enable the cpu weight prepack feature.

    max_trials_per_task : Optional[int]
        The maximum number of trials to run per task.
        If not specified, it defaults to the value of `total_trials`, and this
        may lead to undersubscribed tuning, potentially skipping some tasks
        entirely. Explicitly setting both parameters avoids this issue and
        provides deterministic resource allocation across all tasks.
        For optimal tuning, set `total_trials` to at least
        `max_trials_per_task * number_of_tuning_tasks` to ensure
        each task receives adequate tuning resources in one iteration.

    Note
    ----
    `cpu_weight_prepack` is expected to be `True` when running on CPU for
    better performance. However, it requires an explicit layout transformation
    step by calling the corresponding vm function, which changes the interface
    of deployment. So we disable it by default. Here is an example to enable it:

    .. code-block:: python

        mod = relax.pipeline.static_shape_tuning_pipeline(
            total_trials=1000,
            target={"kind": "llvm", "num-cores": 16},
            work_dir="tuning_logs",
            cpu_weight_prepack=True,
            max_trials_per_task=64,
        )(mod)

        ex = tvm.compile(mod, target=target)
        vm = relax.VirtualMachine(ex, device=tvm.cpu())

        # Transform the params using the vm function
        # the name should be f"{func_name}_transform_params"
        params = vm["main_transform_params"](params["main"])

        input_data = tvm.runtime.tensor(np.random.randn(1, 3, 224, 224).astype("float32"))
        out = vm["main"](input_data, *params).numpy()
    """

    @tvm.transform.module_pass(opt_level=0)
    def _pipeline(mod: tvm.ir.IRModule, _ctx: tvm.transform.PassContext) -> tvm.ir.IRModule:
        if cpu_weight_prepack:
            pre_tuning_layout_rewrite = [transform.AttachAttrLayoutFreeBuffers()]
            post_tuning_layout_rewrite = [
                transform.SplitLayoutRewritePreproc(),
                transform.LiftTransformParams(),
                transform.FoldConstant(),
            ]
        else:
            pre_tuning_layout_rewrite = []
            post_tuning_layout_rewrite = []

        with tvm.target.Target(target):
            mod = tvm.transform.Sequential(
                [
                    transform.DecomposeOpsForInference(),
                    transform.CanonicalizeBindings(),
                    zero_pipeline(),
                    *pre_tuning_layout_rewrite,
                    # Skip tuning if total_trials is 0
                    (
                        transform.MetaScheduleTuneIRMod(
                            params={},
                            work_dir=work_dir,
                            max_trials_global=total_trials,
                            max_trials_per_task=max_trials_per_task,
                        )
                        if total_trials > 0
                        else tvm.transform.Sequential([])
                    ),
                    transform.MetaScheduleApplyDatabase(work_dir),
                    *post_tuning_layout_rewrite,
                ]
            )(mod)

        return mod

    return _pipeline


# global map of pre-built pipelines
PIPELINE_MAP = {
    "zero": zero_pipeline,
    "default": default_build_pipeline,
    "default_build": default_build_pipeline,
    "static_shape_tuning": static_shape_tuning_pipeline,
}


def get_pipeline(name: str = "zero", **kwargs) -> tvm.transform.Pass:
    """Get pre-build pipeline by name

    Parameters
    ----------
    name : Optional[str]
        Name of the pipeline
    kwargs : Dict[str, object]
        Keyword args for configuring the pipeline.

    Returns
    -------
    pipeline: tvm.transform.Pass
       The transformation pipeline.
    """

    if name not in PIPELINE_MAP:
        raise ValueError(
            f"Unknown pre-built pipeline {name},candidates are {list(PIPELINE_MAP.keys())}"
        )
    return PIPELINE_MAP[name](**kwargs)


def register_pipeline(name: str):
    """Register a new pipeline

    Parameters
    ----------
    name : str
        Name of the pipeline
    """
    if name in PIPELINE_MAP:
        raise ValueError(f"Pipeline {name} has already been registered")

    def _register(func):
        PIPELINE_MAP[name] = func
        return func

    return _register


def library_dispatch_passes(target: tvm.target.Target):
    """Get the default library dispatch passes for the given target."""
    if target.kind.name == "cuda":
        return backend.cuda.library_dispatch_passes(target)
    if target.kind.name == "rocm":
        return backend.rocm.library_dispatch_passes(target)
    if target.kind.name == "metal":
        return backend.gpu_generic.library_dispatch_passes(target)
    if target.kind.name == "llvm":
        return backend.cpu_generic.library_dispatch_passes(target)
    if target.kind.name == "opencl" and "adreno" in target.keys:
        return backend.adreno.library_dispatch_passes(target)
    if BackendDispatcher.is_gpu_target(target):
        return backend.gpu_generic.library_dispatch_passes(target)
    raise ValueError(f"Target {target} is not yet supported by library dispatch passes.")


def legalize_passes(target: tvm.target.Target):
    """Get the default legalization passes for the given target."""
    if target.kind.name == "cuda":
        return backend.cuda.legalize_passes(target)
    if target.kind.name == "rocm":
        return backend.rocm.legalize_passes(target)
    if target.kind.name == "metal":
        return backend.gpu_generic.legalize_passes(target)
    if target.kind.name == "llvm":
        return backend.cpu_generic.legalize_passes(target)
    if target.kind.name == "opencl" and "adreno" in target.keys:
        return backend.adreno.legalize_passes(target)
    if BackendDispatcher.is_gpu_target(target):
        return backend.gpu_generic.legalize_passes(target)
    raise ValueError(f"Target {target} is not yet supported by legalize passes.")


def dataflow_lower_passes(target: tvm.target.Target):
    """Get the default legalization passes for the given target."""
    if target.kind.name == "cuda":
        return backend.cuda.dataflow_lower_passes(target)
    if target.kind.name == "rocm":
        return backend.rocm.dataflow_lower_passes(target)
    if target.kind.name == "metal":
        return backend.gpu_generic.dataflow_lower_passes(target)
    if target.kind.name == "llvm":
        return backend.cpu_generic.dataflow_lower_passes(target)
    if target.kind.name == "opencl" and "adreno" in target.keys:
        return backend.adreno.dataflow_lower_passes(target)
    if BackendDispatcher.is_gpu_target(target):
        return backend.gpu_generic.dataflow_lower_passes(target)
    raise ValueError(f"Target {target} is not yet supported by dataflow lowering passes.")


def finalize_passes(target: tvm.target.Target):
    """Get the default legalization passes for the given target."""
    if target.kind.name == "cuda":
        return backend.cuda.finalize_passes(target)
    if target.kind.name == "rocm":
        return backend.rocm.finalize_passes(target)
    if target.kind.name == "metal":
        return backend.gpu_generic.finalize_passes(target)
    if target.kind.name == "llvm":
        return backend.cpu_generic.finalize_passes(target)
    if target.kind.name == "opencl" and "adreno" in target.keys:
        return backend.adreno.finalize_passes(target)
    if BackendDispatcher.is_gpu_target(target):
        return backend.gpu_generic.finalize_passes(target)
    raise ValueError(f"Target {target} is not yet supported by finalization passes.")


def get_default_pipeline(target: tvm.target.Target):
    """Get the default Relax compilation pipeline for the given target."""
    if target.kind.name == "cuda":
        return backend.cuda.get_default_pipeline(target)
    if target.kind.name == "rocm":
        return backend.rocm.get_default_pipeline(target)
    if target.kind.name == "metal":
        return backend.gpu_generic.get_default_pipeline(target)
    if target.kind.name == "llvm":
        return backend.cpu_generic.get_default_pipeline(target)
    if target.kind.name == "opencl" and "adreno" in target.keys:
        return backend.adreno.get_default_pipeline(target)
    if BackendDispatcher.is_gpu_target(target):
        return backend.gpu_generic.get_default_pipeline(target)
    raise ValueError(
        f"Target {target} is not yet supported by default pipeline. "
        "Please lower and build the IRModule manually."
    )
