package org.unicode.cldr.draft.keyboard;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Iterator;
import java.util.Set;

import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Sets;

/**
 * Object containing the combination of modifier keys that must be on and off for a particular
 * combination to be activated. All other keys are considered "don't care keys". This simulates
 * boolean logic for a 3 state system.
 *
 * <p>
 * For example, suppose we have three keys, "A", "B" and "C". Then the boolean expression:
 * {@code A AND !B} means that A must be on, B must be off and C is a don't care.
 *
 * <p>
 * Some keys may have L and R variants (like Control which has a Left-Control and a Right-Control).
 * When the situation occurs that the parent key (the variant without a left or right suffix) is
 * included into the on keys or off keys sets, then the children are included into the don't care
 * pool and are omitted when the combination is printed out.
 *
 * <p>
 * For example, suppose we have three keys "A", "B" and "C" and "A" has a left and right variant
 * which are "A-Right" and "A-Left" respectively. Continuing the example above, the keys fall into
 * the following categories:
 * <ul>
 * <li>ON: { A }
 * <li>OFF: { B }
 * <li>DON'T CARE: { C, A-Left, A-Right }
 * </ul>
 * <p>
 * However when printing out the combination, we would exclude the A-Left and A-Right keys because
 * their parent is already included in the ON set. Therefore the output result would be:
 * {@code A+C?}.
 *
 * <p>
 * A slightly different behavior exists for parent modifier keys. When a parent modifier key is
 * given then its children are also added to the don't care keys pool. Once again, the
 * simplification is shown when printing out the combination.
 *
 * <p>
 * For example, continuing the above example but this time assuming that C has a left and right
 * variant. Therefore the breakdown looks like:
 * <ul>
 * <li>ON: { A }
 * <li>OFF: { B }
 * <li>DON'T CARE: { C, C-Left, C-Right, A-Left, A-Right }
 * </ul>
 */
public final class ModifierKeyCombination implements Comparable<ModifierKeyCombination> {
  public static final ModifierKeyCombination BASE = ModifierKeyCombination.ofOnKeys(
      ImmutableSet.<ModifierKey>of());

  private final ImmutableSet<ModifierKey> onKeys;
  private final ImmutableSet<ModifierKey> offKeys;

  private ModifierKeyCombination(ImmutableSet<ModifierKey> onKeys, ImmutableSet<ModifierKey> offKeys) {
    this.onKeys = checkNotNull(onKeys);
    this.offKeys = checkNotNull(offKeys);
  }

  /**
   * Create a modifier key combination from a set of ON keys. This is the most common factory
   * method since most sources will provide the keys that MUST be on. Simplifies the set as
   * needed.
   */
  public static ModifierKeyCombination ofOnKeys(Set<ModifierKey> onKeys) {
    return ofOnAndDontCareKeys(ImmutableSet.copyOf(onKeys), ImmutableSet.<ModifierKey>of());
  }

  /**
   * Create a modifier key combination from a set of ON keys and a set of DON'T CARE keys.
   * That is a set of keys that MUST be ON and a set of keys that CAN be ON or OFF. Simplifies
   * the sets as needed.
   */
  public static ModifierKeyCombination ofOnAndDontCareKeys(Set<ModifierKey> onKeys,
      Set<ModifierKey> dontCareKeys) {
    checkArgument(Sets.intersection(onKeys, dontCareKeys).size() == 0,
        "On keys and don't care keys must be disjoint");
    return ModifierKeySimplifier.simplifyInput(ImmutableSet.copyOf(onKeys),
        ImmutableSet.copyOf(dontCareKeys));
  }

  /** Internal. */
  static ModifierKeyCombination of(ImmutableSet<ModifierKey> onKeys,
      ImmutableSet<ModifierKey> offKeys) {
    return new ModifierKeyCombination(onKeys, offKeys);
  }

  /** Returns the set of keys that have to be ON for this combination to be active. */
  public ImmutableSet<ModifierKey> onKeys() {
    return onKeys;
  }

  /** Returns the set of keys that have to be OFF for this combination to be active. */
  public ImmutableSet<ModifierKey> offKeys() {
    return offKeys;
  }

  /**
   * Determines if this combination is a base combination. That is, is this combination valid when
   * no modifier keys are pressed?
   */
  public boolean isBase() {
    return onKeys.isEmpty();
  }

  @Override public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o instanceof ModifierKeyCombination) {
      ModifierKeyCombination other = (ModifierKeyCombination) o;
      return onKeys.equals(other.onKeys) && offKeys.equals(other.offKeys);
    }
    return false;
  }

  @Override public int hashCode() {
    return Objects.hashCode(onKeys, offKeys);
  }

  @Override public String toString() {
    // TODO: cache this result.
    return ModifierKeySimplifier.simplifyToString(this);
  }

  @Override public int compareTo(ModifierKeyCombination o) {
    // Compare on keys first.
    ImmutableSortedSet<ModifierKey> sortedOnKeys1 = ImmutableSortedSet.copyOf(onKeys);
    ImmutableSortedSet<ModifierKey> sortedOnKeys2 = ImmutableSortedSet.copyOf(o.onKeys);
    int result = compareSetsDescending(sortedOnKeys1, sortedOnKeys2);
    // If they are identical, compare off keys (this will be the opposite from the result from the
    // on keys because what we really want is the order for the don't care keys which are simply the
    // converse of the off keys)
    // Here is a simple illustrative example:
    // -Suppose Alphabetic order within a combination
    // -Suppose reverse Alphabetic order between combinations
    // -Suppose four keys {A, B, C, D}
    // -Suppose two combinations, A.B.~D (A+B+C?) and A.B.~C (A+B+D?).
    // We want them ordered: A+B+D? A+B+C?
    // Clearly, AB are identical in both so we move onto the off keys: ~D and ~C respectively.
    // According to our initial comparison, ~D comes before ~C, but this is incorrect when looking
    // at the don't care keys which are C? and D? respectively. This is why we multiply the result
    // received from the off keys comparison by -1.
    //
    // More on the reverse ordering scheme between combinations:
    // Within a combination: A+B+C (A comes before B and B comes before C)
    // Between combinations: Suppose two combinations, A+C and B+C. Then the order would be B+C A+C.
    // (Reverse ordering so B comes before A).
    if (result == 0) {
      ImmutableSortedSet<ModifierKey> sortedOffKeys1 = ImmutableSortedSet.copyOf(offKeys);
      ImmutableSortedSet<ModifierKey> sortedOffKeys2 = ImmutableSortedSet.copyOf(o.offKeys);
      return -1 * compareSetsDescending(sortedOffKeys1, sortedOffKeys2);
    } else {
      return result;
    }
  }

  /**
   * Compare two sets of modifier key elements. Returns a negative integer if {@code set1} is
   * less than {@code set2}, a positive integer if {@code set1} is greater than {@code set2} and 0
   * if both sets are equal.
   *
   * <p>
   * Compares the sets based on the reverse ordering of the natural order imposed by the
   * modifier key enum. This is the convention used in the LDML Keyboard Standard.
   */
  private static int compareSetsDescending(
      ImmutableSortedSet<ModifierKey> set1, ImmutableSortedSet<ModifierKey> set2) {
    Iterator<ModifierKey> iterator1 = set1.iterator();
    Iterator<ModifierKey> iterator2 = set2.iterator();
    // Compare on keys until a difference is found.
    while (iterator1.hasNext() && iterator2.hasNext()) {
      ModifierKey modifierKey1 = iterator1.next();
      ModifierKey modifierKey2 = iterator2.next();
      if (modifierKey1.compareTo(modifierKey2) < 0) {
        return 1;
      } else if (modifierKey1.compareTo(modifierKey2) > 0) {
        return -1;
      }
    }
    // If the first x elements are identical, then the set with more modifier keys should come
    // after the set with less.
    return set1.size() - set2.size();
  }
}
