# frozen_string_literal: true

module JSI
  # a module of methods for objects which behave like Hash but are not Hash.
  #
  # this module is intended to be internal to JSI. no guarantees or API promises
  # are made for non-JSI classes including this module.
  module Util::Hashlike
    # safe methods which can be delegated to #to_hash (which the includer is assumed to have defined).
    # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.

    # methods which do not need to access the value.
    SAFE_KEY_ONLY_METHODS = %w(each_key empty? has_key? include? key? keys length member? size).map(&:freeze).freeze
    SAFE_KEY_VALUE_METHODS = %w(< <= > >= any? assoc compact dig each_pair each_value fetch fetch_values has_value? invert key merge rassoc reject select to_h to_proc transform_values value? values values_at).map(&:freeze).freeze
    DESTRUCTIVE_METHODS = %w(clear delete delete_if keep_if reject! replace select! shift).map(&:freeze).freeze
    # these return a modified copy
    safe_modified_copy_methods = %w(compact)
    # select and reject will return a modified copy but need the yielded block variable value from #[]
    safe_kv_block_modified_copy_methods = %w(select reject)
    SAFE_METHODS = SAFE_KEY_ONLY_METHODS | SAFE_KEY_VALUE_METHODS
    custom_methods = %w(merge) # defined below
    safe_to_hash_methods = SAFE_METHODS -
      safe_modified_copy_methods -
      safe_kv_block_modified_copy_methods -
      custom_methods
    safe_to_hash_methods.each do |method_name|
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
        define_method(method_name) { |*a, &b| to_hash.public_send(method_name, *a, &b) }
      else
        define_method(method_name) { |*a, **kw, &b| to_hash.public_send(method_name, *a, **kw, &b) }
      end
    end
    safe_modified_copy_methods.each do |method_name|
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
        define_method(method_name) do |*a, &b|
          jsi_modified_copy do |object_to_modify|
            responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
            responsive_object.public_send(method_name, *a, &b)
          end
        end
      else
        define_method(method_name) do |*a, **kw, &b|
          jsi_modified_copy do |object_to_modify|
            responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
            responsive_object.public_send(method_name, *a, **kw, &b)
          end
        end
      end
    end
    safe_kv_block_modified_copy_methods.each do |method_name|
      define_method(method_name) do |**kw, &b|
        jsi_modified_copy do |object_to_modify|
          responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_hash
          responsive_object.public_send(method_name) do |k, _v|
            b.call(k, self[k, **kw])
          end
        end
      end
    end

    # like [Hash#update](https://ruby-doc.org/core/Hash.html#method-i-update)
    # @param other [#to_hash] the other hash to update this hash from
    # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
    #   is determined by calling the block with the key, its value in self and its value in other.
    # @return self, updated with other
    # @raise [TypeError] when `other` does not respond to #to_hash
    def update(other, &block)
      unless other.respond_to?(:to_hash)
        raise(TypeError, "cannot update with argument that does not respond to #to_hash: #{other.pretty_inspect.chomp}")
      end
      other.to_hash.each_pair do |key, value|
        if block && key?(key)
          value = yield(key, self[key], value)
        end
        self[key] = value
      end
      self
    end

    alias_method :merge!, :update

    # like [Hash#merge](https://ruby-doc.org/core/Hash.html#method-i-merge)
    # @param other [#to_hash] the other hash to merge into this
    # @yield [key, oldval, newval] for entries with duplicate keys, the value of each duplicate key
    #   is determined by calling the block with the key, its value in self and its value in other.
    # @return duplicate of this hash with the other hash merged in
    # @raise [TypeError] when `other` does not respond to #to_hash
    def merge(other, &block)
      jsi_modified_copy do |instance|
        instance.merge(other.is_a?(Base) ? other.jsi_node_content : other, &block)
      end
    end

    # basically the same #inspect as Hash, but has the class name and, if responsive,
    # self's #jsi_object_group_text
    # @return [String]
    def inspect
      object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
      -"\#{<#{object_group_str}>#{map { |k, v| " #{k.inspect} => #{v.inspect}" }.join(',')}}"
    end

    alias_method :to_s, :inspect

    # pretty-prints a representation of this hashlike to the given printer
    # @return [void]
    def pretty_print(q)
      object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
      q.text "\#{<#{object_group_str}>"
      q.group_sub {
        q.nest(2) {
          q.breakable ' ' if !empty?
          q.seplist(self, nil, :each_pair) { |k, v|
            q.group {
              q.pp k
              q.text ' => '
              q.pp v
            }
          }
        }
      }
      q.breakable '' if !empty?
      q.text '}'
    end
  end

  # a module of methods for objects which behave like Array but are not Array.
  #
  # this module is intended to be internal to JSI. no guarantees or API promises
  # are made for non-JSI classes including this module.
  module Util::Arraylike
    # safe methods which can be delegated to #to_ary (which the includer is assumed to have defined).
    # 'safe' means, in this context, nondestructive - methods which do not modify the receiver.

    # methods which do not need to access the element.
    SAFE_INDEX_ONLY_METHODS = %w(each_index empty? length size).map(&:freeze).freeze
    # there are some ambiguous ones that are omitted, like #sort, #map / #collect.
    SAFE_INDEX_ELEMENT_METHODS = %w(| & * + - <=> abbrev at bsearch bsearch_index combination compact count cycle dig drop drop_while fetch find_index first include? index join last pack permutation product reject repeated_combination repeated_permutation reverse reverse_each rindex rotate sample select shelljoin shuffle slice sort take take_while transpose uniq values_at zip).map(&:freeze).freeze
    DESTRUCTIVE_METHODS = %w(<< clear collect! compact! concat delete delete_at delete_if fill flatten! insert keep_if map! pop push reject! replace reverse! rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift).map(&:freeze).freeze

    # methods (well, method) that returns a modified copy and doesn't need any handling of block variable(s)
    safe_modified_copy_methods = %w(compact)

    # methods that return a modified copy and do need handling of block variables
    safe_el_block_methods = %w(reject select)

    SAFE_METHODS = SAFE_INDEX_ONLY_METHODS | SAFE_INDEX_ELEMENT_METHODS
    safe_to_ary_methods = SAFE_METHODS - safe_modified_copy_methods - safe_el_block_methods
    safe_to_ary_methods.each do |method_name|
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
        define_method(method_name) { |*a, &b| to_ary.public_send(method_name, *a, &b) }
      else
        define_method(method_name) { |*a, **kw, &b| to_ary.public_send(method_name, *a, **kw, &b) }
      end
    end
    safe_modified_copy_methods.each do |method_name|
      if Util::LAST_ARGUMENT_AS_KEYWORD_PARAMETERS
        define_method(method_name) do |*a, &b|
          jsi_modified_copy do |object_to_modify|
            responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
            responsive_object.public_send(method_name, *a, &b)
          end
        end
      else
        define_method(method_name) do |*a, **kw, &b|
          jsi_modified_copy do |object_to_modify|
            responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
            responsive_object.public_send(method_name, *a, **kw, &b)
          end
        end
      end
    end
    safe_el_block_methods.each do |method_name|
      define_method(method_name) do |**kw, &b|
        jsi_modified_copy do |object_to_modify|
          i = 0
          responsive_object = object_to_modify.respond_to?(method_name) ? object_to_modify : object_to_modify.to_ary
          responsive_object.public_send(method_name) do |_e|
            b.call(self[i, **kw]).tap { i += 1 }
          end
        end
      end
    end

    # see [Array#assoc](https://ruby-doc.org/core/Array.html#method-i-assoc)
    def assoc(obj)
      # note: assoc implemented here (instead of delegated) due to inconsistencies in whether
      # other implementations expect each element to be an Array or to respond to #to_ary
      detect { |e| e.respond_to?(:to_ary) and e[0] == obj }
    end

    # see [Array#rassoc](https://ruby-doc.org/core/Array.html#method-i-rassoc)
    def rassoc(obj)
      # note: rassoc implemented here (instead of delegated) due to inconsistencies in whether
      # other implementations expect each element to be an Array or to respond to #to_ary
      detect { |e| e.respond_to?(:to_ary) and e[1] == obj }
    end

    # basically the same #inspect as Array, but has the class name and, if responsive,
    # self's #jsi_object_group_text
    # @return [String]
    def inspect
      object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
      -"\#[<#{object_group_str}>#{map { |e| ' ' + e.inspect }.join(',')}]"
    end

    alias_method :to_s, :inspect

    # pretty-prints a representation of this arraylike to the given printer
    # @return [void]
    def pretty_print(q)
      object_group_str = (respond_to?(:jsi_object_group_text, true) ? jsi_object_group_text : [self.class]).join(' ')
      q.text "\#[<#{object_group_str}>"
      q.group_sub {
        q.nest(2) {
          q.breakable ' ' if !empty?
          q.seplist(self, nil, :each) { |e|
            q.pp e
          }
        }
      }
      q.breakable '' if !empty?
      q.text ']'
    end
  end
end