module Litany
  # Inspired by Hashie components, unfortunately I couldn't get Mash to do what I wanted.
  # This class allows for pervasive indifferent access and method based access of data.
  # Ensures that `Indicium.new {|h, k| h[k] = Indicium.new(&h.default_proc) }` will work.
  # noinspection RubyTooManyMethodsInspection
  class Indicium < Hash
    include DeepMerge::DeepMergeHash

    # Indifferent Access
    def initialize(source = nil, default = nil, &block)
      deep_merge!(standardize_value(source)) unless source.nil?
      default.nil? ? super(&block) : super(default)
    end

    def <(other)
      super(standardize_value(other))
    end

    def <=(other)
      super(standardize_value(other))
    end

    def ==(other)
      super(standardize_value(other))
    end

    def >(other)
      super(standardize_value(other))
    end

    def >=(other)
      super(standardize_value(other))
    end

    def [](key)
      super(standardize_key(key))
    end

    def []=(key, value)
      super(standardize_key(key), standardize_value(value))
    end

    def assoc(obj)
      super(standardize_key(obj))
    end

    def default(key = nil)
      super(standardize_key(key))
    end

    def delete(key)
      super(standardize_key(key))
    end

    def dig(*keys)
      super(*standardize_key(keys))
    end

    def eql?(other)
      super(standardize_value(other))
    end

    def fetch(key, *args, &block)
      super(standardize_key(key), *args, &block)
    end

    def fetch_values(*keys, &block)
      super(standardize_key(keys), &block)
    end

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

    def include?(key)
      super(standardize_key(key))
    end

    def invert
      super(collect { |k, v| [k, standardize_key(v)] }.to_h)
    end

    def key?(key)
      super(standardize_key(key))
    end

    def member?(key)
      super(standardize_key(key))
    end

    def merge(other, &block)
      super(standardize_value(other), &block)
    end

    def merge!(other, &block)
      super(standardize_value(other), &block)
    end

    def replace(other)
      super(standardize_value(other))
    end

    def store(key, value)
      super(standardize_key(key), standardize_value(value))
    end

    def update(other, &block)
      super(standardize_value(other), &block)
    end

    def values_at(*keys)
      super(*standardize_key(keys))
    end

    # Method based access
    def respond_to_missing?(*)
      true
    end

    def method_missing(name, value = nil, &block)
      key, op = method_parts(name)
      raise "Cannot perform method based value operations on `#{key}` as it is an instance method." if self.class.instance_methods.include? key

      case op
        when '='
          self[key] = value
        when '?'
          key?(key)
        when nil
          self[key]
        else
          super
      end
    end

    private

    def method_parts(method_name)
      method_name =~ /(.*)[?|=|!]/ ? [method_name[0..-2].to_sym, method_name[-1]] : [method_name.to_sym, nil]
    end

    def standardize_array(array)
      array.collect { |v| v.is_a?(Hash) ? standardize_hash(v) : v }
    end

    def standardize_hash(hash)
      standardized = hash.collect do |k, v|
        v = case v
          when Array
            standardize_array(v)
          when Hash
            standardize_hash(v)
          else
            v
        end
        [standardize_key(k), v]
      end

      standardized.to_h
    end

    def standardize_key(key)
      case key
        when Array
          key.collect { |k| standardize_key(k) }
        else
          key.respond_to?(:to_sym) ? key.to_sym : key
      end
    end

    def standardize_value(value)
      case value
        when Indicium
          value
        when Hash
          standardize_hash(value)
        when Array
          standardize_array(value)
        else
          value
      end
    end
  end
end
