package cse1030;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

/**
 * An immutable class representing a playing card from a standard 52 card deck.
 * A playing card is identified by its suit (clubs, diamond, heart, spade) and
 * its rank (two through ten, jack, queen, king, and ace). Both the suit and
 * rank are represented with strings.
 * 
 * The class is implemented as a multiton; there is only one
 * <code>PlayingCardM</code> object for each unique combination of rank and
 * suit.
 * 
 * 
 * @author CSE1030
 * 
 */
public final class PlayingCardM implements Comparable<PlayingCardM> {

  private static final String[] RANKS = { "2", "3", "4", "5", "6", "7", "8",
      "9", "10", "J", "Q", "K", "A" };
  private static final String[] SUITS = { "S", "C", "D", "H" };

  private static final Map<String, PlayingCardM> instances = 
      new HashMap<String, PlayingCardM>();

  private final String rank;
  private final String suit;

  /**
   * Create a playing card with a given rank and suit.
   * 
   * <p>
   * Valid ranks are "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q",
   * "K", "A" (the number cards 2 through 10, and the face cards jack, queen,
   * king, and ace).
   * 
   * <p>
   * Valid suits are "H", "D", "C", "S" (the suits hearts, diamonds, clubs, and
   * spades).
   * 
   * @param rank
   *          The rank of the card.
   * @param suit
   *          The suit of the card.
   * @throws <code>IllegalArgumentException</code> if the rank or the suit is
   *         not valid.
   */
  private PlayingCardM(String rank, String suit) {
    boolean valueOk = Arrays.asList(RANKS).contains(rank);
    boolean suitOk = Arrays.asList(SUITS).contains(suit);
    if (!valueOk) {
      throw new IllegalArgumentException("Bad rank : " + rank);
    }
    if (!suitOk) {
      throw new IllegalArgumentException("Bad suit : " + suit);
    }
    this.rank = rank;
    this.suit = suit;
  }

  /**
   * Get a playing card with a given rank and suit. Only one
   * <code>PlayingCardM</code> object will ever be created for each unique
   * combination of rank and state.
   * 
   * <p>
   * Valid ranks are "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q",
   * "K", "A" (the number cards 2 through 10, and the face cards jack, queen,
   * king, and ace).
   * 
   * <p>
   * Valid suits are "H", "D", "C", "S" (the suits hearts, diamonds, clubs, and
   * spades).
   * 
   * @param rank
   *          The rank of the card.
   * @param suit
   *          The suit of the card.
   * @return A card with the given rank and suit.
   * @throws <code>IllegalArgumentException</code> if the rank or the suit is
   *         not valid.
   */
  public static PlayingCardM getCard(String rank, String suit) {
    String key = rank + suit;
    PlayingCardM card = PlayingCardM.instances.get(key);
    if (card == null) {
      card = new PlayingCardM(rank, suit);
      PlayingCardM.instances.put(key, card);
    }
    return card;
  }

  /**
   * Gets the rank of the card. The rank is guaranteed to be one of the rank
   * strings described in {@link PlayingCardM#getCard(String, String)}.
   * 
   * @return the rank
   * @see PlayingCardM#getCard(String, String)
   */
  public String getRank() {
    return rank;
  }

  /**
   * Gets the suit of the card. The suit is guaranteed to be one of the suit
   * strings described in {@link PlayingCardM#getCard(String, String)}.
   * 
   * @return the suit
   * @see PlayingCardM#getCard(String, String)
   */
  public String getSuit() {
    return suit;
  }

  /**
   * Return a hash code for this playing card.
   * 
   * @return A hash code value for this playing card.
   */
  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((suit == null) ? 0 : suit.hashCode());
    result = prime * result + ((rank == null) ? 0 : rank.hashCode());
    return result;
  }

  /**
   * Compares this playing card to the specified object. The result is
   * <code>true</code> if and only if the argument is a
   * <code>PlayingCardM</code> with the same rank and suit as this card.
   * 
   * @param obj
   *          The object to compare this <code>PlayingCardM</code> against.
   * @return <code>true</code> if the given object is a
   *         <code>PlayingCardM</code> equal to this playing card,
   *         <code>false</code> otherwise.
   */
  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (obj == null) {
      return false;
    }
    if (getClass() != obj.getClass()) {
      return false;
    }
    PlayingCardM other = (PlayingCardM) obj;
    if (suit == null) {
      if (other.suit != null) {
        return false;
      }
    } else if (!suit.equals(other.suit)) {
      return false;
    }
    if (rank == null) {
      if (other.rank != null) {
        return false;
      }
    } else if (!rank.equals(other.rank)) {
      return false;
    }
    return true;
  }

  /**
   * Compares two playing cards by their ranks and suits. The result is
   * guaranteed to be consistent with equals.
   * 
   * <p>
   * The cards are first compared by their ranks. If their ranks are equal then
   * the cards are compared by their suits.
   * 
   * <p>
   * The ordering of ranks from smallest to largest is 2, 3, 4, 5, 6, 7, 8, 9,
   * 10, J, Q, K, A (i.e., 2 is considered the smallest rank and A (ace) is
   * considered the largest rank).
   * 
   * <p>
   * The order of suits from smallest to largest is S, C, D, H (i.e., spades <
   * clubs < diamonds < hearts).
   * 
   * @param other
   *          The <code>PlayingCardM/code> to be compared against.
   * @return the value <code>0</code> if argument card is equal to this card; a
   *         value less than zero if this card is smaller than the other card; a
   *         value greater than zero if this card is greater than the other
   *         card.
   */
  @Override
  public int compareTo(PlayingCardM other) {
    // compare ranks
    int thisRank = Arrays.asList(RANKS).indexOf(this.getRank());
    int otherRank = Arrays.asList(RANKS).indexOf(other.getRank());
    int result = thisRank - otherRank;
    if (result == 0) {
      // compare suits
      int thisSuit = Arrays.asList(SUITS).indexOf(this.getSuit());
      int otherSuit = Arrays.asList(SUITS).indexOf(other.getSuit());
      result = thisSuit - otherSuit;
    }
    return result;
  }

  /**
   * Returns a string representation of this playing card.
   * 
   * <p>
   * The returned string is the rank of the card followed by <code>" of "</code>
   * followed by the suit of the card. For example, the card representing the
   * ace of hearts has the string representation <code>"A of H"</code>.
   * 
   * @return A string representation of this playing card.
   */
  @Override
  public String toString() {
    return this.getRank() + " of " + this.getSuit();
  }

  /**
   * Returns a set of the strings that can be used to specify the rank of a
   * playing card. The set is guaranteed to contain all of the rank strings
   * described in {@link PlayingCardM#getCard(String, String)}.
   * 
   * @return a set of the valid ranks as strings
   */
  public static Set<String> getAllRanks() {
    return new LinkedHashSet<String>(Arrays.asList(RANKS));
  }

  /**
   * Returns a set of the strings that can be used to specify the suit of a
   * playing card. The set is guaranteed to contain all of the suit strings
   * described in {@link PlayingCardM#getCard(String, String)}.
   * 
   * @return a set of the valid suits as strings
   */
  public static Set<String> getAllSuits() {
    return new LinkedHashSet<String>(Arrays.asList(SUITS));
  }

  public static void main(String[] args) {
    // generate a standard 52 card deck
    Set<String> ranks = PlayingCardM.getAllRanks();
    Set<String> suits = PlayingCardM.getAllSuits();
    for (String r : ranks) {
      for (String s : suits) {
        PlayingCardM card = PlayingCardM.getCard(r, s);
        System.out.println(card);
      }
    }
  }
}
