/*
 * Copyright 2015-present Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

package com.facebook.buck.json;

import com.facebook.buck.util.hashing.StringHashing;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.hash.Hasher;
import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;

/** Hashes parsed BUCK file objects. */
public class JsonObjectHashing {
  private enum HashedObjectType {
    BOOLEAN,
    DOUBLE,
    FLOAT,
    INTEGER,
    LIST,
    LONG,
    SHORT,
    STRING,
    MAP,
    NULL
  }

  // Utility class; do not instantiate.
  private JsonObjectHashing() {}

  /**
   * Given a {@link Hasher} and a parsed BUCK file object, updates the Hasher with the contents of
   * the JSON object.
   */
  public static void hashJsonObject(Hasher hasher, @Nullable Object obj) {
    if (obj instanceof SkylarkNestedSet) {
      obj = ((SkylarkNestedSet) obj).toCollection();
    }
    if (obj instanceof Map) {
      Map<?, ?> map = (Map<?, ?>) obj;
      ImmutableSortedMap.Builder<String, Optional<Object>> sortedMapBuilder =
          ImmutableSortedMap.naturalOrder();
      for (Map.Entry<?, ?> entry : map.entrySet()) {
        Object key = entry.getKey();
        if (!(key instanceof String)) {
          throw new RuntimeException(
              String.format(
                  "Keys of JSON maps are expected to be strings. Actual type: %s, contents: %s",
                  key.getClass().getName(), key));
        }
        Object value = entry.getValue();
        if (value != null) {
          sortedMapBuilder.put((String) key, Optional.of(value));
        }
      }
      ImmutableSortedMap<String, Optional<Object>> sortedMap = sortedMapBuilder.build();
      hasher.putInt(HashedObjectType.MAP.ordinal());
      hasher.putInt(sortedMap.size());
      for (Map.Entry<String, Optional<Object>> entry : sortedMap.entrySet()) {
        hashJsonObject(hasher, entry.getKey());
        if (entry.getValue().isPresent()) {
          hashJsonObject(hasher, entry.getValue().get());
        } else {
          hashJsonObject(hasher, null);
        }
      }
    } else if (obj instanceof Collection) {
      Collection<?> collection = (Collection<?>) obj;
      hasher.putInt(HashedObjectType.LIST.ordinal());
      hasher.putInt(collection.size());
      for (Object collectionEntry : collection) {
        hashJsonObject(hasher, collectionEntry);
      }
    } else if (obj instanceof String) {
      hasher.putInt(HashedObjectType.STRING.ordinal());
      String s = (String) obj;
      StringHashing.hashStringAndLength(hasher, s);
    } else if (obj instanceof Boolean) {
      hasher.putInt(HashedObjectType.BOOLEAN.ordinal());
      hasher.putBoolean((boolean) obj);
    } else if (obj instanceof Number) {
      // This is gross, but it mimics the logic originally in RawParser.
      Number number = (Number) obj;
      if (number.longValue() == number.doubleValue()) {
        hasher.putInt(HashedObjectType.LONG.ordinal());
        hasher.putLong(number.longValue());
      } else {
        hasher.putInt(HashedObjectType.DOUBLE.ordinal());
        hasher.putDouble(number.doubleValue());
      }
    } else if (obj instanceof Void || obj == null) {
      hasher.putInt(HashedObjectType.NULL.ordinal());
    } else {
      throw new RuntimeException(
          String.format("Unsupported object %s (class %s)", obj, obj.getClass()));
    }
  }
}

@@ -777,16 +777,133 @@
 Hasher;%0A
+import com.google.devtools.build.lib.syntax.SelectorList;%0Aimport com.google.devtools.build.lib.syntax.SelectorValue;%0A
 import c
@@ -985,16 +985,39 @@
 ection;%0A
+import java.util.List;%0A
 import j
@@ -1304,16 +1304,55 @@
 AP,%0A
+    SELECTOR_LIST,%0A    SELECTOR_VALUE,%0A
     NULL
 %0A  %7D
@@ -1347,16 +1347,17 @@
     NULL
+,
 %0A  %7D%0A%0A  
@@ -4077,24 +4077,680 @@
 ordinal());%0A
+    %7D else if (obj instanceof SelectorList) %7B%0A      SelectorList selectorList = (SelectorList) obj;%0A      hasher.putInt(HashedObjectType.SELECTOR_LIST.ordinal());%0A      List%3CObject%3E elements = selectorList.getElements();%0A      hasher.putInt(elements.size());%0A      for (Object collectionEntry : elements) %7B%0A        hashJsonObject(hasher, collectionEntry);%0A      %7D%0A    %7D else if (obj instanceof SelectorValue) %7B%0A      SelectorValue selectorValue = (SelectorValue) obj;%0A      hasher.putInt(HashedObjectType.SELECTOR_VALUE.ordinal());%0A      hashJsonObject(hasher, selectorValue.getDictionary());%0A      hashJsonObject(hasher, selectorValue.getNoMatchError());%0A
     %7D else %7B

Add support for hashing of SelectorList and SelectorValue