# frozen_string_literal: true

module KeyTree
  module Refine
    # Refinements to Hash for deep_ methods, for traversing nested structures
    module DeepHash
      refine Hash do
        # Return a deep enumerator for all (+key_path+, +value+) pairs in a
        # nested hash structure.
        #
        # :call-seq:
        #   deep => Enumerator
        def deep
          Enumerator.new do |yielder|
            deep_enumerator(yielder)
          end
        end

        # Fetch a leaf value from a nested hash structure
        #
        # :call-seq:
        #   deep_fetch(key_path) => value
        #   deep_fetch(key_path, default) => value || default
        #   deep_fetch(key_path) { |key_path| block } => value || block
        def deep_fetch(key_path, *default)
          catch do |ball|
            result = key_path.reduce(self) do |hash, key|
              throw ball unless hash.is_a?(Hash)
              hash.fetch(key) { throw ball }
            end
            return result unless result.is_a?(Hash)
          end
          return yield(key_path) if block_given?
          return default.first unless default.empty?

          raise KeyError, %(key path invalid: "#{key_path}")
        end

        # Store a new value in a nested hash structure, expanding it
        # if necessary.
        #
        # :call-seq:
        #   deep_store(key_path, new_value) => new_value
        #
        # Raises KeyError if a prefix of the +key_path+ has a value.
        def deep_store(key_path, new_value)
          *prefix_path, last_key = key_path
          result = prefix_path.reduce(self) do |hash, key|
            result = hash.fetch(key) { hash[key] = {} }
            next result if result.is_a?(Hash)

            raise KeyError, %(prefix has value: "#{key_path}")
          end
          result[last_key] = new_value
        end

        # Delete a leaf value in a nested hash structure
        #
        # :call-seq:
        #   deep_delete(key_path)
        #
        # Raises KeyError if a prefix of the +key_path+ has a value.
        def deep_delete(key_path)
          *prefix_path, last_key = key_path
          result = prefix_path.reduce(self) do |hash, key|
            result = hash.fetch(key, nil)
            next result if result.is_a?(Hash)

            raise KeyError, %(prefix has value: "#{key_path}")
          end
          result.delete(last_key)
        end

        # Deeply merge nested hash structures
        #
        # :call-seq:
        #   deep_merge!(other) => self
        #   deep_merge!(other) { |key_path, lhs, rhs| } => self
        def deep_merge!(other, prefix = [], &block)
          merge!(other) do |key, lhs, rhs|
            key_path = prefix + [key]
            both_are_hashes = lhs.is_a?(Hash) && rhs.is_a?(Hash)
            next lhs.deep_merge!(rhs, key_path, &block) if both_are_hashes
            next yield(key_path, lhs, rhs) unless block.nil?

            rhs
          end
        end

        # Deeply merge nested hash structures
        #
        # :call-seq:
        #   deep_merge(other) => self
        #   deep_merge(other) { |key_path, lhs, rhs| } => self
        def deep_merge(other, prefix = [], &block)
          merge(other) do |key, lhs, rhs|
            key_path = prefix + [key]
            both_are_hashes = lhs.is_a?(Hash) && rhs.is_a?(Hash)
            next lhs.deep_merge(rhs, key_path, &block) if both_are_hashes
            next yield(key_path, lhs, rhs) unless block.nil?

            rhs
          end
        end

        # Transform keys in a nested hash structure
        #
        # :call-seq:
        #   deep_transform_keys { |key| block }
        def deep_transform_keys(&block)
          result = transform_keys(&block)
          result.transform_values! do |value|
            next value unless value.is_a?(Hash)

            value.deep_transform_keys(&block)
          end
        end

        # Transform keys in a nested hash structure
        #
        # :call-seq:
        #   deep_transform_keys! { |key| block }
        def deep_transform_keys!(&block)
          result = transform_keys!(&block)
          result.transform_values! do |value|
            next value unless value.is_a?(Hash)

            value.deep_transform_keys!(&block)
          end
        end

        # Comvert any keys containing a +.+ in a hash structure
        # to nested hashes.
        #
        # :call-seq:
        #   deep_key_pathify => Hash
        def deep_key_pathify
          each_with_object({}) do |(key, value), result|
            key_path = Path[key]
            value = value.deep_key_pathify if value.is_a?(Hash)
            result.deep_store(key_path, value)
          end
        end

        def deep_enumerator(yielder, prefix = [])
          each do |key, value|
            key_path = prefix + [key]
            yielder << [key_path, value]
            value.deep_enumerator(yielder, key_path) if value.is_a?(Hash)
          end
        end
      end
    end
  end
end

require_relative '../path'