module TensorStream
  module Utils
    def float32
      Types.float32
    end

    def graph
      TensorStream::Graph.new
    end

    def get_default_graph
      TensorStream::Graph.get_default_graph
    end

    def reset_default_graph
      TensorStream::Graph.get_default_graph.reset
    end

    def enable_eager_execution
      TensorStream::Graph.get_default_graph.enable_eager_execution
    end

    def disable_eager_execution
      TensorStream::Graph.get_default_graph.disable_eager_execution
    end

    def executing_eagerly?
      TensorStream::Graph.get_default_graph.executing_eagerly?
    end

    ##
    # List available evaluators + devices in the current local environment
    # Returns:
    # - An array containing the names of those devices
    def list_local_devices
      local_name = "job:localhost"
      TensorStream::Evaluator.evaluators.collect { |k, v|
        v[:class].query_supported_devices.collect do |device_str|
          [local_name, "ts:#{k}:#{device_str.name}"].join("/")
        end
      }.flatten
    end

    ##
    # Creates a variable
    # A variable maintains state across sessions
    def variable(value, name: nil, initializer: nil, graph: nil, dtype: nil, trainable: true)
      common_options = {
        initializer: TensorStream.convert_to_tensor(initializer || value),
        name: name,
        graph: graph,
        dtype: dtype,
        trainable: trainable,
      }
      tensor = if value.is_a?(String)
                 i_var(dtype || :string, 0, [], get_variable_scope, common_options)
               elsif value.is_a?(Integer)
                 i_var(dtype || :int32, 0, [], get_variable_scope, common_options)
               elsif value.is_a?(Float)
                 i_var(dtype || :float32, 0, [], get_variable_scope, common_options)
               else
                 i_var(dtype || :float32, 0, nil, get_variable_scope, common_options)
               end
      tensor
    end

    ##
    # Defines a variable context manager
    def variable_scope(scope = nil, default_name = nil, reuse: nil, initializer: nil)
      Thread.current[:tensor_stream_variable_scope] ||= [VariableScope.new]

      # uniquenifier
      if scope.nil? && default_name
        same_names = get_variable_scope.used_names.select { |s| s.start_with?(default_name) }
        new_name = default_name
        index = 1
        while same_names.include?(new_name)
          new_name = "#{default_name}_#{index}"
          index += 1
        end
        scope = new_name
      end

      variable_scope = VariableScope.new(name: scope, reuse: reuse, initializer: initializer)
      get_variable_scope.register_name(scope || "")
      Thread.current[:tensor_stream_variable_scope] << variable_scope
      scope_name = __v_scope_name
      if block_given?
        begin
          TensorStream.get_default_graph.name_scope(scope) do
            yield(scope_name)
          end
        ensure
          Thread.current[:tensor_stream_variable_scope].pop
        end
      else
        variable_scope
      end
    end

    def device(device_uri, &block)
      get_default_graph.device(device_uri, &block)
    end

    def name_scope(name, default_name = nil, default: nil, values: nil)
      if values
        graph_count = values.select { |v| v.is_a?(Tensor) }.map(&:graph).map(&:object_id).uniq.size
        raise "values are not on the same graph" if graph_count > 1
      end

      get_default_graph.name_scope(name || default_name || default) do |scope|
        yield scope if block_given?
      end
    end

    def get_variable_scope
      unless Thread.current[:tensor_stream_variable_scope]
        variable_scope = VariableScope.new
        Thread.current[:tensor_stream_variable_scope] = [variable_scope]
        return variable_scope
      end

      Thread.current[:tensor_stream_variable_scope].last
    end

    def __v_scope_name
      Thread.current[:tensor_stream_variable_scope].map(&:name).compact.reject(&:empty?).join("/")
    end

    ##
    # Creates a session context where operations can be executed
    #
    # Args:
    #     evaluator: Specific evaluator to use, otherwise the best evaluator will automatically be determined
    #
    # Options:
    #     thread_pool_class: Class to use to manage thread pooling
    #     log_device_placement: Show assigned device/evalutor for each tensor op
    #     profile_enabled: Log performance metrics for each operation
    def session(evaluator = nil, thread_pool_class: Concurrent::ImmediateExecutor, log_device_placement: false, profile_enabled: false)
      session = TensorStream::Session.new(evaluator, thread_pool_class: thread_pool_class, log_device_placement: log_device_placement, profile_enabled: profile_enabled)
      yield session if block_given?

      session
    end

    def colocate_with(op, ignore_existing: false)
      # noop for now
      yield
    end

    def program
      yield self
    end

    def layers
      TensorStream::Layers
    end

    def constant(value, dtype: nil, shape: nil, internal: false, name: "Const")
      shared_options = {const: true, value: value, name: name, internal: internal}

      if value.is_a?(Float)
        TensorStream::Constant.new(dtype || :float32, 0, shape || [], shared_options)
      elsif value.is_a?(Integer)
        TensorStream::Constant.new(dtype || :int32, 0, shape || [], shared_options)
      elsif value.is_a?(String)
        TensorStream::Constant.new(dtype || :string, 0, shape || [], shared_options)
      elsif !!value == value
        TensorStream::Constant.new(dtype || :boolean, 0, shape || [], shared_options)
      elsif value.is_a?(Array)
        dimension = shape || shape_eval(value)
        rank = dimension.size
        TensorStream.check_if_dense(value)

        cur_dtype = dtype || Tensor.detect_type(value.flatten.last)
        value = Tensor.cast_dtype(value, cur_dtype) unless dtype.nil?

        shared_options[:value] = value
        TensorStream::Constant.new(cur_dtype, rank, dimension, shared_options)
      end
    end

    def group(inputs, name: nil)
      TensorStream::ControlFlow.new(:group, inputs, nil, name: name)
    end

    def dynamic_stitch(indices, data, name: nil)
      TensorStream::DynamicStitch.new(:dynamic_stitch, [indices, data], name: name)
    end

    def get_variable(name, dtype: nil, shape: nil, initializer: nil, trainable: true, collections: nil, validate_shape: false)
      get_variable_scope.get_variable(name, dtype: dtype, shape: shape, initializer: initializer, trainable: trainable, collections: collections)
    end

    def get_collection(name, options = {})
      Graph.get_default_graph.get_collection(name, options)
    end

    def assign(ref, value, name: nil)
      raise "#{ref.name} not a variable" unless ref.is_a?(Variable)
      ref.assign(value, name: name)
    end

    ##
    # Inserts a placeholder for a tensor that will be always fed.
    def placeholder(dtype, shape: nil, name: nil)
      TensorStream::Placeholder.new(dtype, nil, shape, name: name)
    end

    def global_variables_initializer
      TensorStream::Variable.global_variables_initializer
    end

    def train
      TensorStream::Trainer
    end

    def math
      TensorStream::Maths
    end

    def image
      TensorStream::Images
    end

    def trainable_variables
      TensorStream.get_default_graph.get_collection(TensorStream::GraphKeys::TRAINABLE_VARIABLES)
    end

    ##
    # Sets random seed to use for the default graph
    def set_random_seed(seed)
      TensorStream.get_default_graph.random_seed = seed
    end

    def control_dependencies(control_inputs, &block)
      TensorStream.get_default_graph.control_dependencies(control_inputs, &block)
    end

    def convert_to_tensor(value, dtype: nil, name: nil)
      return value if value.is_a?(Tensor)
      return convert_to_tensor(value.call) if value.is_a?(Proc)
      # raise "Invalid tensor value" if value.nil?

      if value.is_a?(Array) && value.detect { |v| v.is_a?(Tensor) }
        return TensorStream.stack(value) if value.size > 1

        return TensorStream.expand_dims(value[0], 0)
      end

      if value.is_a?(TensorShape)
        value = value.shape
      end

      check_if_dense(value)
      i_cons(value, dtype: dtype || Tensor.detect_type(value), name: name)
    end

    ##
    # Check to make sure passed array is dense
    #
    def check_if_dense(value, expected_shape = nil)
      return unless value.is_a?(Array)
      return if value.empty?

      expected_shape ||= shape_eval(value)

      s = expected_shape.shift
      raise TensorStream::ValueError, "Argument must be a dense tensor: #{value}, expected size #{s} got #{value.size}" if value.size != s

      return if expected_shape.empty?

      value.each do |item|
        check_if_dense(item, expected_shape.dup)
      end
    end

    def check_allowed_types(input, types)
      return input unless input.is_a?(Tensor)
      return input if input.data_type.nil?

      raise "#{input.source}: Parameter data type #{input.data_type} passed not in #{types.join(",")}" unless types.include?(input.data_type.to_sym)
    end

    def check_data_types(*args)
      unique_types = args.select { |a| a.is_a?(Tensor) }. map { |a| DataTypeUtils.norm_dtype(a.data_type) }.uniq

      if unique_types.size > 1
        raise TensorStream::ValueError, "Value Error: Tensor conversion requested dtypes are different -> #{unique_types}"
      end

      unique_types.first
    end

    ##
    # Auto cast ruby constant data types to the same
    # tensor types of other operands
    def apply_data_type_coercion(*args)
      coerced_type = check_data_types(*args)
      args.map { |a| a.is_a?(Tensor) ? a : convert_to_tensor(a, dtype: coerced_type) }
    end
  end
end
