# 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.
"""Meta schedule integration with high-level IR"""

import warnings
from typing import TYPE_CHECKING, Union

# isort: off
from typing import Literal

# isort: on

from tvm_ffi import get_global_func, register_global_func

from tvm.ir import IRModule
from tvm.ir.transform import PassContext
from tvm.runtime import Tensor
from tvm.target import Target
from tvm.tirx.expr import IntImm

from .builder import Builder
from .cost_model import CostModel
from .database import Database
from .extracted_task import ExtractedTask
from .logging import get_loggers_from_work_dir
from .measure_callback import MeasureCallback
from .runner import Runner
from .search_strategy import SearchStrategy
from .space_generator import SpaceGenerator
from .task_scheduler import TaskScheduler
from .tune import tune_tasks
from .tune_context import TuneContext
from .utils import fork_seed

if TYPE_CHECKING:
    from tvm import relax

_extract_task_func = get_global_func(  # pylint: disable=invalid-name
    "relax.backend.MetaScheduleExtractTask",
    allow_missing=True,
)


def extract_tasks(
    mod: Union[IRModule, "relax.Function"],
    target: Target,
    params: dict[str, Tensor] | None = None,
    module_equality: str = "structural",
) -> list[ExtractedTask]:
    """Extract tuning tasks from a relax program.

    Parameters
    ----------
    mod : Union[IRModule, relax.Function]
        The module or function to tune
    target : tvm.target.Target
        The compilation target
    params : Optional[Dict[str, tvm.runtime.Tensor]]
        The associated parameters of the program
    module_equality : Optional[str]
        A string to specify the module equality testing and hashing method.
        It must be one of the followings:
          - "structural": Use StructuralEqual/Hash
          - "ignore-tensor": Same as "structural", but ignore tensor raw data during
                              equality testing and hashing.
          - "anchor-block": Apply equality testing and hashing on the anchor block extracted from a
                            given module. The "ignore-tensor" varint is used for the extracted
                            blocks or in case no anchor block is found.
                            For the definition of the anchor block, see tirx/analysis/analysis.py.

    Returns
    -------
    tasks: List[ExtractedTask]
        The tasks extracted from this module
    """
    # pylint: disable=import-outside-toplevel
    from tvm.relax.expr import Function as RelaxFunc
    from tvm.relax.transform import BindParams

    # pylint: enable=import-outside-toplevel
    if isinstance(mod, RelaxFunc):
        mod = IRModule({"main": mod})
    if not isinstance(target, Target):
        target = Target(target)
    if params:
        mod = BindParams("main", params)(mod)
    return list(_extract_task_func(mod, target, module_equality))


def extracted_tasks_to_tune_contexts(
    extracted_tasks: list[ExtractedTask],
    work_dir: str,
    space: SpaceGenerator.SpaceGeneratorType = "post-order-apply",
    strategy: SearchStrategy.SearchStrategyType = "evolutionary",
    num_threads: Literal["physical", "logical"] | int = "physical",
    seed: int | None = None,
) -> tuple[list[TuneContext], list[float]]:
    """Convert ExtractedTask to TuneContext.

    Parameters
    ----------
    tasks : List[ExtractedTask]
        The tasks to be converted
    work_dir : str
        The working directory to store logs and databases
    space : SpaceGenerator.SpaceGeneratorType
        The space generator to use.
    strategy : SearchStrategy.SearchStrategyType
        The search strategy to use.
    num_threads : Union[Literal["physical", "logical"], int]
        The number of threads to use in multi-threaded search algorithm.
    seed : Optional[int]
        The random seed to use.

    Returns
    -------
    tasks : List[TuneContext]
        The converted tasks
    task_weights : List[float]
        The weights of the tasks
    """
    tasks: list[TuneContext] = []
    task_weights: list[float] = []
    for task, logger, rand_state in zip(
        extracted_tasks,
        get_loggers_from_work_dir(work_dir, [t.task_name for t in extracted_tasks]),
        fork_seed(seed, n=len(extracted_tasks)),
    ):
        if task.mod.attrs.get("tirx.is_scheduled", False):
            warnings.warn("The task {task.task_name} is already scheduled, skipping it.")
            continue
        tasks.append(
            TuneContext(
                mod=task.dispatched[0],
                target=task.target,
                space_generator=space,
                search_strategy=strategy,
                task_name=task.task_name,
                logger=logger,
                rand_state=rand_state,
                num_threads=num_threads,
            ).clone()
        )
        task_weights.append(task.weight)
    return tasks, task_weights


def tune_relax(
    mod: Union[IRModule, "relax.Function"],
    params: dict[str, Tensor],
    target: str | Target,
    work_dir: str,
    max_trials_global: int,
    max_trials_per_task: int | None = None,
    op_names: list[str] | None = None,
    *,
    num_trials_per_iter: int = 64,
    builder: Builder.BuilderType = "local",
    runner: Runner.RunnerType = "local",
    database: Database.DatabaseType = "json",
    cost_model: CostModel.CostModelType = "xgb",
    measure_callbacks: MeasureCallback.CallbackListType = "default",
    task_scheduler: TaskScheduler.TaskSchedulerType = "gradient",
    space: SpaceGenerator.SpaceGeneratorType = "post-order-apply",
    strategy: SearchStrategy.SearchStrategyType = "evolutionary",
    seed: int | None = None,
    module_equality: str = "structural",
) -> Database:
    """Tune a Relax program.

    Parameters
    ----------
    mod : Union[IRModule, relax.Function]
        The module or function to tune
    params : Optional[Dict[str, tvm.runtime.Tensor]]
        The associated parameters of the program
    target : Union[Target, str]
        The compilation target
    work_dir : str
        The working directory to store the tuning records
    max_trials_global : int
        The maximum number of trials to run
    max_trials_per_task : Optional[int]
        The maximum number of trials to run for each task
    op_names: Optional[List[str]]
        A list of operator names to specify which op to tune. When it is None, all operators
        are tuned.
    num_trials_per_iter : int
        The number of trials to run per iteration
    builder : BuilderType
        The builder to use
    runner : RunnerType
        The runner to use
    database : DatabaseType
        The database to use
    cost_model : CostModelType
        The cost model to use
    measure_callbacks : CallbackListType
        The measure callbacks to use
    task_scheduler : TaskSchedulerType
        The task scheduler to use
    space : SpaceGeneratorType
        The space generator to use
    strategy : SearchStrategyType
        The search strategy to use
    seed : Optional[int]
        The random seed
    module_equality : Optional[str]
        A string to specify the module equality testing and hashing method.
        It must be one of the followings:
          - "structural": Use StructuralEqual/Hash
          - "ignore-tensor": Same as "structural", but ignore tensor raw data during
                              equality testing and hashing.
          - "anchor-block": Apply equality testing and hashing on the anchor block extracted from a
                            given module. The "ignore-tensor" variant is used for the extracted
                            blocks or in case no anchor block is found.
                            For the definition of the anchor block, see tirx/analysis/analysis.py.

    Returns
    -------
    database : Database
        The database that contains the tuning records
    """
    all_tasks = extract_tasks(mod, target, params, module_equality=module_equality)

    if not op_names:
        selected_tasks = all_tasks
    else:
        selected_tasks = []

        for task in all_tasks:
            for op_name in op_names:
                if op_name in task.task_name:
                    selected_tasks.append(task)

    tasks, task_weights = extracted_tasks_to_tune_contexts(
        extracted_tasks=selected_tasks,
        work_dir=work_dir,
        space=space,
        strategy=strategy,
        seed=seed,
    )
    return tune_tasks(
        tasks=tasks,
        task_weights=task_weights,
        work_dir=work_dir,
        max_trials_global=max_trials_global,
        max_trials_per_task=max_trials_per_task,
        num_trials_per_iter=num_trials_per_iter,
        builder=builder,
        runner=runner,
        database=database,
        cost_model=cost_model,
        measure_callbacks=measure_callbacks,
        task_scheduler=task_scheduler,
        module_equality=module_equality,
    )


@register_global_func("tvm.s_tir.meta_schedule.tune_relax")
def _tune_relax(
    mod: Union[IRModule, "relax.Function"],
    params: dict[str, Tensor],
    target: str | Target,
    work_dir: str,
    max_trials_global: int,
    max_trials_per_task: int | None = None,
    op_names: list[str] | None = None,
    *,
    num_trials_per_iter: int = 64,
    builder: Builder.BuilderType = "local",
    runner: Runner.RunnerType = "local",
    database: Database.DatabaseType = "json",
    cost_model: CostModel.CostModelType = "xgb",
    measure_callbacks: MeasureCallback.CallbackListType = "default",
    task_scheduler: TaskScheduler.TaskSchedulerType = "gradient",
    space: SpaceGenerator.SpaceGeneratorType = "post-order-apply",
    strategy: SearchStrategy.SearchStrategyType = "evolutionary",
    seed: int | None = None,
    module_equality: str = "structural",
) -> Database:
    """Interface with tuning api to tune a Relax program.

    Parameters
    ----------
    mod : Union[IRModule, relax.Function]
        The module or function to tune
    params : Optional[Dict[str, tvm.runtime.Tensor]]
        The associated parameters of the program
    target : Union[Target, str]
        The compilation target
    work_dir : str
        The working directory to store the tuning records
    max_trials_global : int
        The maximum number of trials to run
    max_trials_per_task : Optional[int]
        The maximum number of trials to run for each task
    op_names: Optional[List[str]]
        A list of operator names to specify which op to tune. When it is None, all operators
        are tuned.
    num_trials_per_iter : int
        The number of trials to run per iteration
    builder : BuilderType
        The builder to use
    runner : RunnerType
        The runner to use
    database : DatabaseType
        The database to use
    cost_model : CostModelType
        The cost model to use
    measure_callbacks : CallbackListType
        The measure callbacks to use
    task_scheduler : TaskSchedulerType
        The task scheduler to use
    space : SpaceGeneratorType
        The space generator to use
    strategy : SearchStrategyType
        The search strategy to use
    seed : Optional[int]
        The random seed
    module_equality : Optional[str]
        A string to specify the module equality testing and hashing method.
        It must be one of the followings:
          - "structural": Use StructuralEqual/Hash
          - "ignore-tensor": Same as "structural", but ignore tensor raw data during
                              equality testing and hashing.
          - "anchor-block": Apply equality testing and hashing on the anchor block extracted from a
                            given module. The "ignore-tensor" varint is used for the extracted
                            blocks or in case no anchor block is found.
                            For the definition of the anchor block, see tirx/analysis/analysis.py.

    Returns
    -------
    ret_mod : IRModule
        IRModule
    """
    if isinstance(max_trials_global, IntImm):
        max_trials_global = int(max_trials_global)
    if isinstance(max_trials_per_task, IntImm):
        max_trials_per_task = int(max_trials_per_task)

    tune_relax(
        mod,
        params,
        target,
        work_dir,
        max_trials_global,
        max_trials_per_task=max_trials_per_task,
        num_trials_per_iter=num_trials_per_iter,
        op_names=op_names,
        builder=builder,
        runner=runner,
        database=database,
        cost_model=cost_model,
        measure_callbacks=measure_callbacks,
        task_scheduler=task_scheduler,
        space=space,
        strategy=strategy,
        seed=seed,
        module_equality=module_equality,
    )
    # Return original IRModule
    # This pass only makes optimization decision
    return mod


def compile_relax(
    database: Database,
    mod: IRModule,
    target: Target | str,
    params: dict[str, Tensor] | None,
    enable_warning: bool = False,
) -> "relax.VMExecutable":
    """Compile a relax program with a MetaSchedule database.

    Parameters
    ----------
    database : Database
        The database to use
    mod : IRModule
        The Relax program to be compiled
    target : tvm.target.Target
        The compilation target
    params : Optional[Dict[str, tvm.runtime.Tensor]]
        The associated parameters of the program
    enable_warning : bool
        A boolean value indicating if to print warnings for TIR functions not
        showing up in the database. By default we don't print warning.

    Returns
    -------
    lib : relax.VMExecutable
        The built runtime module or vm VMExecutable for the given relax workload.
    """
    # pylint: disable=import-outside-toplevel
    import tvm
    from tvm import relax
    from tvm.relax import build as relax_build
    from tvm.relax import pipeline as relax_pipeline_mod
    from tvm.relax.transform import BindParams, MetaScheduleApplyDatabase
    from tvm.s_tir import dlight as dl

    # pylint: enable=import-outside-toplevel
    if not isinstance(target, Target):
        target = Target(target)
    if params:
        mod = BindParams("main", params)(mod)

    # Build a pipeline with the correct ordering:
    #   1. library_dispatch + LegalizeOps + FuseOps + FuseTIR
    #      (same preparation as extract_tasks, so database keys match)
    #   2. MetaScheduleApplyDatabase — replaces tuned fused-TIR functions
    #   3. DLight fallback — schedules remaining untuned functions
    #   4. dataflow_lower + finalize passes
    #
    # Applying MetaScheduleApplyDatabase BEFORE FuseOps (the original bug)
    # caused DLight.Matmul to fail on cache-write stages embedded in fused TIR.
    #
    # All pass lists are obtained from relax.pipeline.*_passes(target) so that
    # target-specific helpers (dispatch, finalize) are shared with the default
    # pipeline rather than duplicated here.
    try:
        dispatch_passes = relax_pipeline_mod.library_dispatch_passes(target)
    except (ValueError, AttributeError):
        dispatch_passes = []

    try:
        lower_passes = relax_pipeline_mod.dataflow_lower_passes(target)
        finalize_passes = relax_pipeline_mod.finalize_passes(target)
    except (ValueError, AttributeError):
        # Fallback for targets not yet registered in the pipeline dispatcher
        lower_passes = [
            relax.transform.RewriteDataflowReshape(),
            relax.transform.ToNonDataflow(),
            relax.transform.RemovePurityChecking(),
            relax.transform.CallTIRRewrite(),
        ]
        finalize_passes = [
            relax.transform.StaticPlanBlockMemory(),
            relax.transform.LowerAllocTensor(),
            relax.transform.KillAfterLastUse(),
            relax.transform.LowerRuntimeBuiltin(),
            relax.transform.ComputePrimValue(),
            relax.transform.VMShapeLower(),
            relax.transform.AttachGlobalSymbol(),
        ]

    is_gpu_target = relax_pipeline_mod.BackendDispatcher.is_gpu_target(target)

    @tvm.transform.module_pass(opt_level=3)
    def _ms_pipeline(mod: tvm.ir.IRModule, _ctx: tvm.transform.PassContext) -> tvm.ir.IRModule:
        fuse_seq = dispatch_passes + [
            relax.transform.LegalizeOps(enable_warning=enable_warning),
            relax.transform.AnnotateTIROpPattern(),
            relax.transform.FoldConstant(),
            relax.transform.FuseOps(),
            relax.transform.FuseTIR(),
        ]
        mod = tvm.transform.Sequential(fuse_seq)(mod)
        mod = MetaScheduleApplyDatabase(enable_warning=enable_warning)(mod)
        # DLight handles functions not covered by the database.
        # GPU rules apply only for GPU targets.
        if is_gpu_target:
            mod = dl.ApplyDefaultSchedule(
                dl.gpu.Matmul(),
                dl.gpu.GEMV(),
                dl.gpu.Reduction(),
                dl.gpu.GeneralReduction(),
                dl.gpu.Fallback(),
            )(mod)
        mod = tvm.transform.Sequential(lower_passes + finalize_passes)(mod)
        return mod

    with target, database, PassContext(opt_level=3):
        relax_ex = relax_build(mod, target=target, relax_pipeline=_ms_pipeline)
    return relax_ex
