/**
 * The Blackjack class allows a single player to play a game of blackjack.
 * The class tracks the player's bankroll but makes no attempt to prevent
 * a negative bankroll.
 */
public class Blackjack
{
    public static final int LOSS_RESULT = -1,
            PUSH_RESULT = 0,
            WIN_RESULT = 1,
            WIN_BJ_RESULT = 2;
    
    private static final int DECKS = 6, CARDS_PER_DECK = 52;
    private static final double SHOE_PENETRATION = 0.75;
    private static final int DEALER_STAND_VALUE = 17;
    private static final double WIN_BJ_PAYOUT = 2.5, WIN_PAYOUT = 2.0;
    
    private Shoe shoe;

    private double playersMoney;

    private BlackjackHand playersHand;
    private double playersBet;

    private BlackjackHand dealersHand;

    /**
     * Constructs a blackjack object that is ready to play.
     * @param playersMoney the player's starting bankroll (all values, including 0 and negative values, are permitted)
     */
    public Blackjack(double playersMoney)
    {
        this.shoe = new Shoe(DECKS);
        this.playersMoney = playersMoney;
        reset();
    }

    /**
     * Resets for another round, including resetting shoe if necessary
     */
    private void reset()
    {
        playersHand = null;
        playersBet = 0;
        dealersHand = null;

        if(shoe.cardsLeft() < DECKS * CARDS_PER_DECK * (1 - SHOE_PENETRATION))
            shoe.reset();
    }

    /**
     * Returns the player's money (can be negative)
     * @return the player's money
     */
    public double getPlayersMoney()
    {
        return playersMoney;
    }

    /**
     * Returns the player's bet
     * @return the player's bet for the hand
     * @throws IllegalStateException if player has not yet bet
     */
    public double getPlayersBet()
    {
        if(playersHand == null)
            throw new IllegalStateException("Player has not yet bet.");
        
        return playersBet;
    }

    /**
     * Places a bet at the start of a round. Deals cards to the player and dealer.
     * @param amount the amount to bet
     */
    public void placeBetAndDealCards(double amount)
    {
        playersMoney -= amount;
        playersBet = amount;

        BlackjackCard first = shoe.dealCard();
        BlackjackCard second = shoe.dealCard();

        playersHand = new BlackjackHand(first, shoe.dealCard());
        dealersHand = new BlackjackHand(second, shoe.dealCard());
      
        // alternate
//        BlackjackCard[] cards = new BlackjackCard[4];
//        for(int i = 0; i < cards.length; i++)
//            cards[i] = shoe.dealCard();
//        
//        playersHand = new BlackjackHand(cards[0], cards[2]);
//        dealersHand = new BlackjackHand(cards[1], cards[3]);
    }

    /**
     * Returns the player's hand
     * @return the player's hand
     * @throws IllegalStateException if player has not yet bet
     */
    public BlackjackHand getPlayersHand()
    {
        if(playersHand == null)
            throw new IllegalStateException("Player has not yet bet.");
        
        return playersHand;
    }

    /**
     * Returns the dealer's hand
     * @return the dealer's hand
     * @throws IllegalStateException if player has not yet bet
     */
    public BlackjackHand getDealersHand()
    {
        if(playersHand == null)
            throw new IllegalStateException("Player has not yet bet.");
        
        return dealersHand;
    }

    /**
     * Returns true if the player can hit, false otherwise
     * @return true if the player can hit, false otherwise
     * @throws IllegalStateException if player has not yet bet
     */
    public boolean canHit()
    {
        if(playersHand == null)
            throw new IllegalStateException("Player has not yet bet.");
        
        return ! dealersHand.isBlackjack() && playersHand.canAddCard();
    }

    /**
     * Deals another card to the player's hand.
     * @throws IllegalStateException if ! canHit()
     */
    public void hit()
    {
        if( ! canHit() )
            throw new IllegalStateException("canHit() must be true to hit");
        
        playersHand.addCard(shoe.dealCard());
    }

    /**
     * Plays the dealer's hand.
     * @throws IllegalStateException if player has not yet bet
     */
    public void playDealersHand()
    {
        if(playersHand == null)
            throw new IllegalStateException("Player has not yet bet.");
        
        if(playersHand.getNumericalValue() <= BlackjackHand.MAX_HAND_VALUE &&
                ! playersHand.isBlackjack())
        {
            while(dealersHand.getNumericalValue() < DEALER_STAND_VALUE)
                dealersHand.addCard(shoe.dealCard());
        }
    }
    
    /**
     * Returns true if the round is over, false otherwise
     * @return true if the round is over, false otherwise
     * @throws IllegalStateException if player has not yet bet
     */
    private boolean roundIsOver()
    {
        if(playersHand == null)
            throw new IllegalStateException("Player has not yet bet.");
        
        return playersHand.getNumericalValue() > BlackjackHand.MAX_HAND_VALUE ||
                playersHand.isBlackjack() || 
                dealersHand.getNumericalValue() >= DEALER_STAND_VALUE;
    }

    /**
     * Returns the result of the hand, as described below.
     *  LOSS_RESULT: player loss/dealer win
     *  PUSH_RESULT: push (tie)
     *  WIN_RESULT: player wins without blackjack
     *  WIN_BJ_RESULT: player wins with blackjack 
     * @return the result of the hand, as described
     * @throws IllegalStateException if round is not over
     */
    public int getResult()
    {
        if ( ! roundIsOver() )
            throw new IllegalStateException("Round is not yet over.");
        
        if(playersHand.isBlackjack() && dealersHand.isBlackjack())
            return PUSH_RESULT;

        if(playersHand.isBlackjack())
            return WIN_BJ_RESULT;

        if(dealersHand.isBlackjack())
            return LOSS_RESULT;
        
        if(playersHand.isBust())
            return LOSS_RESULT;

        if(dealersHand.isBust())
            return WIN_RESULT;

        int playersHandValue = playersHand.getNumericalValue();
        int dealersHandValue = dealersHand.getNumericalValue();
        
        if(playersHandValue > dealersHandValue)
            return WIN_RESULT;

        if(playersHandValue == dealersHandValue)
            return PUSH_RESULT;

        return LOSS_RESULT;
    }

    /**
     * Resolves the player's bets (updates player's money based on the
     * results of the round) and resets for another round
     * @throws IllegalStateException if round is not over
     */
    public void resolveBetsAndReset()
    {
        if ( ! roundIsOver() )
            throw new IllegalStateException("Round is not yet over.");
        
        int result = getResult();
        
        if(result == PUSH_RESULT)
            playersMoney += playersBet;
        else if(result == WIN_RESULT)
            playersMoney += playersBet * WIN_PAYOUT;
        else if(result == WIN_BJ_RESULT)
            playersMoney += playersBet * WIN_BJ_PAYOUT;

        reset();
    }
}
