require 'nugrant/config'

module Nugrant
  class Bag < Hash

    ##
    # Create a new Bag object which holds key/value pairs.
    # The Bag object inherits from the Hash object, the main
    # differences with a normal Hash are indifferent access
    # (symbol or string) and method access (via method call).
    #
    # Hash objects in the map are converted to Bag. This ensure
    # proper nesting of functionality.
    #
    # =| Arguments
    #  * `elements`
    #    The initial elements the bag should be built with it.'
    #    Must be an object responding to `each` and accepting
    #    a block with two arguments: `key, value`. Defaults to
    #    the empty hash.
    #
    #  * `config`
    #    A Nugrant::Config object or hash passed to Nugrant::Config
    #    constructor. Used for `key_error` handler.
    #
    def initialize(elements = {}, config = {})
      super()

      @__config = Config::convert(config)

      (elements || {}).each do |key, value|
        self[key] = value
      end
    end

    def config=(config)
      @__config = Config::convert(config)
    end

    def method_missing(method, *args, &block)
      self[method]
    end

    ##
    ### Enumerable Overridden Methods (for string & symbol indifferent access)
    ##

    def count(item)
      super(__try_convert_item(item))
    end

    def find_index(item = nil, &block)
      block_given? ? super(&block) : super(__try_convert_item(item))
    end

    ##
    ### Hash Overridden Methods (for string & symbol indifferent access)
    ##

    def [](input)
      key = __convert_key(input)
      return @__config.key_error.call(key) if not key?(key)

      super(key)
    end

    def []=(input, value)
      super(__convert_key(input), __convert_value(value))
    end

    def assoc(key)
      super(__convert_key(key))
    end

    def delete(key)
      super(__convert_key(key))
    end

    def fetch(key, default = nil)
      super(__convert_key(key), default)
    end

    def has_key?(key)
      super(__convert_key(key))
    end

    def include?(item)
      super(__try_convert_item(item))
    end

    def key?(key)
      super(__convert_key(key))
    end

    def member?(item)
      super(__try_convert_item(item))
    end

    def dup()
      self.class.new(self, @__config.dup())
    end

    def merge(other, options = {})
      result = dup()
      result.merge!(other)
    end

    def merge!(other, options = {})
      other.each do |key, value|
        current = __get(key)
        case
          when current == nil
            self[key] = value

          when current.kind_of?(Hash) && value.kind_of?(Hash)
            current.merge!(value, options)

          when current.kind_of?(Array) && value.kind_of?(Array)
            strategy = options[:array_merge_strategy]
            if not Nugrant::Config.supported_array_merge_strategy(strategy)
              strategy = @__config.array_merge_strategy
            end

            self[key] = send("__#{strategy}_array_merge", current, value)

          when value != nil
            self[key] = value
        end
      end

      self
    end

    def store(key, value)
      self[key] = value
    end

    def to_hash(options = {})
      return {} if empty?()

      use_string_key = options[:use_string_key]

      Hash[map do |key, value|
        key = use_string_key ? key.to_s() : key
        value = __convert_value_to_hash(value, options)

        [key, value]
      end]
    end

    def walk(path = [], &block)
      each do |key, value|
        nested_bag = value.kind_of?(Nugrant::Bag)

        value.walk(path + [key], &block) if nested_bag
        yield path + [key], key, value if not nested_bag
      end
    end

    alias_method :to_ary, :to_a

    ##
    ### Private Methods
    ##

    private

    def __convert_key(key)
      return key.to_s().to_sym() if !key.nil? && key.respond_to?(:to_s)

      raise ArgumentError, "Key cannot be nil" if key.nil?
      raise ArgumentError, "Key cannot be converted to symbol, current value [#{key}] (#{key.class.name})"
    end

    ##
    # This function change value convertible to Bag into actual Bag.
    # This trick enable deeply using all Bag functionalities and also
    # ensures at the same time a deeply preventive copy since a new
    # instance is created for this nested structure.
    #
    # In addition, it also transforms array elements of type Hash into
    # Bag instance also. This enable indifferent access inside arrays
    # also.
    #
    # @param value The value to convert to bag if necessary
    # @return The converted value
    #
    def __convert_value(value)
      case
      # Converts Hash to Bag instance
      when value.kind_of?(Hash)
        Bag.new(value, @__config)

      # Converts Array elements to Bag instances if needed
      when value.kind_of?(Array)
        value.map(&self.method(:__convert_value))

      # Keeps as-is other elements
      else
        value
      end
    end

    ##
    # This function does the reversion of value conversion to Bag
    # instances. It transforms Bag instances to Hash (using to_hash)
    # and array Bag elements to Hash (using to_hash).
    #
    # The options parameters are pass to the to_hash function
    # when invoked.
    #
    # @param value The value to convert to hash
    # @param options The options passed to the to_hash function
    # @return The converted value
    #
    def __convert_value_to_hash(value, options = {})
      case
      # Converts Bag instance to Hash
      when value.kind_of?(Bag)
        value.to_hash(options)

      # Converts Array elements to Hash instances if needed
      when value.kind_of?(Array)
        value.map { |value| __convert_value_to_hash(value, options) }

      # Keeps as-is other elements
      else
        value
      end
    end

    def __get(key)
      # Calls Hash method [__convert_key(key)], used internally to retrieve value without raising Undefined parameter
      self.class.superclass.instance_method(:[]).bind(self).call(__convert_key(key))
    end

    ##
    # The concat order is the reversed compared to others
    # because we assume that new values have precedence
    # over current ones. Hence, the are prepended to
    # current values. This is also logical for parameters
    # because of the order in which bags are merged.
    #
    def __concat_array_merge(current_array, new_array)
      new_array + current_array
    end

    def __extend_array_merge(current_array, new_array)
      current_array | new_array
    end

    def __replace_array_merge(current_array, new_array)
      new_array
    end

    def __try_convert_item(args)
      return [__convert_key(args[0]), args[1]] if args.kind_of?(Array)

      __convert_key(args)
    rescue
      args
    end
  end
end
