/*
 * Copyright (c) 2011 Google, 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.google.common.truth;

import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Utility methods used in {@code Subject<T>} implementors.
 *
 * @author Christian Gruber
 * @author Jens Nyman
 */
final class SubjectUtils {
  private SubjectUtils() {}

  static final String HUMAN_UNDERSTANDABLE_EMPTY_STRING = "\"\" (empty String)";

  static <T> List<T> accumulate(T first, T second, T... rest) {
    // rest should never be deliberately null, so assume that the caller passed null
    // in the third position but intended it to be the third element in the array of values.
    // Javac makes the opposite inference, so handle that here.
    List<T> items = new ArrayList<T>(2 + ((rest == null) ? 1 : rest.length));
    items.add(first);
    items.add(second);
    if (rest == null) {
      items.add(null);
    } else {
      items.addAll(Arrays.asList(rest));
    }
    return items;
  }

  static <T> int countOf(T t, Iterable<T> items) {
    int count = 0;
    for (T item : items) {
      if (t == null ? (item == null) : t.equals(item)) {
        count++;
      }
    }
    return count;
  }

  static <T> List<Object> countDuplicates(Iterable<T> items) {
    // We use a List to de-dupe instead of a Set in case the elements don't have a proper
    // .hashCode() method (e.g., MessageSet from old versions of protobuf).
    List<T> itemSet = new ArrayList<T>();
    for (T item : items) {
      if (!itemSet.contains(item)) {
        itemSet.add(item);
      }
    }
    Object[] params = new Object[itemSet.size()];
    int n = 0;
    for (T item : itemSet) {
      int count = countOf(item, items);
      params[n++] = (count > 1) ? item + " [" + count + " copies]" : item;
    }
    return Arrays.asList(params);
  }

  /**
   * Makes a String representation of {@code items} with collapsed duplicates and additional class
   * info.
   *
   * <p>Example: {@code countDuplicatesAndAddTypeInfo([1, 2, 2, 3]) == "[1, 2 [3 copies]]
   * (java.lang.Integer)"} and {@code countDuplicatesAndAddTypeInfo([1, 2L]) == "[1
   * (java.lang.Integer), 2 (java.lang.Long)]"}.
   */
  static String countDuplicatesAndAddTypeInfo(Iterable<?> itemsIterable) {
    Collection<?> items = iterableToCollection(itemsIterable);
    Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);

    return homogeneousTypeName.isPresent()
        ? StringUtil.format("%s (%s)", countDuplicates(items), homogeneousTypeName.get())
        : countDuplicates(addTypeInfoToEveryItem(items)).toString();
  }

  /**
   * Makes a String representation of {@code items} with additional class info.
   *
   * <p>Example: {@code iterableToStringWithTypeInfo([1, 2]) == "[1, 2] (java.lang.Integer)"} and
   * {@code iterableToStringWithTypeInfo([1, 2L]) == "[1 (java.lang.Integer), 2 (java.lang.Long)]"}.
   */
  static String iterableToStringWithTypeInfo(Iterable<?> itemsIterable) {
    Collection<?> items = iterableToCollection(itemsIterable);
    Optional<String> homogeneousTypeName = getHomogeneousTypeName(items);

    if (homogeneousTypeName.isPresent()) {
      return StringUtil.format("%s (%s)", items, homogeneousTypeName.get());
    } else {
      return addTypeInfoToEveryItem(items).toString();
    }
  }

  /**
   * Returns a new collection containing all elements in {@code items} for which there exists at
   * least one element in {@code itemsToCheck} that has the same {@code toString()} value without
   * being equal.
   *
   * <p>Example: {@code retainMatchingToString([1L, 2L, 2L], [2, 3]) == [2L, 2L]}
   */
  static List<Object> retainMatchingToString(Iterable<?> items, Iterable<?> itemsToCheck) {
    SetMultimap<String, Object> stringValueToItemsToCheck = HashMultimap.create();
    for (Object itemToCheck : itemsToCheck) {
      stringValueToItemsToCheck.put(String.valueOf(itemToCheck), itemToCheck);
    }

    List<Object> result = Lists.newArrayList();
    for (Object item : items) {
      for (Object itemToCheck : stringValueToItemsToCheck.get(String.valueOf(item))) {
        if (!Objects.equal(itemToCheck, item)) {
          result.add(item);
          break;
        }
      }
    }
    return result;
  }

  /**
   * Returns true if there is a pair of an item from {@code items1} and one in {@code items2} that
   * has the same {@code toString()} value without being equal.
   *
   * <p>Example: {@code hasMatchingToStringPair([1L, 2L], [1]) == true}
   */
  static boolean hasMatchingToStringPair(Iterable<?> items1, Iterable<?> items2) {
    return !retainMatchingToString(items1, items2).isEmpty();
  }

  static String objectToTypeName(Object item) {
    if (item == null) {
      // The name "null type" comes from the interface javax.lang.model.type.NullType.
      return "null type";
    } else if (item instanceof Map.Entry) {
      Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
      return StringUtil.format(
          "Map.Entry<%s, %s>",
          objectToTypeName(entry.getKey()), objectToTypeName(entry.getValue()));
    } else {
      return item.getClass().getName();
    }
  }

  /**
   * Returns the name of the single type of all given items or {@link Optional#absent()} if no such
   * type exists.
   */
  private static Optional<String> getHomogeneousTypeName(Iterable<?> items) {
    Optional<String> homogeneousTypeName = Optional.absent();
    for (Object item : items) {
      if (item == null) {
        return Optional.absent();
      } else if (!homogeneousTypeName.isPresent()) {
        // This is the first item
        homogeneousTypeName = Optional.of(objectToTypeName(item));
      } else if (!objectToTypeName(item).equals(homogeneousTypeName.get())) {
        // items is a heterogeneous collection
        return Optional.absent();
      }
    }
    return homogeneousTypeName;
  }

  private static List<String> addTypeInfoToEveryItem(Iterable<?> items) {
    List<String> itemsWithTypeInfo = Lists.newArrayList();
    for (Object item : items) {
      itemsWithTypeInfo.add(StringUtil.format("%s (%s)", item, objectToTypeName(item)));
    }
    return itemsWithTypeInfo;
  }

  static <T> Collection<T> iterableToCollection(Iterable<T> iterable) {
    if (iterable instanceof Collection) {
      // Should be safe to assume that any Iterable implementing Collection isn't a one-shot
      // iterable, right? I sure hope so.
      return (Collection<T>) iterable;
    } else {
      return Lists.newArrayList(iterable);
    }
  }

  static <T> List<T> iterableToList(Iterable<T> iterable) {
    if (iterable instanceof List) {
      return (List<T>) iterable;
    } else {
      return Lists.newArrayList(iterable);
    }
  }

  /**
   * Returns an iterable with all empty strings replaced by a non-empty human understandable
   * indicator for an empty string.
   *
   * <p>Returns the given iterable if it contains no empty strings.
   */
  static <T> Iterable<T> annotateEmptyStrings(Iterable<T> items) {
    if (Iterables.contains(items, "")) {
      List<T> annotatedItems = Lists.newArrayList();
      for (T item : items) {
        if (Objects.equal(item, "")) {
          // This is a safe cast because know that at least one instance of T (this item) is a
          // String.
          @SuppressWarnings("unchecked")
          T newItem = (T) HUMAN_UNDERSTANDABLE_EMPTY_STRING;
          annotatedItems.add(newItem);
        } else {
          annotatedItems.add(item);
        }
      }
      return annotatedItems;
    } else {
      return items;
    }
  }
}
