content: /*
   * Protocol Buffers - Google's data interchange format
   * Copyright 2014 Google Inc.  All rights reserved.
   * https://developers.google.com/protocol-buffers/
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions are
   * met:
   *
   *     * Redistributions of source code must retain the above copyright
   * notice, this list of conditions and the following disclaimer.
   *     * Redistributions in binary form must reproduce the above
   * copyright notice, this list of conditions and the following disclaimer
   * in the documentation and/or other materials provided with the
   * distribution.
   *     * Neither the name of Google Inc. nor the names of its
   * contributors may be used to endorse or promote products derived from
   * this software without specific prior written permission.
   *
   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
   * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
   * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   */
  
  package com.google.protobuf.jruby;
  
  import com.google.protobuf.Descriptors;
  import com.google.protobuf.DynamicMessage;
  import com.google.protobuf.MapEntry;
  import org.jruby.*;
  import org.jruby.anno.JRubyClass;
  import org.jruby.anno.JRubyMethod;
  import org.jruby.internal.runtime.methods.DynamicMethod;
  import org.jruby.runtime.Block;
  import org.jruby.runtime.ObjectAllocator;
  import org.jruby.runtime.ThreadContext;
  import org.jruby.runtime.builtin.IRubyObject;
  import org.jruby.util.ByteList;
  
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  @JRubyClass(name = "Map", include = "Enumerable")
  public class RubyMap extends RubyObject {
      public static void createRubyMap(Ruby runtime) {
          RubyModule protobuf = runtime.getClassFromPath("Google::Protobuf");
          RubyClass cMap = protobuf.defineClassUnder("Map", runtime.getObject(), new ObjectAllocator() {
              @Override
              public IRubyObject allocate(Ruby ruby, RubyClass rubyClass) {
                  return new RubyMap(ruby, rubyClass);
              }
          });
          cMap.includeModule(runtime.getEnumerable());
          cMap.defineAnnotatedMethods(RubyMap.class);
      }
  
      public RubyMap(Ruby ruby, RubyClass rubyClass) {
          super(ruby, rubyClass);
      }
  
      /*
       * call-seq:
       *     Map.new(key_type, value_type, value_typeclass = nil, init_hashmap = {})
       *     => new map
       *
       * Allocates a new Map container. This constructor may be called with 2, 3, or 4
       * arguments. The first two arguments are always present and are symbols (taking
       * on the same values as field-type symbols in message descriptors) that
       * indicate the type of the map key and value fields.
       *
       * The supported key types are: :int32, :int64, :uint32, :uint64, :bool,
       * :string, :bytes.
       *
       * The supported value types are: :int32, :int64, :uint32, :uint64, :bool,
       * :string, :bytes, :enum, :message.
       *
       * The third argument, value_typeclass, must be present if value_type is :enum
       * or :message. As in RepeatedField#new, this argument must be a message class
       * (for :message) or enum module (for :enum).
       *
       * The last argument, if present, provides initial content for map. Note that
       * this may be an ordinary Ruby hashmap or another Map instance with identical
       * key and value types. Also note that this argument may be present whether or
       * not value_typeclass is present (and it is unambiguously separate from
       * value_typeclass because value_typeclass's presence is strictly determined by
       * value_type). The contents of this initial hashmap or Map instance are
       * shallow-copied into the new Map: the original map is unmodified, but
       * references to underlying objects will be shared if the value type is a
       * message type.
       */
  
      @JRubyMethod(required = 2, optional = 2)
      public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
          this.table = new HashMap<IRubyObject, IRubyObject>();
          this.keyType = Utils.rubyToFieldType(args[0]);
          this.valueType = Utils.rubyToFieldType(args[1]);
  
          switch(keyType) {
              case INT32:
              case INT64:
              case UINT32:
              case UINT64:
              case BOOL:
              case STRING:
              case BYTES:
                  // These are OK.
                  break;
              default:
                  throw context.runtime.newArgumentError("Invalid key type for map.");
          }
  
          int initValueArg = 2;
          if (needTypeclass(this.valueType) && args.length > 2) {
              this.valueTypeClass = args[2];
              Utils.validateTypeClass(context, this.valueType, this.valueTypeClass);
              initValueArg = 3;
          } else {
              this.valueTypeClass = context.runtime.getNilClass();
          }
  
          // Table value type is always UINT64: this ensures enough space to store the
          // native_slot value.
          if (args.length > initValueArg) {
              mergeIntoSelf(context, args[initValueArg]);
          }
          return this;
      }
  
      /*
       * call-seq:
       *     Map.[]=(key, value) => value
       *
       * Inserts or overwrites the value at the given key with the given new value.
       * Throws an exception if the key type is incorrect. Returns the new value that
       * was just inserted.
       */
      @JRubyMethod(name = "[]=")
      public IRubyObject indexSet(ThreadContext context, IRubyObject key, IRubyObject value) {
          key = Utils.checkType(context, keyType, key, (RubyModule) valueTypeClass);
          value = Utils.checkType(context, valueType, value, (RubyModule) valueTypeClass);
          IRubyObject symbol;
          if (valueType == Descriptors.FieldDescriptor.Type.ENUM &&
                  Utils.isRubyNum(value) &&
                  ! (symbol = RubyEnum.lookup(context, valueTypeClass, value)).isNil()) {
              value = symbol;
          }
          this.table.put(key, value);
          return value;
      }
  
      /*
       * call-seq:
       *     Map.[](key) => value
       *
       * Accesses the element at the given key. Throws an exception if the key type is
       * incorrect. Returns nil when the key is not present in the map.
       */
      @JRubyMethod(name = "[]")
      public IRubyObject index(ThreadContext context, IRubyObject key) {
          if (table.containsKey(key))
              return this.table.get(key);
          return context.runtime.getNil();
      }
  
      /*
       * call-seq:
       *     Map.==(other) => boolean
       *
       * Compares this map to another. Maps are equal if they have identical key sets,
       * and for each key, the values in both maps compare equal. Elements are
       * compared as per normal Ruby semantics, by calling their :== methods (or
       * performing a more efficient comparison for primitive types).
       *
       * Maps with dissimilar key types or value types/typeclasses are never equal,
       * even if value comparison (for example, between integers and floats) would
       * have otherwise indicated that every element has equal value.
       */
      @JRubyMethod(name = "==")
      public IRubyObject eq(ThreadContext context, IRubyObject _other) {
          if (_other instanceof RubyHash)
              return toHash(context).op_equal(context, _other);
          RubyMap other = (RubyMap) _other;
          if (this == other) return context.runtime.getTrue();
          if (!typeCompatible(other) || this.table.size() != other.table.size())
              return context.runtime.getFalse();
          for (IRubyObject key : table.keySet()) {
              if (! other.table.containsKey(key))
                  return context.runtime.getFalse();
              if (! other.table.get(key).equals(table.get(key)))
                  return context.runtime.getFalse();
          }
          return context.runtime.getTrue();
      }
  
      /*
       * call-seq:
       *     Map.inspect => string
       *
       * Returns a string representing this map's elements. It will be formatted as
       * "{key => value, key => value, ...}", with each key and value string
       * representation computed by its own #inspect method.
       */
      @JRubyMethod
      public IRubyObject inspect() {
          return toHash(getRuntime().getCurrentContext()).inspect();
      }
  
      /*
       * call-seq:
       *     Map.hash => hash_value
       *
       * Returns a hash value based on this map's contents.
       */
      @JRubyMethod
      public IRubyObject hash(ThreadContext context) {
          try {
              MessageDigest digest = MessageDigest.getInstance("SHA-256");
              for (IRubyObject key : table.keySet()) {
                  digest.update((byte) key.hashCode());
                  digest.update((byte) table.get(key).hashCode());
              }
              return context.runtime.newString(new ByteList(digest.digest()));
          } catch (NoSuchAlgorithmException ignore) {
              return context.runtime.newFixnum(System.identityHashCode(table));
          }
      }
  
      /*
       * call-seq:
       *     Map.keys => [list_of_keys]
       *
       * Returns the list of keys contained in the map, in unspecified order.
       */
      @JRubyMethod
      public IRubyObject keys(ThreadContext context) {
          return RubyArray.newArray(context.runtime, table.keySet());
      }
  
      /*
       * call-seq:
       *     Map.values => [list_of_values]
       *
       * Returns the list of values contained in the map, in unspecified order.
       */
      @JRubyMethod
      public IRubyObject values(ThreadContext context) {
          return RubyArray.newArray(context.runtime, table.values());
      }
  
      /*
       * call-seq:
       *     Map.clear
       *
       * Removes all entries from the map.
       */
      @JRubyMethod
      public IRubyObject clear(ThreadContext context) {
          table.clear();
          return context.runtime.getNil();
      }
  
      /*
       * call-seq:
       *     Map.each(&block)
       *
       * Invokes &block on each |key, value| pair in the map, in unspecified order.
       * Note that Map also includes Enumerable; map thus acts like a normal Ruby
       * sequence.
       */
      @JRubyMethod
      public IRubyObject each(ThreadContext context, Block block) {
          for (IRubyObject key : table.keySet()) {
              block.yieldSpecific(context, key, table.get(key));
          }
          return context.runtime.getNil();
      }
  
      /*
       * call-seq:
       *     Map.delete(key) => old_value
       *
       * Deletes the value at the given key, if any, returning either the old value or
       * nil if none was present. Throws an exception if the key is of the wrong type.
       */
      @JRubyMethod
      public IRubyObject delete(ThreadContext context, IRubyObject key) {
          return table.remove(key);
      }
  
      /*
       * call-seq:
       *     Map.has_key?(key) => bool
       *
       * Returns true if the given key is present in the map. Throws an exception if
       * the key has the wrong type.
       */
      @JRubyMethod(name = "has_key?")
      public IRubyObject hasKey(ThreadContext context, IRubyObject key) {
          return this.table.containsKey(key) ? context.runtime.getTrue() : context.runtime.getFalse();
      }
  
      /*
       * call-seq:
       *     Map.length
       *
       * Returns the number of entries (key-value pairs) in the map.
       */
      @JRubyMethod
      public IRubyObject length(ThreadContext context) {
          return context.runtime.newFixnum(this.table.size());
      }
  
      /*
       * call-seq:
       *     Map.dup => new_map
       *
       * Duplicates this map with a shallow copy. References to all non-primitive
       * element objects (e.g., submessages) are shared.
       */
      @JRubyMethod
      public IRubyObject dup(ThreadContext context) {
          RubyMap newMap = newThisType(context);
          for (Map.Entry<IRubyObject, IRubyObject> entry : table.entrySet()) {
              newMap.table.put(entry.getKey(), entry.getValue());
          }
          return newMap;
      }
  
      @JRubyMethod(name = {"to_h", "to_hash"})
      public RubyHash toHash(ThreadContext context) {
          return RubyHash.newHash(context.runtime, table, context.runtime.getNil());
      }
  
      // Used by Google::Protobuf.deep_copy but not exposed directly.
      protected IRubyObject deepCopy(ThreadContext context) {
          RubyMap newMap = newThisType(context);
          switch (valueType) {
              case MESSAGE:
                  for (IRubyObject key : table.keySet()) {
                      RubyMessage message = (RubyMessage) table.get(key);
                      newMap.table.put(key.dup(), message.deepCopy(context));
                  }
                  break;
              default:
                  for (IRubyObject key : table.keySet()) {
                      newMap.table.put(key.dup(), table.get(key).dup());
                  }
          }
          return newMap;
      }
  
      protected List<DynamicMessage> build(ThreadContext context, RubyDescriptor descriptor) {
          List<DynamicMessage> list = new ArrayList<DynamicMessage>();
          RubyClass rubyClass = (RubyClass) descriptor.msgclass(context);
          Descriptors.FieldDescriptor keyField = descriptor.lookup("key").getFieldDef();
          Descriptors.FieldDescriptor valueField = descriptor.lookup("value").getFieldDef();
          for (IRubyObject key : table.keySet()) {
              RubyMessage mapMessage = (RubyMessage) rubyClass.newInstance(context, Block.NULL_BLOCK);
              mapMessage.setField(context, keyField, key);
              mapMessage.setField(context, valueField, table.get(key));
              list.add(mapMessage.build(context));
          }
          return list;
      }
  
      protected RubyMap mergeIntoSelf(final ThreadContext context, IRubyObject hashmap) {
          if (hashmap instanceof RubyHash) {
              ((RubyHash) hashmap).visitAll(new RubyHash.Visitor() {
                  @Override
                  public void visit(IRubyObject key, IRubyObject val) {
                      indexSet(context, key, val);
                  }
              });
          } else if (hashmap instanceof RubyMap) {
              RubyMap other = (RubyMap) hashmap;
              if (!typeCompatible(other)) {
                  throw context.runtime.newTypeError("Attempt to merge Map with mismatching types");
              }
          } else {
              throw context.runtime.newTypeError("Unknown type merging into Map");
          }
          return this;
      }
  
      protected boolean typeCompatible(RubyMap other) {
          return this.keyType == other.keyType &&
                  this.valueType == other.valueType &&
                  this.valueTypeClass == other.valueTypeClass;
      }
  
      private RubyMap newThisType(ThreadContext context) {
          RubyMap newMap;
          if (needTypeclass(valueType)) {
              newMap = (RubyMap) metaClass.newInstance(context,
                      Utils.fieldTypeToRuby(context, keyType),
                      Utils.fieldTypeToRuby(context, valueType),
                      valueTypeClass, Block.NULL_BLOCK);
          } else {
              newMap = (RubyMap) metaClass.newInstance(context,
                      Utils.fieldTypeToRuby(context, keyType),
                      Utils.fieldTypeToRuby(context, valueType),
                      Block.NULL_BLOCK);
          }
          newMap.table = new HashMap<IRubyObject, IRubyObject>();
          return newMap;
      }
  
      private boolean needTypeclass(Descriptors.FieldDescriptor.Type type) {
          switch(type) {
              case MESSAGE:
              case ENUM:
                  return true;
              default:
                  return false;
          }
      }
  
      private Descriptors.FieldDescriptor.Type keyType;
      private Descriptors.FieldDescriptor.Type valueType;
      private IRubyObject valueTypeClass;
      private Map<IRubyObject, IRubyObject> table;
  }
  