import numpy as np
import tensorflow as tf
try:
  from tensorflow.math import floormod as tf_floormod
except ImportError: # for older tf-1.x versions
  from tensorflow import floormod as tf_floormod

from onnx_tf.handlers.backend_handler import BackendHandler
from onnx_tf.handlers.handler import onnx_op
from onnx_tf.handlers.handler import tf_func


@onnx_op("Slice")
@tf_func(tf.strided_slice)
class Slice(BackendHandler):

  @classmethod
  def version_1(cls, node, **kwargs):
    tensor_dict = kwargs["tensor_dict"]
    x = tensor_dict[node.inputs[0]]

    full_sizes = x.get_shape().as_list()
    full_begin = [0] * len(full_sizes)

    starts = node.attrs.get("starts")
    ends = node.attrs.get("ends")
    slice_len = len(starts)
    axes = node.attrs.get("axes", list(range(slice_len)))

    for i in range(slice_len):
      starts[i] = full_sizes[
          axes[i]] + starts[i] if starts[i] < 0 else starts[i]
      ends[i] = full_sizes[axes[i]] + ends[i] if ends[i] < 0 else ends[i]
      if full_sizes[axes[i]] is not None:
        ends[i] = np.min([full_sizes[axes[i]], ends[i]])
        starts[i] = np.min([full_sizes[axes[i]], starts[i]])
      full_begin[axes[i]] = starts[i]
      full_sizes[axes[i]] = ends[i] - starts[i]

    return [
        cls.make_tensor_from_onnx_node(
            node,
            tf_func=tf.slice,
            inputs=[
                tensor_dict[node.inputs[0]],
                tf.constant(full_begin),
                tf.constant(full_sizes)
            ],
            **kwargs)
    ]

  @classmethod
  def version_10(cls, node, **kwargs):
    tensor_dict = kwargs["tensor_dict"]
    input_tensor = tensor_dict[node.inputs[0]]
    starts = tensor_dict[node.inputs[1]]
    ends = tensor_dict[node.inputs[2]]

    # first of all, get the input tensor shape
    input_tensor_shape = tf.shape(input_tensor, out_type=ends.dtype)

    axes = tensor_dict[node.inputs[3]] if len(
        node.inputs) >= 4 else tf.range(tf.shape(starts, out_type=ends.dtype)[0])

    # process negative axes
    input_rank = tf.cast(tf.rank(input_tensor), axes.dtype)
    axes = tf_floormod(tf.add(axes, input_rank), input_rank)

    # expand a dimension of 1 at the end
    sparse_indices = tf.expand_dims(axes, -1)

    # build the indexed dimension sizes as sparse_shape
    sparse_shape = tf.gather_nd(
        params=input_tensor_shape, indices=sparse_indices)
    sparse_shape = tf.cast(sparse_shape, ends.dtype)

    # take care of starts, ends that are larger than the dim size.
    starts_min = tf.minimum(starts, sparse_shape)
    ends_min = tf.minimum(ends, sparse_shape)

    # take care of starts, ends that are negative
    is_starts_negative = tf.less(starts_min, tf.zeros_like(starts_min))
    starts_final = tf.where(is_starts_negative, starts_min + sparse_shape,
                            starts_min)
    is_ends_negative = tf.less(ends_min, tf.zeros_like(ends_min))
    ends_final = tf.where(is_ends_negative, ends_min + sparse_shape, ends_min)

    # need to densify everything for the inputs to slice
    # the output shape is the input_tensor rank
    output_shape = tf.reshape(tf.rank(input_tensor), [1])
    output_shape = tf.cast(output_shape, ends.dtype)

    # create dense tensor, pad 0 as default begins
    dense_begins = tf.sparse_to_dense(sparse_indices, output_shape,
                                      starts_final)
    # create dense tensor, pad -1 for next step
    dense_ends = tf.sparse_to_dense(
        sparse_indices,
        output_shape,
        ends_final,
        default_value=tf.constant(-1, dtype=dense_begins.dtype))
    # replace -1 with respective dimension sizes
    dense_ends = tf.where(
        tf.equal(dense_ends, tf.constant(-1, dtype=dense_begins.dtype)),
        input_tensor_shape, dense_ends)

    # create dense tensor for steps if not already so
    if len(node.inputs) >= 5:
      dense_steps = tf.sparse_to_dense(
          sparse_indices,
          output_shape,
          tensor_dict[node.inputs[4]],
          default_value=tf.constant(1, dtype=tensor_dict[node.inputs[4]].dtype))
    else:
      dense_steps = tf.ones(input_tensor_shape.shape, ends.dtype)

    return [
        cls.make_tensor_from_onnx_node(
            node,
            inputs=[
                tensor_dict[node.inputs[0]], dense_begins, dense_ends,
                dense_steps
            ],
            **kwargs)
    ]

  @classmethod
  def version_11(cls, node, **kwargs):
    return cls.version_10(node, **kwargs)
