/** Spider - Solitaire game */

import java.applet.Applet;
import java.awt.*;
import java.awt.image.*;
import java.util.Random;
import java.util.Vector;

public class Spider extends Applet
{
   private Deck deck;
   private Tableau table;

   private Color deckColor;
   private Image deckImage;
   private Image[] cards;

   private int cardsInSuit;
   private int suitCount;
   private int deckCount;

   private int finalCount;
   private int pileCount;
   private int dealCount;

   private int cardImgX, cardImgY;
   private int cardX, cardY;

   private Rectangle pack;
   private Rectangle score;
   private Rectangle history;
   private Rectangle[] finals;
   private Rectangle[] piles;
   private int[] distances;

   private Image offscreenImg;
   private Graphics offscreenG;
   private boolean changed;

   public String getAppletInfo ()
   {
      return "Spider Solitaire 1.0\nTom, May 1998";
   }

   public void init ()
   {
      String at;

      at = getParameter("backgroundHexRGB");
      setBackground((at != null) ? new Color(Integer.parseInt(at, 16)) :
                                   Color.green.darker());
      at = getParameter("deckHexRGB");
      deckColor = (at != null) ? new Color(Integer.parseInt(at, 16)) :
                                 Color.gray;

      String deckFile = getParameter("deckImage");
      String cardsFile = getParameter("cardsImage");
      if (cardsFile == null)
      {
         System.out.println("No card image file specified, aborting");
         System.exit(1);
      }

      at = getParameter("cardsInSuit");
      cardsInSuit = (at != null) ? Integer.parseInt(at) : 13;
      at = getParameter("suitCount");
      suitCount = (at != null) ? Integer.parseInt(at) : 4;
      at = getParameter("deckCount");
      deckCount = (at != null) ? Integer.parseInt(at) : 2;

      finalCount = suitCount * deckCount;
      at = getParameter("pileCount");
      pileCount = (at != null) ? Integer.parseInt(at) : finalCount + 2;
      if (pileCount > cardsInSuit * suitCount * deckCount)
         pileCount = cardsInSuit * suitCount * deckCount;
      at = getParameter("dealCount");
      dealCount = (at != null) ? Integer.parseInt(at) : 5;

      MediaTracker tracker = new MediaTracker(this);
      showStatus("Getting the card images: " + cardsFile);
      Image img = getImage(getDocumentBase(), cardsFile);
      tracker.addImage(img, 0);
      if (deckFile != null)
      {
         deckImage = getImage(getDocumentBase(), deckFile);
         tracker.addImage(deckImage, 1);
      }
      try
      {
         tracker.waitForAll();
      }
      catch (InterruptedException ex) { }
      if (tracker.isErrorAny())
      {
         if (tracker.isErrorID(1))
         {
            System.out.println("Error getting deck " + deckFile);
            deckImage = null;
         }
         if (tracker.isErrorID(0))
         {
            System.out.println("Error getting cards " + cardsFile);
            System.exit(1);
         }
      }
      showStatus("Got the images, processing...");

      cardImgX = img.getWidth(this) / cardsInSuit;
      cardImgY = img.getHeight(this) / suitCount;

      cards = new Image[cardsInSuit * suitCount];
      for (int i = 0; i < cards.length; i++)
      {
         int x = cardImgX * (i % cardsInSuit);
         int y = cardImgY * (i / cardsInSuit);
         CropImageFilter f = new CropImageFilter(x, y, cardImgX, cardImgY);
         cards[i] = createImage(new FilteredImageSource(img.getSource(), f));
      }
      showStatus("Finished processing");

      finals = new Rectangle[finalCount];
      piles = new Rectangle[pileCount];
      distances = new int[pileCount];
      calculateNewTable();

      deck = new Deck(cardsInSuit, suitCount, deckCount);
      table = new Tableau(deck, pileCount, finalCount, dealCount);
      changed = true;
   }

   public void destroy ()
   {
      deck = null;
      table = null;
      deckColor = null;
      cards = null;
      pack = null;
      score = null;
      finals = null;
      piles = null;
      distances = null;
      offscreenImg = null;
      offscreenG = null;
   }

   private void calculateNewTable ()
   {
      int max = Math.max(pileCount, finalCount + 2);
      int ofs = max - finalCount;
      cardX = (size().width * 20) / (21 * max);
      cardY = (cardImgY * cardX) / cardImgX;
      int padd = (size().width - cardX * max) / (max + 1);
      int paddX = (size().width - cardX * max - padd * (max - 1)) / 2;

      pack = new Rectangle(paddX, padd, cardX, cardY);
      score = new Rectangle(paddX + cardX + padd, padd,
                            (ofs - 2) * (cardX + padd) + cardX,
                            (2 * cardY) / 3 );
      history = new Rectangle(score.x, score.y + score.height,
                              score.width, cardY - score.height);
      for (int f = 0; f < finals.length; f++)
         finals[f] = new Rectangle(paddX + (f + ofs) * (cardX + padd), padd,
                                   cardX, cardY);
      for (int p = 0; p < piles.length; p++)
         piles[p] = new Rectangle(paddX + p * (cardX + padd), 2 * padd + cardY,
                                  cardX, size().height - (3 * padd + cardY));
      for (int p = 0; p < piles.length; p++)
         distances[p] = cardY / 4;

      offscreenImg = createImage(size().width, size().height);
      offscreenG = offscreenImg.getGraphics();
      offscreenG.setColor(getBackground());
      offscreenG.fillRect(0, 0, size().width, size().height);
   }

   public Dimension preferredSize ()
   {
      return new Dimension((cardImgX * piles.length * 20) / 19, 6 * cardImgY);
   }

   public void update (Graphics g)
   {
      paint(g);
   }

   public void paint (Graphics g)
   {
      if (size().width != offscreenImg.getWidth(this) ||
          size().height != offscreenImg.getHeight(this))
         calculateNewTable();

//  for (int i = 0; i < 10; ++i)
//  {
//      if (!isValid())
         changed = true; /***** ??????????????????????????????????? ****/
      paintPack(offscreenG);
      for (int f = 0; f < finals.length; f++)
         paintFinal(offscreenG, f);
      for (int p = 0; p < piles.length; p++)
         paintPile(offscreenG, p);
      paintScore(offscreenG);
      paintHistory(offscreenG);
//  }
      g.drawImage(offscreenImg, 0, 0, this);
      changed = false;
   }

   private void paintPack (Graphics g)
   {
      if (changed || table.pack.changed())
      {
         if (table.pack.empty())
            paintEmpty(g, pack.x, pack.y);
         else
            paintDeck(g, pack.x, pack.y);

         table.pack.resetChanged();
      }
   }

   private void paintFinal (Graphics g, int f)
   {
      if (changed || table.finals[f].changed())
      {
         if (table.finals[f].empty())
            paintEmpty(g, finals[f].x, finals[f].y);
         else
            paintCard(g, finals[f].x, finals[f].y,
                      table.finals[f].top(), false);

         table.finals[f].resetChanged();
      }
   }

   private void paintPile (Graphics g, int p)
   {
      Pile pile = table.piles[p];
      if (changed || pile.changed())
      {
         int distance = cardY / 4;
         if (cardY + distance * (pile.count() - 1) > piles[p].height)
            distance = (piles[p].height - cardY) / (pile.count() - 1);
         int firstChanged = 0;
         if (!changed && distance == distances[p])
            firstChanged = pile.count() - pile.cardsChanged();
         distances[p] = distance;

         for (int i = firstChanged; i < pile.count(); ++i)
         {
            if (i < pile.count() - pile.open())
               paintDeck(g, piles[p].x, piles[p].y + distance * i);
            else
               paintCard(g, piles[p].x, piles[p].y + distance * i,
                         pile.card(i), (i >= pile.count() - pile.selected()));
         }
         g.setColor(getBackground());
         g.fillRect(piles[p].x,
                    piles[p].y + cardY + distance * (pile.count() - 1),
                    cardX + 1, size().height);
         if (pile.empty())
            paintEmpty(g, piles[p].x, piles[p].y);

         pile.resetChanged();
      }
   }

   private void paintCard (Graphics g, int x, int y, Card c, boolean selected)
   {
      Color color = Color.white;
      if (selected)
         color = Color.lightGray;
      g.drawImage(cards[c.suit * cardsInSuit + c.rank],
                  x, y, cardX, cardY, color, this);
      g.setColor(Color.black);
      g.drawRoundRect(x, y, cardX - 1, cardY - 1, cardX / 10, cardY / 10);
   }

   private void paintDeck (Graphics g, int x, int y)
   {
      g.setColor(deckColor);
      g.fillRoundRect(x, y, cardX - 1, cardY - 1, cardX / 10, cardY / 10);
      if (deckImage != null)
         g.drawImage(deckImage, x, y, cardX, cardY, this);
      g.setColor(Color.black);
      g.drawRoundRect(x, y, cardX - 1, cardY - 1, cardX / 10, cardY / 10);
   }

   private void paintEmpty (Graphics g, int x, int y)
   {
      g.setColor(getBackground());
      g.fillRect(x, y, cardX + 1, cardY + 1);
      g.setColor(Color.black);
      g.drawRect(x, y, cardX - 1, cardY - 1);
   }

   private void paintScore (Graphics g)
   {
      String s;
      int x, y = score.height / 5;

      g.setColor(getBackground());
      g.fillRect(score.x, score.y, score.width, score.height);
      g.setColor(Color.black);

      g.setFont(new Font("TimesRoman", Font.BOLD, y));
      FontMetrics fm = g.getFontMetrics();

      s = table.points() + "";
      x = (score.width - fm.stringWidth(s)) / 2;
      g.drawString(s, score.x + x, score.y + y);

      s = "points";
      x = (score.width - fm.stringWidth(s)) / 2;
      g.drawString(s, score.x + x, score.y + 2 * y);

      s = table.deals() + " of " + dealCount;
      x = (score.width - fm.stringWidth(s)) / 2;
      g.drawString(s, score.x + x, score.y + 7 * y / 2);

      s = "deals";
      x = (score.width - fm.stringWidth(s)) / 2;
      g.drawString(s, score.x + x, score.y + 9 * y / 2);
   }

   private void paintHistory (Graphics g)
   {
      String s;
      int x = history.width / 2;
      int y = history.height / 2;
      int rx = x / 5;
      int ry = y / 5;

      g.setColor(getBackground());
      g.fillRect(history.x, history.y, history.width, history.height);
      g.setFont(new Font("TimasRoman", Font.BOLD, y - 1));
      FontMetrics fm = g.getFontMetrics();
      int baseline = (y * fm.getAscent()) / (fm.getAscent() + fm.getDescent());

      s = "replay";
      g.setColor(Color.gray);
      g.fillRoundRect(history.x, history.y, history.width - 1, y - 1, rx, ry);
      g.setColor(Color.black);
      g.drawRoundRect(history.x, history.y, history.width - 1, y - 1, rx, ry);
      if (table.history.movesExecuted())
         g.setColor(Color.black);
      else
         g.setColor(Color.lightGray);
      g.drawString(s, history.x + (history.width - fm.stringWidth(s)) / 2,
                      history.y + baseline);

      s = "<<";
      g.setColor(Color.gray);
      g.fillRoundRect(history.x, history.y + y, x - 1, y - 1, rx, ry);
      g.setColor(Color.black);
      g.drawRoundRect(history.x, history.y + y, x - 1, y - 1, rx, ry);
      if (table.history.movesExecuted())
         g.setColor(Color.black);
      else
         g.setColor(Color.lightGray);
      g.drawString(s, history.x + (x - fm.stringWidth(s)) / 2,
                      history.y + y + baseline);

      s = ">>";
      g.setColor(Color.gray);
      g.fillRoundRect(history.x + x, history.y + y, x - 1, y - 1, rx, ry);
      g.setColor(Color.black);
      g.drawRoundRect(history.x + x, history.y + y, x - 1, y - 1, rx, ry);
      if (table.history.movesToExecute())
         g.setColor(Color.black);
      else
         g.setColor(Color.lightGray);
      g.drawString(s, history.x + x + (x - fm.stringWidth(s)) / 2,
                      history.y + y + baseline);
   }

   private int cardInPos (int p, int y)
   {
      int max = table.piles[p].count();
      int pos = (y - piles[p].y) / distances[p];
      if (pos >= max && y < piles[p].y + cardY + (max -1) * distances[p])
         pos = max - 1;
      return pos;
   }

   public boolean mouseDown (Event evt, int x, int y)
   {
      int p = piles.length;
      while (--p >= 0)
         if (piles[p].inside(x, y))
            break;
      if (p >= 0)
      {
         if (table.selected != null)
            table.move(p);
         else if (evt.controlDown())
            table.select(p, 0);
         else if (evt.shiftDown())
            table.select(p, table.piles[p].count() - cardInPos(p, y));
         else
            table.autoMove(p);
      }
      else if (finals[0].union(finals[finalCount - 1]).inside(x, y))
      {
         if (table.selected != null)
            table.finalMove();
      }
      else if (pack.inside(x, y))
      {
         if (table.noPileEmpty())
            table.deal();
         else
            showStatus("Can't deal while any row is empty");
      }
      else if (history.inside(x, y))
      {
         if (y < history.y + history.height / 2)
            while (table.history.movesExecuted())
               table.backward();
         else if (x < history.x + history.width / 2)
            table.backward();
         else
            table.forward();
      }
      if (changed || table.changed)
      {
         repaint();
         table.changed = false;
      }
      return true;
   }

   public boolean keyDown (Event evt, int key)
   {
      int keys[] = { Event.F1, Event.F2, Event.F3, Event.F4, Event.F5,
                     Event.F6, Event.F7, Event.F8, Event.F9, Event.F10 };
      int p = keys.length;
      while (--p >= 0)
         if (key == keys[p])
            break;
      if (p >= 0 && p < piles.length)
         table.autoMove(p);
      else if (key == Event.HOME)
         while (table.history.movesExecuted())
            table.backward();
      else if (key == Event.PGUP)
         table.backward();
      else if (key == Event.PGDN)
         table.forward();
      if (changed || table.changed)
      {
         repaint();
         table.changed = false;
      }
      return true;
   }
}

class Deck
{
   protected Card[] allCards;
   public int cardsInSuit;
   public int suitCount;
   public int deckCount;

   /** Constructor */
   public Deck (int cards, int suits, int decks)
   {
      cardsInSuit = cards;
      suitCount = suits;
      deckCount = decks;

      /* Create new deck of cards and shuffle the deck */
      allCards = new Card[cards * suits * decks];
      Random rand = new Random();
      for (int i = 0; i < allCards.length; ++i)
      {
         int pos = -1;
         while (pos < 0)
            pos = (int)Math.floor(rand.nextLong() % allCards.length);
         while (allCards[pos] != null)
            pos = (pos + 1) % allCards.length;
         allCards[pos] = new Card((i / cardsInSuit) % suitCount,
                                  i % cardsInSuit);
      }
   }

   /** Current count of cards */
   public int count ()
   {
      return cardsInSuit * suitCount * deckCount;
   }

   /** Card in deck */
   public Card card (int position)
   {
      return allCards[position];
   }
}

class Card
{
   public int suit;
   public int rank;

   /** Contructor */
   public Card (int s, int r)
   {
      suit = s;
      rank = r;
   }

   /** Matches this card to an other card? */
   public boolean matchesTo (Card other)
   {
      return (suit == other.suit && rank + 1 == other.rank);
   }
}

class Heap
{
   protected Card[] allCards;
   protected int currentCount;
   protected boolean emptyChanged;

   /** Constructor */
   public Heap (int maxCards)
   {
      allCards = new Card[maxCards];
      currentCount = 0;
      emptyChanged = true;
   }

   /** Current count of cards */
   public int count ()
   {
      return currentCount;
   }

   /** Card in heap */
   public Card card (int position)
   {
      return allCards[position];
   }

   /** Top card of the heap */
   public Card top ()
   {
      return allCards[currentCount-1];
   }

   /** Add a new card */
   public void add (Card card)
   {
      if (currentCount < allCards.length)
      {
         allCards[currentCount] = card;
         if (currentCount == 0)
            emptyChanged = true;
         ++currentCount;
      }
   }

   /** Remove the top card */
   public void remove ()
   {
      if (currentCount > 0)
      {
         --currentCount;
         if (currentCount == 0)
            emptyChanged = true;
      }
   }

   /** Move some matching cards to an other heap */
   public void moveTo (Heap other, int countToMove)
   {
      for (int i = count() - countToMove; i < count(); ++i)
         other.add(card(i));
      for (int i = 0; i < countToMove; ++i)
         remove();
   }

   /** Is the heap empty? */
   public boolean empty ()
   {
      return (currentCount == 0);
   }

   /** Is the heap changed? */
   public boolean changed ()
   {
      return emptyChanged;
   }

   /** Reset changed property */
   public void resetChanged ()
   {
      emptyChanged = false;
   }
}

class Pack extends Heap
{
   /** Constructor */
   public Pack (Deck deck)
   {
      super(deck.count());
      for (int pos = 0; pos < deck.count(); ++pos)
         add(deck.card(pos));
   }

   /** First initial deal of a game */
   public void initialDeal (Pile piles[], int rows, Pile extra[])
   {
      for (int r = 0; r < rows; ++r)
         deal(piles);
      deal(extra);

      // turn all open cards
      for (int p = 0; p < piles.length; ++p)
         piles[p].turn();
   }

   /** Deal one row to the piles */
   public void deal (Pile piles[])
   {
      for (int p = 0; p < piles.length; ++p)
      {
         piles[p].add(top());
         remove();
      }
   }

   /** Cancel the deal */
   public void takeBackDeal (Pile piles[])
   {
      for (int p = piles.length; --p >= 0; )
      {
         add(piles[p].top());
         piles[p].remove();
      }
   }
}

class Pile extends Heap
{
   protected int currentOpen;
   protected int currentMatching;
   protected int currentSelected;
   protected int currentChanged;

   /** Constructor */
   public Pile (Deck deck)
   {
      super(deck.count());
      currentOpen = 0;
      currentMatching = 0;
      currentSelected = 0;
      currentChanged = 0;
   }

   /** Add a new card */
   public void add (Card card)
   {
      super.add(card);
      ++currentOpen;
      if (topCardsMatches())
         ++currentMatching;
      currentSelected = 0;
      ++currentChanged;
   }

   /** Remove top card from pile */
   public void remove ()
   {
      if (currentSelected > 0)
         --currentSelected;
      if (topCardsMatches())
         --currentMatching;
      if (currentOpen > 0)
         --currentOpen;
      super.remove();
      if (currentChanged > 0)
         --currentChanged;
      if (currentChanged < 1 && !empty())
         currentChanged = 1;
   }

   /** Turn all open top cards or rather open the top card */
   public void turn ()
   {
      if (currentOpen < 1 && !empty())
         currentOpen = 1;
      else
         currentOpen = 0;
      currentMatching = 0;
      if (currentChanged < 1 && !empty())
         currentChanged = 1;
   }

   /** Current count of open cards */
   public int open ()
   {
      return currentOpen;
   }

   /** Current count of matching cards */
   public int getMatching ()
   {
      return currentMatching;
   }

   /** The two open top cards are matching */
   public boolean topCardsMatches ()
   {
      return (open() >= 2 && card(count() - 1).matchesTo(card(count() - 2)));
   }

   /** Current count of selected cards */
   public int selected ()
   {
      return currentSelected;
   }

   /** Select up to max matching cards on the end of this pile */
   public void select (int max)
   {
      currentSelected = 0;
      if (open() > 0)
      {
         currentSelected = 1;
         for (int i = count(); --i > count() - open(); )
            if (card(i).matchesTo(card(i - 1)))
               currentSelected++;
            else
               break;
      }
      if (currentSelected > max && max > 0)
         currentSelected = max;
      if (currentChanged < currentSelected)
         currentChanged = currentSelected;
   }

   /** Unselect this pile */
   public void unselect ()
   {
      if (currentChanged < currentSelected)
         currentChanged = currentSelected;
      currentSelected = 0;
   }

   /** Adapt the selection to match an other pile */
   public void adaptSelectionTo (Pile other)
   {
      if (!other.empty())
      {
         if (currentChanged < currentSelected)
            currentChanged = currentSelected;
         int diff = other.top().rank - top().rank;
         if (diff > 0 && diff <= currentSelected)
            currentSelected = diff;
         else
            currentSelected = 0;
      }
   }

   /** Matches the selection to an other pile? */
   public boolean selectionMatchesTo (Pile other, boolean matchSuit)
   {
      return (!other.empty() &&
              (other.top().rank == top().rank + currentSelected) &&
              (other.top().suit == top().suit || !matchSuit));
   }

   /** Is the heap changed? */
   public boolean changed ()
   {
      return (super.changed() || currentChanged > 0);
   }

   /** Reset changed property */
   public void resetChanged ()
   {
      super.resetChanged();
      currentChanged = 0;
   }

   /** How many cards are changed? */
   public int cardsChanged ()
   {
      return currentChanged;
   }
}

class FinalHeap extends Heap
{
   private boolean bonus;

   /** Constructor */
   FinalHeap (Deck deck)
   {
      super(deck.cardsInSuit);
      bonus = false;
   }

   /** Set bonus of the final heap */
   public void setBonus (boolean newBonus)
   {
      bonus = newBonus;
   }

   /** Has this final heap bonus? */
   public boolean getBonus ()
   {
      return bonus;
   }
}

class Tableau
{
   private int dealCount;
   private int cardsToOpen;
   private Deck deck;
   public History history;
   public Pack pack;
   public Pile piles[];
   public FinalHeap finals[];
   public Pile selected;
   public boolean changed;

   /** Constructor */
   public Tableau (Deck deck, int pileCount, int finalCount, int deals)
   {
      this.deck = deck;
      dealCount = deals;
      cardsToOpen = deck.count() - (deals + 1) * pileCount;
      for ( ; cardsToOpen < 0; cardsToOpen += pileCount)
         --deals;

      pack = new Pack(deck);
      piles = new Pile[pileCount];
      for (int p = 0; p < piles.length; ++p)
         piles[p] = new Pile(deck);
      finals = new FinalHeap[finalCount];
      for (int f = 0; f < finals.length; ++f)
         finals[f] = new FinalHeap(deck);

      history = new History();

      // deal cards to open
      pack.initialDeal(piles, cardsToOpen / pileCount,
                       extraDealPiles(cardsToOpen % pileCount));
      // deal one open row
      pack.deal(piles);
   }

   /* Choice of piles for extra deal with remaining cards */
   private Pile[] extraDealPiles (int count)
   {
      Pile[] extra = new Pile[count];
      if (count > 0)
      {
         int extraMax = extra.length - 1;
         int pilesMax = piles.length - 1;
         for (int e = 0; e <= extraMax / 2; ++e)
         {
            int p = (e * pilesMax) / extraMax;
            extra[e] = piles[p];
            extra[extraMax - e] = piles[pilesMax - p];
         }
      }
      return extra;
   }

   /** Current count of deals */
   public int deals ()
   {
      return dealCount - pack.count() / piles.length;
   }

   /** Current count of points */
   public int points ()
   {
      int openCard        = 10;
      int openPile        = 15;
      int matchingCard    = 2;
      int readyFinal      = 50;
      int bonusFinal      = 2;
      int bonusfreeFinals = 3;

      int points = openCard * cardsToOpen;
      for (int p = 0; p < piles.length; ++p)
      {
         if (piles[p].count() > piles[p].open())
            points -= openCard * (piles[p].count() - piles[p].open());
         else
            points += openPile;
         points += matchingCard * piles[p].getMatching();
      }
      int emptyFinals = 0;
      int bonusFinals = 0;
      for (int f = 0; f < finals.length; ++f)
         if (finals[f].empty())
            ++emptyFinals;
         else if (finals[f].getBonus())
            ++bonusFinals;
      points += readyFinal * (finals.length - emptyFinals);
      if (emptyFinals == 0 && bonusFinals > bonusfreeFinals)
         points += bonusFinal * (bonusFinals - bonusfreeFinals);
      return points;
   }

   /** Is no pile empty? */
   public boolean noPileEmpty ()
   {
      for (int p = 0; p < piles.length; p++)
         if (piles[p].empty())
            return false;
      return true;
   }

   /** Matches all cards in all piles? */
   public boolean allCardsMatches ()
   {
      for (int p = 0; p < piles.length; ++p)
         if (piles[p].count() > piles[p].open() ||
             piles[p].count() > piles[p].getMatching() *
                                deck.cardsInSuit / (deck.cardsInSuit - 1))
         return false;
      return true;
   }

   /** Select p-th pile by selecting up to max matching cards on its end */
   public void select (int p, int max)
   {
      if (!piles[p].empty())
      {
         unselect();
         selected = piles[p];
         selected.select(max);
         changed = true;
      }
   }

   /** Unselect the selected pile */
   public void unselect ()
   {
      if (selected != null)
      {
         selected.unselect();
         selected = null;
      }
   }

   /** Move cards from selected pile to p-th pile */
   public void move (int p)
   {
      selected.adaptSelectionTo(piles[p]);
      int count = selected.selected();
      if (count > 0)
      {
         boolean turn = (count == selected.open() && count < selected.count());
         history.add(new NormalMove(selected, piles[p], count, turn));
         history.current().execute();
      }
      unselect();
      changed = true;
   }

   /** Search best move from p-th pile */
   public void autoMove (int p)
   {
      if (!piles[p].empty())
      {
         select(p, 0);

         if (allCardsMatches())
            finalMove();
         else
         {
            int i = p;
            while ((i = (i + 1) % piles.length) != p)
               if (selected.selectionMatchesTo (piles[i], true))
                  break;
            if (i == p)
               while ((i = (i + 1) % piles.length) != p)
                  if (selected.selectionMatchesTo (piles[i], false))
                     break;
            if (i == p)
               while ((i = (i + 1) % piles.length) != p)
                  if (piles[i].empty())
                     break;
            if (i != p)
               move(i);
         }
         changed = true;
      }
   }

   /** Deal one row */
   public void deal ()
   {
      if (!pack.empty() && noPileEmpty())
      {
         history.add(new DealMove(pack, piles));
         history.current().execute();

         unselect();
         changed = true;
      }
   }

   /** Move one suit of cards from selected pile to the first final heap */
   public void finalMove ()
   {
      int count = selected.selected();
      if (count == deck.cardsInSuit)
      {
         int f;
         for (f = 0; f < finals.length; ++f)
            if (finals[f].empty())
               break;
         if (f < finals.length)
         {
            boolean turn = (count == selected.open() &&
                            count < selected.count());
            boolean bonus = allCardsMatches();
            history.add(new FinalMove(selected, finals[f],
                                      count, turn, bonus));
            history.current().execute();
         }
         unselect();
         changed = true;
      }
   }

   /** Go one move backward in the history */
   public void backward ()
   {
      if (history.movesExecuted())
      {
         history.current().takeBack();
         history.backward();

         unselect();
         changed = true;
      }
   }

   /** Go one move forward in the history */
   public void forward ()
   {
      if (history.movesToExecute())
      {
         history.forward();
         history.current().execute();

         unselect();
         changed = true;
      }
   }
}

interface Move
{
   /** Do the move */
   public void execute ();

   /** Redo the move */
   public void takeBack ();
}

class DealMove implements Move
{
   private Pack source;
   private Pile[] destination;

   /** Constructor */
   public DealMove (Pack s, Pile[] d)
   {
      source = s;
      destination = d;
   }

   /** Do the move */
   public void execute ()
   {
      source.deal(destination);
   }

   /** Redo the move */
   public void takeBack ()
   {
      source.takeBackDeal(destination);
   }
}

class NormalMove implements Move
{
   private Pile source, destination;
   private int count;
   private boolean turn;

   /** Constructor */
   public NormalMove (Pile s, Pile d, int c, boolean t)
   {
      source = s;
      destination = d;
      count = c;
      turn = t;
   }

   /** Do the move */
   public void execute ()
   {
      source.moveTo(destination, count);
      if (turn)
         source.turn();
   }

   /** Redo the move */
   public void takeBack ()
   {
      if (turn)
         source.turn();
      destination.moveTo(source, count);
   }
}

class FinalMove implements Move
{
   private Pile source;
   private FinalHeap destination;
   private int count;
   private boolean turn;
   private boolean bonus;

   /** Constructor */
   public FinalMove (Pile s, FinalHeap d, int c, boolean t, boolean b)
   {
      source = s;
      destination = d;
      count = c;
      turn = t;
      bonus = b;
   }

   /** Do the move */
   public void execute ()
   {
      source.moveTo(destination, count);
      if (turn)
         source.turn();
      destination.setBonus(bonus);
   }

   /** Redo the move */
   public void takeBack ()
   {
      destination.setBonus(false);
      if (turn)
         source.turn();
      destination.moveTo(source, count);
   }
}

class History
{
   private Vector history;
   private int executed;

   /** Constructor */
   public History ()
   {
      history = new Vector();
      executed = 0;
   }

   /** Current move in the history */
   public Move current ()
   {
      return (Move)history.elementAt(executed - 1);
   }

   /** Add a new move */
   public void add (Move move)
   {
      if (executed != history.size())
         history.setSize(executed);
      history.addElement(move);
      ++executed;
   }

   /** Set previous move as current */
   public void backward ()
   {
      if (movesExecuted())
         --executed;
   }

   /** Set next move as current */
   public void forward ()
   {
      if (movesToExecute())
         ++executed;
   }

   /** Are there executed moves in the history */
   public boolean movesExecuted ()
   {
      return executed > 0;
   }

   /** Are there moves to execute in the history */
   public boolean movesToExecute ()
   {
      return executed < history.size();
   }
}
