# -*- coding: utf-8 -*-
# MegEngine is Licensed under the Apache License, Version 2.0 (the "License")
#
# Copyright (c) 2014-2021 Megvii Inc. All rights reserved.
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT ARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
import math
from typing import Iterable, Optional, Sequence, Union

import numpy as np

from ..core._imperative_rt import CompNode
from ..core._imperative_rt.core2 import SymbolVar, apply
from ..core._wrap import device as as_device
from ..core.ops import builtin
from ..core.ops.builtin import Copy, Identity
from ..core.ops.special import Const
from ..core.tensor.array_method import _broadcast, _remove_axis
from ..core.tensor.utils import (
    astensor1d,
    convert_inputs,
    convert_single_value,
    dtype_promotion,
    get_device,
)
from ..device import get_default_device
from ..tensor import Tensor
from .elemwise import ceil, floor_div

__all__ = [
    "arange",
    "broadcast_to",
    "concat",
    "cond_take",
    "expand_dims",
    "eye",
    "flatten",
    "full",
    "full_like",
    "gather",
    "linspace",
    "ones",
    "ones_like",
    "repeat",
    "reshape",
    "split",
    "squeeze",
    "stack",
    "scatter",
    "tile",
    "copy",
    "transpose",
    "where",
    "zeros",
    "zeros_like",
]


def eye(N, M=None, *, dtype="float32", device: Optional[CompNode] = None) -> Tensor:
    """
    Returns a 2D tensor with ones on the diagonal and zeros elsewhere.

    :param shape: expected shape of output tensor.
    :param dtype: data type. Default: None
    :param device: compute node of the matrix. Default: None
    :return: eye matrix.

    Examples:

    .. testcode::

        import numpy as np
        import megengine.functional as F

        out = F.eye(4, 6, dtype=np.float32)
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[1. 0. 0. 0. 0. 0.]
         [0. 1. 0. 0. 0. 0.]
         [0. 0. 1. 0. 0. 0.]
         [0. 0. 0. 1. 0. 0.]]

    """
    if M is not None:
        if isinstance(N, Tensor) or isinstance(M, Tensor):
            shape = astensor1d((N, M))
        else:
            shape = Tensor([N, M], dtype="int32", device=device)
    elif isinstance(N, Tensor):
        shape = N
    else:
        shape = Tensor(N, dtype="int32", device=device)
    op = builtin.Eye(k=0, dtype=dtype, comp_node=device)
    (result,) = apply(op, shape)
    return result


def full(shape, value, dtype="float32", device=None) -> Tensor:
    """
    Returns a tensor with given shape and value.
    """
    if isinstance(shape, int):
        shape = (shape,)
    if device is None:
        device = get_default_device()
    (x,) = Const(value, dtype=dtype, device=device)()
    if shape is ():  # scalar.shape
        return x
    return broadcast_to(x, shape)


def ones(shape, dtype="float32", device=None) -> Tensor:
    """
    Returns a ones tensor with given shape.

    :param inp: input tensor.
    :return: output zero tensor.

    Examples:

    .. testcode::

        import megengine.functional as F

        out = F.ones((2, 1))
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[1.]
         [1.]]

    """
    return full(shape, 1.0, dtype=dtype, device=device)


def zeros(shape, dtype="float32", device=None) -> Tensor:
    """
    Returns a zero tensor with given shape.
    """
    return full(shape, 0.0, dtype=dtype, device=device)


def zeros_like(inp: Union[Tensor, SymbolVar]) -> Union[Tensor, SymbolVar]:
    """
    Returns a zero tensor with the same shape as input tensor.

    :param inp: input tensor.
    :return: output zero tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        inp = tensor(np.arange(1, 7, dtype=np.int32).reshape(2,3))
        out = F.zeros_like(inp)
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[0 0 0]
         [0 0 0]]

    """
    return full_like(inp, 0.0)


def ones_like(inp: Union[Tensor, SymbolVar]) -> Union[Tensor, SymbolVar]:
    """
    Returns a ones tensor with the same shape as input tensor.
    """
    return full_like(inp, 1.0)


def full_like(
    inp: Union[Tensor, SymbolVar], value: Union[int, float]
) -> Union[Tensor, SymbolVar]:
    """
    Returns a tensor filled with given value with the same shape as input tensor.
    """
    (x,) = Const(value, dtype=inp.dtype, device=inp.device)(inp)
    if inp.shape is ():
        return x
    return broadcast_to(x, inp.shape)


def broadcast_to(inp: Tensor, shape: Union[int, Iterable[int]]) -> Tensor:
    """
    Broadcasts a tensor to given shape.

    :param inp: input tensor.
    :param shape: target shape.
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        data = tensor(np.arange(0, 3, dtype=np.float32).reshape(3))
        out = F.broadcast_to(data, (2, 3))
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[0. 1. 2.]
         [0. 1. 2.]]

    """
    return _broadcast(inp, shape)


def concat(inps: Iterable[Tensor], axis: int = 0, device=None) -> Tensor:
    r"""
    Concat some tensors

    :param inps: input tensors to concat.
    :param axis: over which dimension the tensors are concatenated. Default: 0
    :param device: which device output will be. Default: None
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        data1 = tensor(np.arange(0, 6, dtype=np.float32).reshape((2, 3)))
        data2 = tensor(np.arange(6, 12, dtype=np.float32).reshape((2, 3)))
        out = F.concat([data1, data2])
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[ 0.  1.  2.]
         [ 3.  4.  5.]
         [ 6.  7.  8.]
         [ 9. 10. 11.]]

    """
    if len(inps) == 1:
        return inps[0]

    inps = convert_inputs(*inps, device=device)
    if device is None:
        device = get_device(inps)
    device = as_device(device)
    (result,) = apply(builtin.Concat(axis=axis, comp_node=device.to_c()), *inps)
    return result


def stack(inps, axis=0, device=None):
    """
    Concats a sequence of tensors along a new axis.
    The input tensors must have the same shape.

    :param inps: input tensors.
    :param axis: which axis will be concatenated.
    :param device: the device output will be. Default: None
    :return: output concatenated tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        x1 = tensor(np.arange(0, 3, dtype=np.float32).reshape((3)))
        x2 = tensor(np.arange(6, 9, dtype=np.float32).reshape((3)))
        out = F.stack([x1, x2], axis=0)
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[0. 1. 2.]
         [6. 7. 8.]]

    """
    if len(inps) > 0 and not isinstance(inps[0].shape, inps[0].__class__):
        shapes = {arr.shape for arr in inps}
        if len(shapes) != 1:
            raise ValueError("All input tensors must have the same shape")

    inps = [expand_dims(inp, axis=axis) for inp in inps]
    return concat(inps, axis=axis, device=device)


def split(inp, nsplits_or_sections, axis=0):
    """
    Splits the input tensor into several smaller tensors.
    When nsplits_or_sections is int, the last tensor may be smaller than others.

    :param inp: input tensor.
    :param nsplits_or_sections: number of sub tensors or sections information list.
    :param axis: which axis will be splited.
    :return: output tensor list.

    Examples:

    .. testcode::

        import os
        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        x = tensor(np.random.random((10, 20)), dtype=np.float32)
        y = F.split(x, 3)
        z = F.split(x, [6, 17], axis=1)

        print([i.numpy().shape for i in y])
        print([i.numpy().shape for i in z])

    Outputs:

    .. testoutput::

        [(4, 20), (3, 20), (3, 20)]
        [(10, 6), (10, 11), (10, 3)]

    """
    ndim = len(inp.shape)
    if axis >= ndim:
        raise ValueError("Invalid axis {}".format(axis))

    Ntotal = inp.shape[axis]

    try:
        Nsections = len(nsplits_or_sections) + 1
        is_array = True
    except TypeError:
        Nsections = int(nsplits_or_sections)
        is_array = False

    if is_array:
        div_points = [0] + list(nsplits_or_sections) + [Ntotal]
        for i in range(1, len(div_points)):
            if div_points[i - 1] >= div_points[i]:
                raise ValueError(
                    "Invalid nsplits_or_secions: {}".format(nsplits_or_sections)
                )
    else:  # scalar
        if Nsections <= 0:
            raise ValueError("Number sections must be larger than 0")
        if Nsections > Ntotal:
            raise ValueError(
                "The size {} at dim {} cannot be split into {} sections".format(
                    Ntotal, axis, Nsections
                )
            )

        func = (
            floor_div
            if isinstance(Nsections, (SymbolVar, Tensor))
            else lambda x, y: x // y
        )
        div_points = [0] + [
            func(Ntotal + Nsections - i - 1, Nsections) for i in range(Nsections)
        ]
        for i in range(2, Nsections + 1):
            div_points[i] = div_points[i - 1] + div_points[i]

    sub_tensors = []
    for i in range(Nsections):
        l = div_points[i]
        r = div_points[i + 1]
        slices = tuple(
            [slice(None)] * axis + [slice(l, r)] + [slice(None)] * (ndim - axis - 1)
        )
        sub_tensors.append(inp[slices])
    return sub_tensors


def _get_idx(index, axis):
    index_dims = len(index.shape)
    idx = []
    for i in range(index_dims):
        if i != axis:
            shape = [1] * index_dims
            shape[i] = index.shape[i]
            arange = linspace(
                0, index.shape[i] - 1, index.shape[i], device=index.device,
            )
            arange = (
                broadcast_to(arange.reshape(*shape), index.shape)
                .reshape(-1)
                .astype(np.int32)
            )
            idx.append(arange)
        else:
            idx.append(index.reshape(-1))
    return tuple(idx)


def gather(inp: Tensor, axis: int, index: Tensor) -> Tensor:
    # TODO: rewrite doc
    r"""
    Gathers data from input tensor on axis using index.

    For a 3-D tensor, the output is specified by::

        out[i][j][k] = inp[index[i][j][k]][j][k] # if axis == 0
        out[i][j][k] = inp[i][index[i][j][k]][k] # if axis == 1
        out[i][j][k] = inp[i][j][index[i][j][k]] # if axis == 2

    if input tensor is a n-dimensional tensor with size
    :math:`(x_0,x_1,...,x_{i-1},x_i,x_{i+1},...,x_{n-1})` and axis=i,
    then index must be a n-dimensional tensor with size
    :math:`(x_0,x_1,...,x_{i-1},y,x_{i+1},...,x_{n-1})` where :math:`y\ge 1` and
    output will have the same size as index.

    :param inp: input tensor.
    :param axis: along which axis to index.
    :param index: indices of elements to gather.
    :return: output tensor.

    Examples:

    .. testcode::

        import megengine.functional as F
        from megengine import tensor

        inp = tensor([
            [1,2], [3,4], [5,6],
        ])
        index = tensor([[0,2], [1,0]])
        oup = F.gather(inp, 0, index)
        print(oup.numpy())

    Outputs:

    .. testoutput::

        [[1 6]
         [3 2]]

    """
    input_shape = inp.shape
    index_shape = index.shape
    input_dims = len(input_shape)
    index_dims = len(index_shape)
    if input_dims != index_dims:
        raise ValueError(
            "The index tensor must have same dimensions as input tensor, "
            "But the input dims:{}, the index dims:{}".format(input_dims, index_dims)
        )

    if axis < 0 or axis >= input_dims:
        raise ValueError(
            "Index axis {} is output of bounds, should in range [0 {})".format(
                axis, input_dims
            )
        )

    for i in range(input_dims):
        if i != axis and input_shape[i] != index_shape[i]:
            raise ValueError(
                "The input {} and index {} must have the same size apart from axis {}".format(
                    input_shape, index_shape, axis
                )
            )

    idx = _get_idx(index, axis)
    return inp[idx].reshape(index.shape)  # pylint: disable=no-member


def scatter(inp: Tensor, axis: int, index: Tensor, source: Tensor) -> Tensor:
    # TODO: rewrite doc
    r"""
    Writes all values from the tensor source into input tensor
    at the indices specified in the index tensor.

    For each value in source, its output index is specified by its index
    in source for ``axis != dimension`` and by the corresponding value in
    index for ``axis = dimension``.

    For a 3-D tensor, input tensor is updated as::

        inp[index[i][j][k]][j][k] = source[i][j][k]  # if axis == 0
        inp[i][index[i][j][k]][k] = source[i][j][k]  # if axis == 1
        inp[i][j][index[i][j][k]] = source[i][j][k]  # if axis == 2

    ``inp``, ``index`` and ``source`` should have same number of dimensions.

    It is also required that ``source.shape(d) <= inp.shape(d)`` and ``index.shape(d) == source.shape(d)``
    for all dimensions ``d``.

    Moreover, the values of index must be between ``0`` and ``inp.shape(axis) - 1`` inclusive.

    .. note::
        Please notice that, due to performance issues, the result is uncertain on the GPU device
        if scattering different positions from source to the same destination position
        regard to index tensor.

        Check the following examples, the oup[0][2] is maybe
        from source[0][2] which value is 0.2256 or source[1][2] which value is 0.5339
        if set the index[1][2] from 1 to 0.

    :param inp: inp tensor which to be scattered.
    :param axis: axis along which to index.
    :param index: indices of elements to scatter.
    :param source: source element(s) to scatter.
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        import megengine.functional as F
        from megengine import tensor

        inp = tensor(np.zeros(shape=(3,5),dtype=np.float32))
        source = tensor([[0.9935,0.9465,0.2256,0.8926,0.4396],[0.7723,0.0718,0.5939,0.357,0.4576]])
        index = tensor([[0,2,0,2,1],[2,0,1,1,2]])
        oup = F.scatter(inp, 0, index,source)
        print(oup.numpy())

    Outputs:

    .. testoutput::

        [[0.9935 0.0718 0.2256 0.     0.    ]
         [0.     0.     0.5939 0.357  0.4396]
         [0.7723 0.9465 0.     0.8926 0.4576]]

    """
    input_shape = inp.shape
    index_shape = index.shape
    source_shape = source.shape
    input_dims = len(input_shape)
    index_dims = len(index_shape)
    source_dims = len(source_shape)

    if input_dims != index_dims or input_dims != source_dims:
        raise ValueError("The input, source and index tensor must have same dimensions")

    if axis < 0 or axis >= input_dims:
        raise ValueError(
            "Index axis {} is output of bounds, should in range [0 {})".format(
                axis, input_dims
            )
        )

    for i in range(source_dims):
        if source_shape[i] > input_shape[i]:
            raise ValueError(
                "The each shape size for source {} must be less than or equal to input {} ".format(
                    source_shape, input_shape
                )
            )

    for i in range(index_dims):
        if index_shape[i] != source_shape[i]:
            raise ValueError(
                "The each shape size for index {} must be equal to source {} ".format(
                    index_shape, source_shape
                )
            )

    for i in range(index_dims):
        if i != axis and index_shape[i] > input_shape[i]:
            raise ValueError(
                "The index {} must be less than or equal to input {} size apart from axis {}".format(
                    index_shape, input_shape, axis
                )
            )

    idx = _get_idx(index, axis)
    inp[idx] = source.flatten()
    return inp


def where(mask: Tensor, x: Tensor, y: Tensor) -> Tensor:
    r"""
    Selects elements either from Tensor x or Tensor y, according to mask.

    .. math::

        \textrm{out}_i = x_i \textrm{ if } \textrm{mask}_i \textrm{ is True else } y_i

    :param mask: a mask used for choosing ``x`` or ``y``.
    :param x: first choice.
    :param y: second choice.
    :return: output tensor.

    Examples:

    .. testcode::

        from megengine import tensor
        import megengine.functional as F
        mask = tensor(np.array([[True, False], [False, True]], dtype=np.bool))
        x = tensor(np.array([[1, np.inf], [np.nan, 4]],
            dtype=np.float32))
        y = tensor(np.array([[5, 6], [7, 8]], dtype=np.float32))
        out = F.where(mask, x, y)
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[1. 6.]
         [7. 4.]]
    """

    x, y = convert_inputs(x, y)
    if not isinstance(x, Tensor):
        raise TypeError("input x must be a tensor")
    if not isinstance(y, Tensor):
        raise TypeError("input y must be a tensor")
    if not isinstance(mask, Tensor):
        raise TypeError("mask must be a tensor")
    if mask.dtype != np.bool_:
        raise ValueError("mask must be bool")
    if x.device != mask.device:
        raise ValueError("ambiguous device: {} vs {}".format(x.device, mask.device))

    v0, index0 = cond_take(mask, x)
    v1, index1 = cond_take(~mask, y)

    out = concat([v0, v1])

    out[index0] = v0
    out[index1] = v1
    out = out.reshape(x.shape)
    return out


def cond_take(mask: Tensor, x: Tensor) -> Tensor:
    r"""
    Takes elements from data if specific condition is satisfied on mask.
    This operator has two outputs: the first is the elements taken,
    and the second is the indices corresponding to those elements;
    they are both 1-dimensional. High-dimension input would first be flattened.

    :param mask: condition param; must be the same shape with data.
    :param x: input tensor from which to take elements.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F
        mask = tensor(np.array([[True, False], [False, True]], dtype=np.bool_))
        x = tensor(np.array([[1, np.inf], [np.nan, 4]],
            dtype=np.float32))
        v, index = F.cond_take(mask, x)
        print(v.numpy(), index.numpy())

    Outputs:

    .. testoutput::

        [1. 4.] [0 3]

    """
    if not isinstance(x, (Tensor, SymbolVar)):
        raise TypeError("input must be a tensor")
    if not isinstance(mask, (Tensor, SymbolVar)):
        raise TypeError("mask must be a tensor")
    if mask.dtype != np.bool_:
        raise ValueError("mask must be bool")
    if x.device != mask.device:
        raise ValueError("ambiguous device: {} vs {}".format(x.device, mask.device))

    op = builtin.CondTake()
    v, index = apply(op, x, mask)
    return v, index


def transpose(inp: Tensor, pattern: Iterable[int]) -> Tensor:
    r"""
    Swaps shapes and strides according to given pattern.

    :param inp: input tensor.
    :param pattern: a list of integers including 0, 1, ... , ``ndim``-1,
        and any number of ``'x'`` char in dimensions where this tensor should be broadcasted. For examples:

        * (``'x'``) -> make a 0d (scalar) into a 1d vector
        * (0, 1) -> identity for 2d vectors
        * (1, 0) -> inverts the first and second dimensions
        * (``'x'``, 0) -> make a row out of a 1d vector (N to 1xN)
        * (0, ``'x'``) -> make a column out of a 1d vector (N to Nx1)
        * (2, 0, 1) -> AxBxC to CxAxB
        * (0, ``'x'``, 1) -> AxB to Ax1xB
        * (1, ``'x'``, 0) -> AxB to Bx1xA
        * (1,) -> this removes dimensions 0. It must be a broadcastable dimension (1xA to A)

    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F
        x = tensor(np.array([[1, 1], [0, 0]], dtype=np.int32))
        out = F.transpose(x, (1, 0))
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[1 0]
         [1 0]]

    """
    return inp.transpose(list(-1 if _ == "x" else _ for _ in pattern))


def reshape(inp: Tensor, target_shape: Iterable[int]) -> Tensor:
    r"""
    Reshapes a tensor to given target shape; total number of logical elements must
    remain unchanged

    :param inp: input tensor.
    :param target_shape: target shape, it can contain an element of -1 representing ``unspec_axis``.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F
        x = tensor(np.arange(12, dtype=np.int32))
        out = F.reshape(x, (3, 4))
        print(out.numpy())

    Outputs:

    .. testoutput::

        [[ 0  1  2  3]
         [ 4  5  6  7]
         [ 8  9 10 11]]

    """
    return inp.reshape(target_shape)


def flatten(inp: Tensor, start_axis: int = 0, end_axis: int = -1) -> Tensor:
    r"""
    Reshapes the tensor by flattening the sub-tensor from dimension ``start_axis`` to dimension ``end_axis``.

    :param inp: input tensor.
    :param start_axis: start dimension that the sub-tensor to be flattened. Default: 0
    :param end_axis: end dimension that the sub-tensor to be flattened. Default: -1
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        inp_shape = (2, 2, 3, 3)
        x = tensor(
            np.arange(36, dtype=np.int32).reshape(inp_shape),
        )
        out = F.flatten(x, 2)
        print(x.numpy().shape)
        print(out.numpy().shape)

    Outputs:

    .. testoutput::

        (2, 2, 3, 3)
        (2, 2, 9)

    """
    target_shape = tuple(inp.shape[i] for i in range(start_axis)) + (-1,)
    if end_axis != -1:
        target_shape += (*inp.shape[end_axis + 1 :],)
    return inp.reshape(*target_shape)


def expand_dims(inp: Tensor, axis: Union[int, Sequence[int]]) -> Tensor:
    r"""
    Adds dimension before given axis.

    :param inp: input tensor.
    :param axis: place of new axes.
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        x = tensor([1, 2])
        out = F.expand_dims(x, 0)
        print(out.numpy().shape)

    Outputs:

    .. testoutput::

        (1, 2)

    """

    def get_axes():
        try:
            return [int(axis)]
        except (TypeError, ValueError):
            pass
        return list(map(int, axis))

    axis = get_axes()
    ndim = inp.ndim + len(axis)
    axis = sorted(i + ndim if i < 0 else i for i in axis)

    op = builtin.AddAxis(axis=axis)
    (result,) = apply(op, inp)
    return result


def squeeze(inp: Tensor, axis: Optional[Union[int, Sequence[int]]] = None) -> Tensor:
    r"""
    Removes dimension of shape 1.

    :param inp: input tensor.
    :param axis: place of axis to be removed.
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        x = tensor(np.array([1, 2], dtype=np.int32).reshape(1, 1, 2, 1))
        out = F.squeeze(x, 3)
        print(out.numpy().shape)

    Outputs:

    .. testoutput::

        (1, 1, 2)

    """
    return _remove_axis(inp, axis)


def linspace(
    start: Union[int, float, Tensor],
    stop: Union[int, float, Tensor],
    num: Union[int, Tensor],
    dtype="float32",
    device: Optional[CompNode] = None,
) -> Tensor:
    r"""
    Returns equally spaced numbers over a specified interval.

    :param start: starting value of the squence, shoule be scalar.
    :param stop: last value of the squence, shoule be scalar.
    :param num: number of values to generate.
    :param dtype: result data type.
    :return: generated tensor.

    Examples:

    .. testcode::

        import numpy as np
        import megengine.functional as F

        a = F.linspace(3, 10, 5)
        print(a.numpy())

    Outputs:

    .. testoutput::

        [ 3.    4.75  6.5   8.25 10.  ]

    """
    for item in (start, stop, num):
        cur_device = getattr(item, "device", None)
        if device is None:
            device = cur_device
        else:
            if not (cur_device is None or device == cur_device):
                raise ("ambiguous device for linspace opr")

    is_symbolvar = list(isinstance(x, SymbolVar) for x in [start, stop, num])
    if any(is_symbolvar) and not all(is_symbolvar):
        raise TypeError("start, stop and num should all be VarNode or none of them")

    if not isinstance(start, (Tensor, SymbolVar)):
        start = Tensor(start, device=device)
    if not isinstance(stop, (Tensor, SymbolVar)):
        stop = Tensor(stop, device=device)
    if not isinstance(num, (Tensor, SymbolVar)):
        num = Tensor(num, device=device)

    op = builtin.Linspace(comp_node=device)
    (result,) = apply(op, start, stop, num)
    if np.dtype(dtype) == np.int32:
        return result.astype(dtype)
    return result


def arange(
    start: Union[int, float, Tensor] = 0,
    stop: Optional[Union[int, float, Tensor]] = None,
    step: Union[int, float, Tensor] = 1,
    dtype="float32",
    device: Optional[CompNode] = None,
) -> Tensor:
    r"""
    Returns a tensor with values from start to stop with adjacent interval step.

    :param start: starting value of the squence, shoule be scalar.
    :param stop: ending value of the squence, shoule be scalar.
    :param step: gap between each pair of adjacent values. Default: 1
    :param dtype: result data type.
    :return: generated tensor.

    Examples:

    .. testcode::

        import numpy as np
        import megengine.functional as F

        a = F.arange(5)
        print(a.numpy())

    Outputs:

    Outputs:

    .. testoutput::

        [0. 1. 2. 3. 4.]

    """
    if stop is None:
        start, stop = 0, start

    if isinstance(start, Tensor):
        start = start.astype("float32")
    if isinstance(stop, Tensor):
        stop = stop.astype("float32")
    if isinstance(step, Tensor):
        step = step.astype("float32")
    num = ceil((stop - start) / step)
    stop = start + step * (num - 1)
    result = linspace(start, stop, num, device=device)
    if np.dtype(dtype) == np.int32:
        return result.astype(dtype)
    return result


def repeat(inp: Tensor, repeats: int, axis: Optional[int] = None):
    """
    Repeat elements of an array.

    :param inp: input tensor.
    :param repeats: the number of repetitions for each element.
    :param axis: the axis along which to repeat values. By default, use the
                 flattened input array, and return a flat output array.
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        import megengine.functional as F
        from megengine import tensor

        x = tensor([[1, 2], [3, 4]], np.int32)
        y = F.repeat(x, 2, axis=0)
        print(y.numpy())

    Outputs:

    .. testoutput::

        [[1 2]
         [1 2]
         [3 4]
         [3 4]]

    """
    if axis is None:
        inp = inp.reshape(-1)  # flatten
        axis = 0
    if inp._isscalar():
        inp._unsetscalar()
    shape = astensor1d(inp.shape, inp, dtype="int32", device=inp.device)
    # assume inp.ndim is not changed during trace
    max_axis = len(shape) - 1
    assert axis >= 0 and axis <= max_axis
    assert repeats >= 1

    base_shape, bcast_shape, target_shape = [], [], []
    if axis != 0:
        target_shape.append(shape[:axis])
    base_shape.extend([shape[: axis + 1], [1,]])
    bcast_shape.extend([shape[: axis + 1], [repeats,]])
    target_shape.extend(
        [shape[axis] * repeats,]
    )
    if axis + 1 <= max_axis:
        base_shape.append(shape[axis + 1 :])
        bcast_shape.append(shape[axis + 1 :])
        target_shape.append(shape[axis + 1 :])

    out = broadcast_to(inp.reshape(concat(base_shape)), concat(bcast_shape)).reshape(
        concat(target_shape)
    )
    return out


def _tile_one_dim(inp, rep, axis):
    shape = astensor1d(inp.shape, inp, dtype="int32", device=inp.device)
    # assume inp.ndim is not changed during trace
    max_axis = len(shape) - 1

    base_shape, bcast_shape, target_shape = [], [], []

    if axis != 0:
        base_shape.append(shape[:axis])
        bcast_shape.append(shape[:axis])
        target_shape.append(shape[:axis])
    base_shape.extend([[1,], shape[axis:]])
    bcast_shape.extend([rep, shape[axis:]])
    target_shape.append(shape[axis] * rep)
    if axis + 1 <= max_axis:
        target_shape.append(shape[axis + 1 :])

    out = broadcast_to(inp.reshape(concat(base_shape)), concat(bcast_shape)).reshape(
        concat(target_shape)
    )
    return out


def tile(inp: Tensor, reps: Iterable[int]):
    """
    Construct an array by repeating ``inp`` the number of times given by ``reps``. If reps has length d,
    the result will have dimension of ``max(d, inp.ndim)``. It is required that ``d >= inp.dim``. If ``inp.ndim < d``,
    ``inp`` is promoted to be ``d``-dimensional by prepending new axis.

    :param inp: input tensor.
    :param reps: The number of repetitions of inp along each axis.
    :return: output tensor.

    Examples:

    .. testcode::

        import numpy as np
        import megengine.functional as F
        from megengine import tensor

        x = tensor([[1, 2], [3, 4]], np.int32)
        y = F.tile(x, (2,1))
        print(y.numpy())

    Outputs:

    .. testoutput::

        [[1 2]
        [3 4]
        [1 2]
        [3 4]]

    """
    shape = astensor1d(inp.shape, inp, dtype="int32", device=inp.device)
    reps = astensor1d(reps, inp, dtype="int32", device=inp.device)
    l_shape = len(shape)
    l_reps = len(reps)
    assert (
        l_reps >= l_shape
    ), "Number of dimensions of tiled dims can not be smaller than number of dimensions of tensor"

    for i in range(l_shape):
        rep = reps[i + (l_reps - l_shape)]
        inp = _tile_one_dim(inp, rep, i)

    if l_reps > l_shape:
        shape = inp.shape
        extra = reps[:-l_shape]
        extra_ones = ones_like(extra)
        base_shape = concat([extra_ones, shape])
        bcast_shape = concat([extra, shape])
        target_shape = concat([extra, shape])
        inp = broadcast_to(inp.reshape(base_shape), bcast_shape).reshape(target_shape)

    return inp


def copy(inp, device=None):
    r"""
    Copies tensor to another device.

    :param inp: input tensor.
    :param device: destination device.

    Examples:

    .. testcode::

        import numpy as np
        from megengine import tensor
        import megengine.functional as F

        x = tensor([1, 2, 3], np.int32)
        y = F.copy(x, "xpu1")
        print(y.numpy())

    Outputs:

    .. testoutput::

        [1 2 3]
    """
    if device is None:
        return apply(Identity(), inp)[0]
    return apply(Copy(comp_node=as_device(device).to_c()), inp)[0]
