module TensorStream
  module ArrayOps
    def self.included(klass)
      klass.class_eval do
        register_op :slice do |context, tensor, inputs|
          input = inputs[0]
          start = inputs[1]
          size = complete_eval(tensor.options[:size], context)
          raise "start index and size not of the same shape #{start.size} != #{size.size}" if start.size != size.size

          slice_tensor(input, start.dup, size.dup)
        end

        register_op %i[flow_dynamic_stitch dynamic_stitch] do |context, tensor, inputs|
          number_of_indexes = tensor.options[:n]
          indexes = inputs[0...number_of_indexes]
          data = inputs[number_of_indexes...inputs.size]

          merged = []

          merge_dynamic_stitch(merged, indexes, data, context)
          merged
        end

        register_op :gather do |_context, tensor, inputs|
          params, indexes = inputs
          raise "axis !=0 not supported" if tensor.options[:axis] != 0
          gather(params, indexes)
        end

        register_op %i[concat concat_v2] do |_context, _tensor, inputs|
          axis = inputs.shift
          concat_array(inputs, axis)
        end

        register_op :stack do |_context, tensor, inputs|
          axis = tensor.options[:axis] || 0
          shape = shape_eval(inputs[0])
          rank = shape.size + 1
          elem_size = shape.empty? ? 1 : shape.reduce(:*)
          output_buffer = Array.new(inputs.size * elem_size) { 0 }
          new_shape = [inputs.size]
          shape.inject(new_shape) { |ns, s| ns << s }

          divisors = new_shape.dup.drop(1).reverse.inject([1]) { |a, s|
            a << s * a.last
          }.reverse

          axis = rank + axis if axis < 0
          rotated_shape = Array.new(axis + 1) { new_shape.shift }
          new_shape = rotated_shape.rotate! + new_shape

          multipliers = new_shape.dup.drop(1).reverse.inject([1]) { |a, s|
            a << s * a.last
          }.reverse

          inputs.each_with_index do |input, index|
            raw_input = input.is_a?(Array) ? input.flatten : [input]
            start = index * divisors.first

            raw_input.each_with_index do |x, index2|
              index_map = []
              ptr = start + index2
              divisors.each_with_object(index_map) do |div, a|
                a << (ptr / div.to_f).floor
                ptr = ptr % div
              end

              rotated_index = Array.new(axis + 1) { index_map.shift }
              index_map = rotated_index.rotate! + index_map

              ptr2 = 0
              multipliers.each_with_index do |m, idx|
                ptr2 += index_map[idx] * m
              end

              output_buffer[ptr2] = x
            end
          end

          TensorShape.reshape(output_buffer, new_shape)
        end

        register_op :unstack do |_context, tensor, inputs|
          axis = tensor.options[:axis] || 0
          new_shape = shape_eval(inputs[0])
          rank = new_shape.size - 1

          divisors = new_shape.dup.drop(1).reverse.inject([1]) { |a, s|
            a << s * a.last
          }.reverse

          axis = rank + axis if axis < 0
          rotated_shape = Array.new(axis + 1) { new_shape.shift }
          new_shape = rotated_shape.rotate!(-1) + new_shape
          output_buffer = Array.new(new_shape.reduce(:*)) { 0 }

          multipliers = new_shape.dup.drop(1).reverse.inject([1]) { |a, s|
            a << s * a.last
          }.reverse

          inputs.each_with_index do |input, index|
            raw_input = input.is_a?(Array) ? input.flatten : [input]
            start = index * divisors.first

            raw_input.each_with_index do |x, index2|
              index_map = []
              ptr = start + index2
              divisors.each_with_object(index_map) do |div, a|
                a << (ptr / div.to_f).floor
                ptr = ptr % div
              end

              rotated_index = Array.new(axis + 1) { index_map.shift }
              index_map = rotated_index.rotate!(-1) + index_map

              ptr2 = 0
              multipliers.each_with_index do |m, idx|
                ptr2 += index_map[idx] * m
              end

              output_buffer[ptr2] = x
            end
          end

          res = TensorShape.reshape(output_buffer, new_shape)

          TensorStream::Evaluator::OutputGroup.new(res, res.map { tensor.inputs[0].data_type })
        end

        register_op :squeeze do |_context, tensor, inputs|
          val = inputs[0]
          shape = shape_eval(val)

          axis = !tensor.options[:axis].is_a?(Array) ? [tensor.options[:axis]] : tensor.options[:axis]

          if !axis.empty?
            axis.each do |x|
              raise TensorStream::ValueError, "unable to squeeze dimension that does not have a size of 1" if shape[x] != 1

              shape[x] = nil
            end
          else
            shape = shape.map { |s| s == 1 ? nil : s }
          end

          TensorShape.reshape(val, shape.compact)
        end

        register_op :expand_dims do |_context, _tensor, inputs|
          val, axis = inputs
          axis = axis.nil? ? 0 : axis

          shape = shape_eval(val)
          axis = -axis if axis == shape.size

          new_shape = shape.dup.insert(axis, 1).compact

          TensorShape.reshape([val], new_shape)
        end

        register_op :fill do |_context, tensor, inputs|
          shape = inputs[0] || tensor.shape.shape
          value = inputs[1]

          func = -> { value }

          if shape.is_a?(Array) && shape.size.zero?
            func.call
          else
            shape = [shape.to_i] unless shape.is_a?(Array)
            generate_vector(shape, generator: func)
          end
        end

        register_op :invert_permutation do |_context, _tensor, inputs|
          input = inputs[0]
          output = input.dup

          unless input.nil?
            input.size.times.each do |index|
              output[input[index]] = index
            end
          end

          output
        end

        register_op :index, no_eval: true do |_context, _tensor, inputs|
          f = inputs[0]
          index = inputs[1]
          if f.is_a?(TensorStream::Evaluator::OutputGroup)
            f.outputs[index]
          else
            f[index]
          end
        end

        register_op :setdiff1d do |_context, tensor, inputs|
          input, remove = inputs
          idx = []
          out = []
          input.each_with_index do |x, index|
            next if remove.include?(x)

            out << x
            idx << index
          end
          idx = idx.map { |i| Tensor.cast_dtype(i, tensor.options[:index_dtype]) } unless tensor.options[:index_dtype] == :int32
          TensorStream::Evaluator::OutputGroup.new([out, idx], tensor.inputs.map(&:data_type))
        end

        register_op :size do |_context, tensor, inputs|
          input = inputs[0]
          Tensor.cast_dtype(input.flatten.size, tensor.options[:out_type])
        end

        register_op :range do |_context, _tensor, inputs|
          start, limit, delta = inputs

          raise " delta !=0 " if delta.zero?

          if limit.zero?
            limit = start
            start = 0
          end

          raise " Requires start <= limit when delta > 0" if (start > limit) && delta > 0
          raise " Requires start >= limit when delta < 0" if (start < limit) && delta < 0

          cur_step = start
          r = []
          Kernel.loop do
            break if start == limit
            break if (start < limit) && (cur_step >= limit)
            break if (start > limit) && (cur_step <= limit)

            r << cur_step
            cur_step += delta
          end
          r
        end

        register_op :eye do |_context, tensor, inputs|
          rows, columns = inputs

          Array.new(rows) do |i|
            Array.new(columns) do |col|
              if fp_type?(tensor.data_type)
                i == col ? 1.0 : 0.0
              else
                i == col ? 1 : 0
              end
            end
          end
        end

        register_op %i[zeros ones zeros_like ones_like] do |_context, tensor, inputs|
          shape = if %i[zeros_like ones_like].include?(tensor.operation)
            shape_eval(inputs[0])
          else
            inputs[0] || tensor.shape.shape
          end

          func = if %i[zeros zeros_like].include?(tensor.operation)
            -> { int_type?(tensor.data_type) ? 0 : 0.0 }
          else
            -> { int_type?(tensor.data_type) ? 1 : 1.0 }
          end
          if shape.is_a?(Array) && shape.size.zero?
            func.call
          else
            shape = [shape.to_i] unless shape.is_a?(Array)

            cache_key = "#{tensor.operation}_#{shape}"
            if @context[:_cache].key?(cache_key)
              @context[:_cache][cache_key]
            else
              generate_vector(shape, generator: func).tap do |v|
                @context[:_cache][cache_key] = v
              end
            end
          end
        end

        register_op :truncate do |_context, _tensor, inputs|
          truncate(inputs[0], inputs[1])
        end

        register_op :rank do |_context, _tensor, inputs|
          get_rank(inputs[0])
        end

        register_op :split  do |_context, tensor, inputs|
          value, num_split, axis = inputs

          value_shape = shape_eval(value)
          res = if num_split.is_a?(Array)
            begin_index = 0
            num_split.collect do |num|
              end_index = begin_index + num
              arr = split_tensor(value, begin_index, end_index, axis)
              begin_index = end_index
              arr
            end
          else
            raise TensorStream::ValueError, "#{num_split} does not divide #{value_shape[axis]} evenly" if value_shape[axis] % num_split != 0

            piece_sizes = value_shape[axis] / num_split
            Array.new(num_split) do |num|
              begin_index = num * piece_sizes
              end_index = begin_index + piece_sizes
              split_tensor(value, begin_index, end_index, axis)
            end
          end
          TensorStream::Evaluator::OutputGroup.new(res, res.map { tensor.inputs[0].data_type })
        end

        register_op :reshape do |_context, _tensor, inputs|
          arr, new_shape = inputs
          arr = [arr] unless arr.is_a?(Array)
          flat_arr = arr.flatten
          if new_shape.size.zero? && flat_arr.size == 1
            flat_arr[0]
          else
            TensorShape.reshape(flat_arr, new_shape)
          end
        end

        register_op :pad do |_context, tensor, inputs|
          arr_pad(inputs[0], inputs[1], tensor.data_type)
        end

        register_op :tile do |_context, _tensor, inputs|
          input, multiples = inputs
          rank = get_rank(input)
          raise "1D or higher tensor required" if rank.zero?
          raise "invalid multiple size passed #{rank} != #{multiples.size}" if rank != multiples.size

          tile = tile_arr(input, 0, multiples)
          tile.nil? ? [] : tile
        end

        register_op %i[select where] do |context, tensor, inputs|
          pred = inputs[0]
          call_3way_vector_op(pred, inputs[1], inputs[2], context) { |t, u, v| t ? u : v }
        end

        register_op :shape do |_context, tensor, inputs|
          shape_eval(inputs[0], tensor.options[:out_type])
        end

        register_op :shape_n do |_context, tensor, inputs|
          shapes = inputs.collect { |input|
            shape_eval(input)
          }
          TensorStream::Evaluator::OutputGroup.new(shapes, shapes.map { tensor.options[:out_type] })
        end

        register_op :transpose do |_context, _tensor, inputs|
          shape = shape_eval(inputs[0])
          rank = get_rank(inputs[0])
          perm = inputs[1] || (0...rank).to_a.reverse

          if rank == 2 && perm.nil? # use native transpose for general case
            inputs[0].transpose
          else
            arr = inputs[0].flatten

            new_shape = perm.map { |p| shape[p] }
            new_arr = Array.new(shape.reduce(:*)) { 0 }
            transpose_with_perm(arr, new_arr, shape, new_shape, perm)
            TensorShape.reshape(new_arr, new_shape)
          end
        end

        register_op :case, noop: true do |context, tensor, _inputs|
          pred = global_eval(tensor, tensor.inputs[0], context)
          result = nil

          if tensor.options[:exclusive]
            p_true = pred.each_with_index.collect { |p, index| [p, index] }.select { |a| a[0] }
            raise TensorStream::ValueError, "more than one predicate returns true pos #{p_true.map { |a| a[1] }.join(",")}" if p_true.size > 1
          end

          pred.each_with_index do |p, index|
            next unless p

            result = global_eval(tensor, tensor.inputs[2 + index], context)
          end

          result = global_eval(tensor, tensor.inputs[1], context) if result.nil?

          result
        end

        register_op :case_grad do |_context, tensor, inputs|
          index, pred, func, grad = inputs
          if index < 0 && !pred.find { |p| !!p }
            grad
          elsif index >= 0 && pred[index]
            grad
          else
            func = -> { int_type?(tensor.data_type) ? 0 : 0.0 }
            shape = shape_eval(func)
            generate_vector(shape, generator: func)
          end
        end

        register_op :dynamic_partition do |context, tensor, inputs|
          data, partitions = inputs
          num_partitions = tensor.options[:num_partitions]
          output_arr = Array.new(num_partitions) { [] }

          partitions.each_with_index do |part, index|
            output_arr[part] << data[index]
          end
          TensorStream::Evaluator::OutputGroup.new(output_arr, num_partitions.times.map { tensor.data_type })
        end

        register_op :gather_grad do |context, tensor, inputs|
          grad, indexes, input_shape = inputs
          output = Array.new(input_shape.reduce(:*)) { fp_type?(tensor.data_type) ? 0.0 : 0 }
          indexes.each_with_index.map do |x, index|
            output[x] += grad[index]
          end
          TensorShape.reshape(output, input_shape)
        end

        register_op :strided_slice do |_context, _tensor, inputs|
          value, b_index, e_index, stride = inputs
          slices = b_index.zip(e_index).zip(stride).map do |params|
            selection, stride = params
            s, e = selection
            [s, e, stride]
          end
          strided_slice(value, slices)
        end

        register_op :strided_slice_grad do |_context, tensor, inputs|
          x, b_index, e_index, stride, grad = inputs
          slices = b_index.zip(e_index).zip(stride).map do |params|
            selection, stride = params
            s, e = selection
            [s, e, stride]
          end

          target_val = generate_vector(x, generator: ->() { fp_type?(tensor.data_type) ? 0.0 : 0 })

          strided_slice_grad(target_val, grad, x.dup, slices.dup)
          target_val
        end

        def merge_dynamic_stitch(merged, indexes, data, context)
          indexes.each_with_index do |ind, m|
            if ind.is_a?(Array)
              merge_dynamic_stitch(merged, ind, data[m], context)
            else
              ind = ind.is_a?(Tensor) ? complete_eval(ind, context) : ind
              merged[ind] = data[m]
            end
          end
        end
      end
    end
  end
end
