require 'rollbar/util/hash'

module Rollbar
  module Util # :nodoc:
    def self.iterate_and_update_with_block(obj, &block)
      iterate_and_update(obj, block)
    end

    def self.iterate_and_update(obj, block, seen = {})
      seen.compare_by_identity
      return if obj.frozen? || seen[obj]

      seen[obj] = true

      if obj.is_a?(Array)
        iterate_and_update_array(obj, block, seen)
      else
        iterate_and_update_hash(obj, block, seen)
      end
    end

    def self.iterate_and_update_array(array, block, seen)
      array.each_with_index do |value, i|
        if value.is_a?(::Hash) || value.is_a?(Array)
          iterate_and_update(value, block, seen)
        else
          array[i] = block.call(value)
        end
      end
    end

    def self.iterate_and_update_hash(obj, block, seen)
      obj.keys.each do |k| # rubocop:disable Style/HashEachMethods
        v = obj[k]
        new_key = block.call(k)

        if v.is_a?(::Hash) || v.is_a?(Array)
          iterate_and_update(v, block, seen)
        else
          obj[k] = block.call(v)
        end

        if new_key != k
          obj[new_key] = obj[k]
          obj.delete(k)
        end
      end
    end

    def self.deep_copy(obj, copied = {})
      copied.compare_by_identity

      # if we've already made a copy, return it.
      return copied[obj] if copied[obj]

      result = clone_obj(obj)

      # Memoize the cloned object before recursive calls to #deep_copy below.
      # This is the point of doing the work in two steps.
      copied[obj] = result

      case obj
      when ::Hash
        obj.each { |k, v| result[k] = deep_copy(v, copied) }
      when Array
        obj.each { |v| result << deep_copy(v, copied) }
      end

      result
    end

    def self.clone_obj(obj)
      case obj
      when ::Hash
        obj.dup
      when Array
        obj.dup.clear
      else
        obj
      end
    end

    def self.deep_merge(hash1, hash2, merged = {})
      hash1 ||= {}
      hash2 ||= {}
      merged.compare_by_identity

      # If we've already merged these two objects, return hash1 now.
      return hash1 if merged[hash1] && merged[hash1].include?(hash2.object_id)

      merged[hash1] ||= []
      merged[hash1] << hash2.object_id

      perform_deep_merge(hash1, hash2, merged)

      hash1
    end

    def self.perform_deep_merge(hash1, hash2, merged) # rubocop:disable Metrics/AbcSize
      hash2.each_key do |k|
        if hash1[k].is_a?(::Hash) && hash2[k].is_a?(::Hash)
          hash1[k] = deep_merge(hash1[k], hash2[k], merged)
        elsif hash1[k].is_a?(Array) && hash2[k].is_a?(Array)
          hash1[k] += deep_copy(hash2[k])
        elsif hash2[k]
          hash1[k] = deep_copy(hash2[k])
        end
      end
    end

    def self.truncate(str, length)
      ellipsis = '...'

      return str if str.length <= length || str.length <= ellipsis.length

      str.unpack('U*').slice(0, length - ellipsis.length).pack('U*') + ellipsis
    end

    def self.uuid_rollbar_url(data, configuration)
      "#{configuration.web_base}/instance/uuid?uuid=#{data[:uuid]}"
    end

    def self.enforce_valid_utf8(payload)
      normalizer = lambda { |object| Encoding.encode(object) }

      Util.iterate_and_update(payload, normalizer)
    end

    def self.count_method_in_stack(method_symbol, file_path = '')
      caller.grep(/#{file_path}.*#{method_symbol}/).count
    end

    def self.method_in_stack(method_symbol, file_path = '')
      count_method_in_stack(method_symbol, file_path) > 0
    end

    def self.method_in_stack_twice(method_symbol, file_path = '')
      count_method_in_stack(method_symbol, file_path) > 1
    end
  end
end