# 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.
# pylint: disable=invalid-name
# ruff: noqa: RUF012
"""A builder to build Relax VM executable."""

from enum import IntEnum

import tvm_ffi
from tvm_ffi import Shape

import tvm

from . import _ffi_api
from .vm_build import VMExecutable


class SpecialReg(IntEnum):
    """Magic numbers that represent special registers in vm."""

    VOID_ARG = (1 << 54) + 0
    VM_STATE = (1 << 54) + 1


class VMFuncKind(IntEnum):
    """VM function kind code."""

    PACKED_FUNC = 0
    VM_FUNC = 1


class VMFuncScope:
    """An object corresponds to each VM function, working as a context manager."""

    stack: list["VMFuncScope"] = []

    def __init__(self, exit_callback):
        self.exit_callback = exit_callback

    def __enter__(self):
        VMFuncScope.stack.append(self)
        return self

    def __exit__(self, ptype, value, trace):
        VMFuncScope.stack.pop()
        self.exit_callback()


@tvm_ffi.register_object("relax.ExecBuilder")
class ExecBuilder(tvm_ffi.core.Object):
    """A builder to emit instructions and build executable for the virtual machine."""

    def __init__(self) -> None:
        self.__init_handle_by_constructor__(_ffi_api.ExecBuilderCreate)  # type: ignore

    def r(self, idx: int) -> int:
        """set instruction's argument as a register."""
        return _ffi_api.ExecBuilderR(self, idx)  # type: ignore

    def imm(self, value: int) -> int:
        """set instruction's argument as an immediate."""
        return _ffi_api.ExecBuilderImm(self, value)  # type: ignore

    def c(self, idx: int) -> int:
        """set instruction's argument as a constant."""
        return _ffi_api.ExecBuilderC(self, idx)  # type: ignore

    def f(self, name: str) -> int:
        """set instruction's argument as a function."""
        return _ffi_api.ExecBuilderF(self, name)  # type: ignore

    def void_arg(self) -> int:
        return self.r(SpecialReg.VOID_ARG)

    def vm_state(self) -> int:
        return self.r(SpecialReg.VM_STATE)

    def declare_function(self, func_name: str, kind: VMFuncKind = VMFuncKind.PACKED_FUNC) -> None:
        """Declare a function"""
        _ffi_api.ExecBuilderDeclareFunction(self, func_name, kind)  # type: ignore

    def function(
        self, func_name: str, num_inputs: int | None = 0, param_names: list[str] | None = None
    ) -> VMFuncScope:
        """annotate a VM function."""
        _ffi_api.ExecBuilderEmitFunction(self, func_name, num_inputs, param_names)  # type: ignore
        return VMFuncScope(lambda: _ffi_api.ExecBuilderEndFunction(self, func_name))  # type: ignore

    def _check_scope(self) -> None:
        if len(VMFuncScope.stack) == 0:
            raise ValueError("emit should happen in a function scope")

    def convert_constant(self, const: object) -> int:
        return _ffi_api.ExecBuilderConvertConstant(self, const)  # type: ignore

    def emit_call(
        self,
        name: str,
        args: list[tvm.runtime.Tensor | tvm.DataType] | None = None,
        dst: int | None = None,
    ) -> None:
        """emit a call instruction which calls a packed function."""
        self._check_scope()
        if dst is None:
            dst = SpecialReg.VOID_ARG
        args_ = []
        if args is not None:
            for arg in args:
                if isinstance(arg, tuple):
                    shape_tuple = Shape(arg)
                    new_arg = self.convert_constant(shape_tuple)
                    args_.append(new_arg)
                elif isinstance(arg, tvm.runtime.Tensor | tvm.DataType | Shape):
                    new_arg = self.convert_constant(arg)
                    args_.append(new_arg)
                else:
                    args_.append(arg)
        _ffi_api.ExecBuilderEmitCall(self, name, args_, dst)  # type: ignore

    def emit_ret(self, result: int) -> None:
        """emit a return instruction"""
        self._check_scope()
        _ffi_api.ExecBuilderEmitRet(self, result)  # type: ignore

    def emit_goto(self, pc_offset):
        """emit a goto instruction"""
        self._check_scope()
        _ffi_api.ExecBuilderEmitGoto(self, pc_offset)  # type: ignore

    def emit_if(self, cond, false_offset):
        """emit an if instruction"""
        self._check_scope()
        _ffi_api.ExecBuilderEmitIf(self, cond, false_offset)  # type: ignore

    def get(self) -> VMExecutable:
        """return the executable"""
        return VMExecutable(_ffi_api.ExecBuilderGet(self))  # type: ignore
