require 'stringify-hash/version'
# A hash that treats Symbol and String keys interchangeably
# and recursively merges hashes
class StringifyHash < Hash

  # The dividor between elements when StringifyHash is dumped
  DIV = '    '

  # The end of line when dumping
  EOL = "\n"

  # Get value for given key, search for both k as String and k as Symbol,
  # if not present return nil
  #
  # @param [Object] k The key to find, searches for both k as String
  #                   and k as Symbol
  #
  # @example Use this method to return the value for a given key
  #     a['key'] = 'value'
  #     a['key'] == a[:key] == 'value'
  #
  # @return [nil, Object] Return the Object found at given key,
  #                       or nil if no Object found
  def [] k
    super(k.to_s) || super(k.to_sym)
  end

  # Set Symbol key to Object value
  # @param [Object] k The key to associated with the value,
  #                   converted to Symbol key
  # @param [Object] v The value to store in the ObjectHash
  #
  # @example Use this method to set the value for a key
  #     a['key'] = 'value'
  #
  # @return [Object] Return the Object value just stored
  def []=k,v
    super(k.to_sym, v)
  end

  # Determine if key is stored in ObjectHash
  # @param [Object] k The key to find in ObjectHash, searches for
  #                   both k as String and k as Symbol
  #
  # @example Use this method to set the value for a key
  #     a['key'] = 'value'
  #     a.has_key[:key] == true
  #
  # @return [Boolean]
  def has_key? k
    super(k.to_s) || super(k.to_sym)
  end

  # Determine key=>value entry in StringifyHash, remove both value at
  # String key and value at Symbol key
  #
  # @param [Object] k The key to delete in ObjectHash,
  # deletes both k as String and k as Symbol
  #
  # @example Use this method to set the value for a key
  #     a['key'] = 'value'
  #     a.delete[:key] == 'value'
  #
  # @return [Object, nil] The Object deleted at value,
  # nil if no Object deleted
  def delete k
    super(k.to_s) || super(k.to_sym)
  end

  # Recursively merge and StringifyHash with an OptionsHash or Hash
  #
  # @param [StringifyHash]       base The hash to merge into
  # @param [StringifyHash, Hash] hash The hash to merge from
  #
  # @example
  #   base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
  #   hash = { :key => { :subkey1 => 'newval'} }
  #
  #   rmerge(base, hash)
  #   #=>  {:key =>
  #           {:subkey1 => 'newval',
  #            :subkey2 => 'subval'}}
  #
  # @return [StringifyHash] The combined bash and hash
  def rmerge base, hash
    return base unless hash.is_a?(Hash) || hash.is_a?(self.class)
    hash.each do |key, v|
      if (base[key].is_a?(Hash) || base[key].is_a?(self.class)) && (hash[key].is_a?(Hash) || hash[key].is_a?(self.class))
        rmerge(base[key], hash[key])
      elsif hash[key].is_a?(Hash)
        base[key] = self.class.new.merge(hash[key])
      else
        base[key]= hash[key]
      end
    end
    base
  end

  # Create new StringifyHash from recursively merged self with an OptionsHash or Hash
  #
  # @param [StringifyHash, Hash] hash The hash to merge from
  #
  # @example
  #   base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
  #   hash = { :key => { :subkey1 => 'newval'} }
  #
  #   base.merge(hash)
  #   #=> {:key =>
  #         {:subkey1 => 'newval',
  #          :subkey2 => 'subval' }
  #
  # @return [StringifyHash] The combined hash
  def merge hash
    #make a deep copy into an empty hash object
    merged_hash = rmerge(self.class.new, self)
    rmerge(merged_hash, hash)
  end

  # Recursively merge self with an StringifyHash or Hash
  #
  # @param [StringifyHash, Hash] hash The hash to merge from
  #
  # @example
  #   base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
  #   hash = { :key => { :subkey1 => 'newval'} }
  #
  #   base.merge!(hash)
  #   #=> {:key =>
  #         {:subkey1 => 'newval',
  #          :subkey2 => 'subval' }
  #
  # @return [StringifyHash] The combined hash
  def merge! hash
    rmerge(self, hash)
  end

  # Helper for formatting collections
  # Computes the indentation level for elements of the collection
  # Yields indentation to block to so the caller can create
  # map of element strings
  # Places delimiters in the correct location
  # Joins everything with correct EOL
  #
  #
  # !@visibility private
  def as_coll( opening, closing, in_lvl, in_inc, &block )
    delim_indent = in_inc * in_lvl
    elem_indent  = in_inc * (in_lvl + 1)

    open_brace  = opening
    close_brace = delim_indent + closing

    fmtd_coll = block.call( elem_indent )
    str_coll = fmtd_coll.join( ',' + EOL )

    return open_brace + EOL + str_coll + EOL + close_brace
  end

  # Pretty prints a collection
  #
  # @param [Enumerable] collection The collection to be printed
  # @param [Integer]    in_lvl     The level of indentation
  # @param [String]     in_inc     The increment to indent
  #
  # @example
  #   base = {:key => { :subkey1 => 'subval', :subkey2 => ['subval'] }}
  #   self.fmt_collection( base )
  #   #=> '{
  #            "key": {
  #                "subkey": "subval",
  #                "subkey2": [
  #                    "subval"
  #                ]
  #            }
  #        }'
  #
  # @return [String] The collection as a pretty JSON object
  def fmt_collection( collection, in_lvl = 0, in_inc = DIV )
    if collection.respond_to? :each_pair
      string = fmt_assoc( collection, in_lvl, in_inc )
    else
      string = fmt_list( collection, in_lvl, in_inc )
    end

    return string
  end

  # Pretty prints an associative collection
  #
  # @param [#each_pair] coll    The collection to be printed
  # @param [Integer]    in_lvl  The level of indentation
  # @param [String]     in_inc  The increment to indent
  #
  # @example
  #   base = { :key => 'value', :key2 => 'value' }
  #   self.fmt_assoc( base )
  #   #=> '{
  #            "key": "value",
  #            "key2": "value"
  #        }'
  #
  # @return [String] The collection as a pretty JSON object
  def fmt_assoc( coll, in_lvl = 0, in_inc = DIV )
    if coll.empty?
      return '{}'
    else
      as_coll '{', '}', in_lvl, in_inc do |elem_indent|
        coll.map do |key, value|
          assoc_line = elem_indent + '"' + key.to_s + '"' + ': '
          assoc_line += fmt_value( value, in_lvl, in_inc )
        end
      end
    end
  end

  # Pretty prints a list collection
  #
  # @param [#each]    coll    The collection to be printed
  # @param [Integer]  in_lvl  The level of indentation
  # @param [String]   in_inc  The increment to indent
  #
  # @example
  #   base = [ 'first', 'second' ]
  #   self.fmt_list( base )
  #   #=> '[
  #            "first",
  #            "second"
  #        ]'
  #
  # @return [String] The collection as a pretty JSON object
  def fmt_list( coll, in_lvl = 0, in_inc = DIV )
    if coll.empty?
      return '[]'
    else
      as_coll '[', ']', in_lvl, in_inc do |indent|
        coll.map do |el|
          indent + fmt_value( el, in_lvl, in_inc )
        end
      end
    end
  end

  # Chooses between collection and primitive formatting
  #
  # !@visibility private
  def fmt_value( value, in_lvl = 0, in_inc = DIV )
    if value.kind_of? Enumerable and not value.is_a? String
      fmt_collection( value, in_lvl + 1, in_inc )
    else
      fmt_basic( value )
    end
  end

  # Pretty prints primitive JSON values
  #
  # @param [Object] value The collection to be printed
  #
  # @example
  #   self.fmt_value( 4 )
  #   #=> '4'
  #
  # @example
  #   self.fmt_value( true )
  #   #=> 'true'
  #
  # @example
  #   self.fmt_value( nil )
  #   #=> 'null'
  #
  # @example
  #   self.fmt_value( 'string' )
  #   #=> '"string"'
  #
  # @return [String] The value as a valid JSON primitive
  def fmt_basic( value )
    case value
    when Numeric, TrueClass, FalseClass then value.to_s
    when NilClass then "null"
    else "\"#{value}\""
    end
  end

  # Pretty print the options as JSON
  #
  # @example
  #   base = { :key => { :subkey1 => 'subval', :subkey2 => 'subval' } }
  #   base.dump
  #   #=>  '{
  #             "key": {
  #                 "subkey1": "subval",
  #                 "subkey2": 2
  #             }
  #         }
  #
  # @return [String] The description of self
  def dump
    fmt_collection( self, 0, DIV )
  end
end
