# 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.
"""Runners"""
from typing import Callable, List, Optional, Union

# isort: off
from typing_extensions import Literal

# isort: on

from tvm._ffi import register_object
from tvm.runtime import Object

from .. import _ffi_api
from ..arg_info import ArgInfo


@register_object("meta_schedule.RunnerInput")
class RunnerInput(Object):
    """The runner's input

    Parameters
    ----------
    artifact_path : str
        The path to the built artifact.
    device_type : str
        The device type.
    args_info : List[ArgInfo]
        The argument information.
    """

    artifact_path: str
    device_type: str
    args_info: List[ArgInfo]

    def __init__(
        self,
        artifact_path: str,
        device_type: str,
        args_info: List[ArgInfo],
    ) -> None:
        """Constructor

        Parameters
        ----------
        artifact_path : str
            The path to the built artifact.
        device_type : str
            The device type.
        args_info : List[ArgInfo]
            The argument information.
        """
        self.__init_handle_by_constructor__(
            _ffi_api.RunnerInput,  # type: ignore # pylint: disable=no-member
            artifact_path,
            device_type,
            args_info,
        )


@register_object("meta_schedule.RunnerResult")
class RunnerResult(Object):
    """The runner's result

    Parameters
    ----------
    run_secs : Optional[List[float]]
        The run time in seconds.
    error_msg : Optional[str]
        The error message, if any.
    """

    run_secs: Optional[List[float]]
    error_msg: Optional[str]

    def __init__(
        self,
        run_secs: Optional[List[float]],
        error_msg: Optional[str],
    ) -> None:
        """Constructor

        Parameters
        ----------
        run_secs : Optional[List[float]]
            The run time in seconds.
        error_msg : Optional[str]
            The error message, if any.
        """
        self.__init_handle_by_constructor__(
            _ffi_api.RunnerResult,  # type: ignore # pylint: disable=no-member
            run_secs,
            error_msg,
        )


@register_object("meta_schedule.RunnerFuture")
class RunnerFuture(Object):
    """
    A class to fetch asynchronous runner's output.
    This is NOT the user facing class for function overloading inheritance.
    Can be used for general return type of runner.

    See also: PyRunnerFuture
    """

    def __init__(self, f_done: Callable, f_result: Callable = None) -> None:
        """Constructor"""

        self.__init_handle_by_constructor__(
            _ffi_api.RunnerFuture,  # type: ignore # pylint: disable=no-member
            f_done,
            f_result,
        )

    def done(self) -> bool:
        """Check whether the runner has finished."""
        return _ffi_api.RunnerFutureDone(self)  # type: ignore # pylint: disable=no-member

    def result(self) -> RunnerResult:
        """Fetch the runner's output if it is ready."""
        return _ffi_api.RunnerFutureResult(self)  # type: ignore # pylint: disable=no-member


class PyRunnerFuture:
    """
    A class to fetch asynchronous runner's output with customizable function on the python side.
    This is the user facing class for function overloading inheritance.
    Can NOT be used for general return type of runner.

    Note: @derived_object is required for proper usage of any inherited class.
    Example:
        @derived_object
        def LocalRunnerFuture(PyRunnerFuture):
            ...
    """

    _tvm_metadata = {
        "cls": RunnerFuture,
        "methods": ["done", "result"],
    }

    def done(self) -> bool:
        """Check whether the runner has finished."""
        raise NotImplementedError

    def result(self) -> RunnerResult:
        """Fetch the runner's output if it is ready."""
        raise NotImplementedError


@register_object("meta_schedule.Runner")
class Runner(Object):
    """The abstract runner interface"""

    RunnerType = Union["Runner", Literal["local", "rpc"]]

    def run(self, runner_inputs: List[RunnerInput]) -> List[RunnerFuture]:
        """Run the built artifact and get runner futures.

        Parameters
        ----------
        runner_inputs : List[RunnerInput]
            The inputs to the runner.

        Returns
        -------
        runner_futures: List[RunnerFuture]
            The runner futures.
        """
        return _ffi_api.RunnerRun(self, runner_inputs)  # type: ignore # pylint: disable=no-member

    @staticmethod
    def create(  # pylint: disable=keyword-arg-before-vararg
        kind: Literal["local", "rpc"] = "local",
        *args,
        **kwargs,
    ) -> "Runner":
        """Create a Runner."""
        from . import LocalRunner, RPCRunner  # pylint: disable=import-outside-toplevel

        if kind == "local":
            if "max_workers" in kwargs:
                kwargs.pop("max_workers")
            return LocalRunner(*args, **kwargs)  # type: ignore
        elif kind == "rpc":
            return RPCRunner(*args, **kwargs)  # type: ignore
        raise ValueError(f"Unknown Runner: {kind}")


create = Runner.create  # pylint: disable=invalid-name


@register_object("meta_schedule.PyRunner")
class _PyRunner(Runner):
    """
    A TVM object runner to support customization on the python side.
    This is NOT the user facing class for function overloading inheritance.

    See also: PyRunner
    """

    def __init__(self, f_run: Callable = None) -> None:
        """Constructor"""

        self.__init_handle_by_constructor__(
            _ffi_api.RunnerPyRunner,  # type: ignore # pylint: disable=no-member
            f_run,
        )


class PyRunner:
    """
    An abstract runner with customized run method on the python-side.
    This is the user facing class for function overloading inheritance.

    Note: @derived_object is required for proper usage of any inherited class.
    """

    _tvm_metadata = {
        "cls": _PyRunner,
        "methods": ["run"],
    }

    def run(self, runner_inputs: List[RunnerInput]) -> List[RunnerFuture]:
        """Run the built artifact and get runner futures.

        Parameters
        ----------
        runner_inputs : List[RunnerInput]
            The inputs to the runner.

        Returns
        -------
        runner_futures: List[RunnerFuture]
            The runner futures.
        """
        raise NotImplementedError
