module Protobuf
  module Field
    class FieldHash < Hash

      ##
      # Attributes
      #

      attr_reader :field, :key_field, :value_field

      ##
      # Constructor
      #

      def initialize(field)
        @field = field
        @key_field = field.type_class.get_field(:key)
        @value_field = field.type_class.get_field(:value)
      end

      ##
      # Public Instance Methods
      #

      def []=(key, val)
        super(normalize_key(key), normalize_val(val))
      end

      alias store []=

      def replace(val)
        raise_type_error(val) unless val.is_a?(Hash)
        clear
        update(val)
      end

      def merge!(other)
        raise_type_error(other) unless other.is_a?(Hash)
        # keys and values will be normalized by []= above
        other.each { |k, v| self[k] = v }
      end

      alias update merge!

      # Return a hash-representation of the given values for this field type.
      # The value in this case would be the hash itself, right? Unfortunately
      # not because the values of the map could be messages themselves that we
      # need to transform.
      def to_hash_value
        each_with_object({}) do |(key, value), hash|
          hash[key] = value.respond_to?(:to_hash_value) ? value.to_hash_value : value
        end
      end

      # Return a hash-representation of the given values for this field type
      # that is safe to convert to JSON.
      #
      # The value in this case would be the hash itself, right? Unfortunately
      # not because the values of the map could be messages themselves that we
      # need to transform.
      def to_json_hash_value
        if field.respond_to?(:json_encode)
          each_with_object({}) do |(key, value), hash|
            hash[key] = field.json_encode(value)
          end
        else
          each_with_object({}) do |(key, value), hash|
            hash[key] = value.respond_to?(:to_json_hash_value) ? value.to_json_hash_value : value
          end
        end
      end

      def to_s
        "{#{field.name}}"
      end

      private

      ##
      # Private Instance Methods
      #

      def normalize_key(key)
        normalize(:key, key, key_field)
      end

      def normalize_val(value)
        normalize(:value, value, value_field)
      end

      def normalize(what, value, normalize_field)
        raise_type_error(value) if value.nil?
        value = value.to_proto if value.respond_to?(:to_proto)
        fail TypeError, "Unacceptable #{what} #{value} for field #{field.name} of type #{normalize_field.type_class}" unless normalize_field.acceptable?(value)

        if normalize_field.is_a?(::Protobuf::Field::EnumField)
          fetch_enum(normalize_field.type_class, value)
        elsif normalize_field.is_a?(::Protobuf::Field::MessageField) && value.is_a?(normalize_field.type_class)
          value
        elsif normalize_field.is_a?(::Protobuf::Field::MessageField) && value.respond_to?(:to_hash)
          normalize_field.type_class.new(value.to_hash)
        else
          value
        end
      end

      def fetch_enum(type, val)
        en = type.fetch(val)
        raise_type_error(val) if en.nil?
        en
      end

      def raise_type_error(val)
        fail TypeError, <<-TYPE_ERROR
          Expected map value of type '#{key_field.type_class} -> #{value_field.type_class}'
          Got '#{val.class}' for map protobuf field #{field.name}
        TYPE_ERROR
      end

    end
  end
end