# 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.
"""Creation operators."""

from tvm import DataType, DataTypeCode
from tvm.ir import PrimType, is_prim_expr

from ..expr import Expr, ShapeExpr, prim_value
from . import _ffi_api

PrimExprLike = int | Expr


def _raw_dtype(dtype):
    return dtype.dtype if isinstance(dtype, PrimType) else dtype


def _normalize_shape(shape):
    if isinstance(shape, tuple | list):
        return ShapeExpr(shape)
    if not isinstance(shape, Expr) or is_prim_expr(shape):
        raise TypeError("shape must be a tuple/list or a Relax shape expression")
    return shape


def full(
    shape: tuple[PrimExprLike] | Expr,
    fill_value: Expr,
    dtype: str | DataType | None = None,
) -> Expr:
    """Fill array with scalar value.

    Parameters
    ----------
    shape : Union[Tuple[PrimExprLike], Expr]
        The shape of the created tensor.

    fill_value : relax.Expr
        The value to fill. Must be a scalar tensor.

    dtype : Optional[str | DataType]
        The data type of the created tensor.
        If dtype is not given, it will by default use the dtype of fill_value.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    shape = _normalize_shape(shape)
    return _ffi_api.full(shape, fill_value, _raw_dtype(dtype))  # type: ignore


def full_like(x: Expr, fill_value: Expr, dtype: str | DataType | None = None) -> Expr:
    """Construct a tensor such that
    - its shape is the same as the input data tensor's shape,
    - its value is filled with the input scalar fill value.

    Parameters
    ----------
    x : relax.Expr
        The input tensor, which provides the shape, and dtype
        when the `dtype` field is not specified.

    fill_value : relax.Expr
        The value to fill. Must be a scalar tensor.

    dtype : Optional[str | DataType]
        The data type of the created tensor.
        If dtype is not given, it will by default use the dtype of the input tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    return _ffi_api.full_like(x, fill_value, _raw_dtype(dtype))  # type: ignore


def ones(shape: tuple[PrimExprLike] | Expr, dtype: str | DataType) -> Expr:
    """Construct a tensor of all ones, with the input shape and dtype.

    Parameters
    ----------
    shape : Union[Tuple[PrimExprLike], Expr]
        The shape of the created tensor.

    dtype : str | DataType
        The data type of the created tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    shape = _normalize_shape(shape)
    return _ffi_api.ones(shape, _raw_dtype(dtype))  # type: ignore


def ones_like(x: Expr, dtype: str | DataType | None = None) -> Expr:
    """Construct a tensor with all ones, with shape of the input tensor shape.

    Parameters
    ----------
    x : relax.Expr
        The input tensor, which provides the shape, and dtype
        when the `dtype` field is not specified.

    dtype : Optional[str | DataType]
        The data type of the created tensor.
        If dtype is not given, it will by default use the dtype of the input tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    return _ffi_api.ones_like(x, _raw_dtype(dtype))  # type: ignore


def zeros(shape: tuple[PrimExprLike] | Expr, dtype: str | DataType) -> Expr:
    """Construct a tensor of all zeros, with the input shape and dtype.

    Parameters
    ----------
    shape : Union[Tuple[PrimExprLike], Expr]
        The shape of the created tensor.

    dtype : str | DataType
        The data type of the created tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    shape = _normalize_shape(shape)
    return _ffi_api.zeros(shape, _raw_dtype(dtype))  # type: ignore


def zeros_like(x: Expr, dtype: str | DataType | None = None) -> Expr:
    """Construct a tensor with all zeros, with shape of the input tensor shape.

    Parameters
    ----------
    x : relax.Expr
        The input tensor, which provides the shape, and dtype
        when the `dtype` field is not specified.

    dtype : Optional[str | DataType]
        The data type of the created tensor.
        If dtype is not given, it will by default use the dtype of the input tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    return _ffi_api.zeros_like(x, _raw_dtype(dtype))  # type: ignore


def eye(
    n: PrimExprLike,
    m: PrimExprLike | None = None,
    k: PrimExprLike = 0,
    dtype: str | DataType = "float32",
) -> Expr:
    """Construct a 2-D tensor with ones on the diagonal and zeros elsewhere.

    Parameters
    ----------
    n : PrimExprLike
        Number of rows in the output.

    m : Optional[PrimExprLike]
        Number of columns in the output. If None, defaults to n.

    k : PrimExprLike
        Index of the diagonal: 0 (the default) refers to the main diagonal,
        a positive value refers to an upper diagonal, and a negative value
        to a lower diagonal.

    dtype : str | DataType
        The data type of the created tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    m = n if m is None else m
    n = prim_value(n)
    m = prim_value(m)
    k = prim_value(k)
    return _ffi_api.eye(n, m, k, _raw_dtype(dtype))  # type: ignore


def eye_like(
    x: Expr,
    k: PrimExprLike = 0,
    dtype: str | DataType | None = None,
) -> Expr:
    """Return a 2-D tensor with ones on the diagonal and zeros elsewhere,
    with the same shape as the input tensor.

    Parameters
    ----------
    x : relax.Expr
        The input tensor, which provides the shape, and dtype
        when the `dtype` field is not specified.

    k : PrimExprLike
        Index of the diagonal: 0 (the default) refers to the main diagonal,
        a positive value refers to an upper diagonal, and a negative value
        to a lower diagonal.

    dtype : Optional[str | DataType]
        The data type of the created tensor.
        If dtype is not given, it will by default use the dtype of the input tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    k = prim_value(k)
    return _ffi_api.eye_like(x, k, _raw_dtype(dtype))  # type: ignore


def arange(
    start: PrimExprLike,
    end: PrimExprLike | None = None,
    step: PrimExprLike = 1,
    dtype: str | DataType | PrimType | None = None,
) -> Expr:
    """Construct a tensor with evenly spaced elements.

    Parameters
    ----------
    start : PrimExprLike
        The start of the interval.

    end : Optional[PrimExprLike]
        The end of the interval. If not given, it will be set to start,
        and start will be set to 0.

    step : PrimExprLike
        The step size.

    dtype : Optional[str | DataType | PrimType]
        The data type of the created tensor.

    Returns
    -------
    result : relax.Expr
        The result tensor.
    """
    if end is None:
        end = start
        start = 0

    def is_int(expr):
        if isinstance(expr, int):
            return True
        if is_prim_expr(expr):
            return expr.ty.matches_code(DataTypeCode.INT)
        return False

    if dtype is None:
        args = (start, end, step)
        integer_args = all(is_int(arg) for arg in args)
        dtype = "int64" if integer_args else "float32"

    start = prim_value(start)
    end = prim_value(end)
    step = prim_value(step)
    return _ffi_api.arange(start, end, step, _raw_dtype(dtype))  # type: ignore


def hamming_window(window_size, periodic, alpha, beta, dtype):
    """Hamming window function.

    Parameters
    ----------
    window_size : Expr
        The size of returned window.

    periodic : Expr
        If True, returns a window to be used as periodic function.
        If False, return a symmetric window.

    alpha : Expr
        The co-efficient alpha.

    beta : Expr
        The co-efficient beta.

    Returns
    -------
    ret : relax.Expr
        The result tensor.
    """
    if not is_prim_expr(window_size):
        window_size = prim_value(window_size)
    if not is_prim_expr(periodic):
        periodic = prim_value(periodic)
    if not is_prim_expr(alpha):
        alpha = prim_value(alpha)
    if not is_prim_expr(beta):
        beta = prim_value(beta)

    return _ffi_api.hamming_window(window_size, periodic, alpha, beta, dtype)


def tril(x: Expr, k: int | Expr = 0) -> Expr:
    """Return the lower triangular part of a matrix or a batch of matrices.

    Parameters
    ----------
    x : relax.Expr
        The tensor that tril will be applied to.
        It is required to have at least two dimensions.

    k : int
        The index indicating the diagonal above which to zero elements.
        If k = 0, the diagonal is the main diagonal.
        If k < 0, the diagonal is below the main diagonal.
        If k > 0, the diagonal is above the main diagonal.

    Returns
    -------
    ret : relax.Expr
        The result tensor.
    """
    if not is_prim_expr(k):
        k = prim_value(k)

    return _ffi_api.tril(x, k)  # type: ignore


def triu(x: Expr, k: int | Expr = 0) -> Expr:
    """Return the upper triangular part of a matrix or a batch of matrices.

    Parameters
    ----------
    x : relax.Expr
        The tensor that triu will be applied to.
        It is required to have at least two dimensions.

    k : int
        The index indicating the diagonal below which to zero elements.
        If k = 0, the diagonal is the main diagonal.
        If k < 0, the diagonal is below the main diagonal.
        If k > 0, the diagonal is above the main diagonal.

    Returns
    -------
    ret : relax.Expr
        The result tensor.
    """
    if not is_prim_expr(k):
        k = prim_value(k)

    return _ffi_api.triu(x, k)  # type: ignore
