import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

/**
 * Implements a simple program for playing the log files generated by an
 * Othello tournament.  The log file player works by generating a set of
 * "frames" corresponding to the initial board position, each intermediate
 * board position, and the final state.
 *
 * <p>The constants {@link #LOG_DIR} is used the default directory to look for
 * log files in.  The files must be named "blackAI vs. whiteAI.log".
 *
 * <p>Log files are assumed to have three kinds of lines.</p>
 *
 * <ul>
 *
 * <li>null lines:  These lines consist only of the text "null", which is
 * taken to mean that a player passed.
 *
 * <li>move lines:  These lines are of the form "(X,Y)" where X and Y are
 * digits in the range 0, 1, ..., 9.  This is taken to mean that a player made
 * a move at that specified location.
 *
 * <li>anything else:  These lines are any lines that do not fall in one of
 * the above two categories.  These lines will be displayed with the final
 * frame of the log file player.
 *
 * </ul>
 *
 * <p>Implementation notes: everything must synchronize in {@link #lock}
 * whenever it queries or modified {@link #currentPos}, as this latter
 * variable is accessed from multiple threads.
 *
 * <p>$Id: LogFilePlayer.java,v 1.8 2005/02/17 06:30:54 plattner Exp $
 *
 * @author Brian Emre Aydemir (emre@cs.caltech.edu)
 **/

public class LogFilePlayer extends JPanel
{
   /** The default directory for log files. **/
   public final static String LOG_DIR = "www.ugcs.caltech.edu/~wagner/log";

   /** The delay between moves when auto playing a game (in millis). **/
   public final static int DELAY = 750;

   /** The status line(s) at the bottom of the log file display. **/
   protected JTextArea statusLine;

   /**
    * Displays the count of black pieces on the currently displayed board.
    **/
   protected JLabel blackPieces;

   /**
    * Displays the count of white pieces on the currently displayed board.
    **/
   protected JLabel whitePieces;

   /** Button used to move to the next board position. **/
   protected JButton forward;

   /** Button used to move to the previous board position. **/
   protected JButton previous;

   /** Button used to start playing the moves. **/
   protected JButton play;

   /** Button used to stop the playhing of moves. **/
   protected JButton stop;

   /** Indicates which board is currently being displayed. **/
   protected int currentPos;

   /** The list of boards to display. **/
   protected LinkedList boards;

   /** The list of status lines, one for each board. **/
   protected LinkedList lines;

   /** The object to sync on before using {@link #currentPos}. **/
   protected Object lock;

   /** True if and only if we should auto play the moves. **/
   protected boolean autoPlay;

   /** The last board added to the display. **/
   protected BoardComponent lastBoard;

   /////////////////////////////////////////////////////////////////////////
   // CONSTRUCTORS.

   /**
    * Constructs a new log file player display.
    *
    * @param logLines The lines (as Strings) from the log file.
    * @param blackAI The name of the black AI.
    * @param whiteAI The name of the white AI.
    **/

   public LogFilePlayer
      (LinkedList logLines, String blackAI, String whiteAI)
   {
      super(new BorderLayout());

      // Thread related setup.

      lock = new Object();
      autoPlay = false;

      // Parse the lines.

      LinkedList moves = new LinkedList();
      StringBuffer finalResult = new StringBuffer();
      parseLines(logLines, moves, finalResult);

      // Set up the information / button display.

      JPanel infoDisplay = new JPanel(new GridLayout(4,2));
      blackPieces = new JLabel("0");
      whitePieces = new JLabel("0");
      forward = new JButton("Next");
      previous = new JButton("Previous");

      infoDisplay.add(new JLabel("Black: "));
      infoDisplay.add(blackPieces);
      infoDisplay.add(new JLabel("White: "));
      infoDisplay.add(whitePieces);
      infoDisplay.add(previous);
      infoDisplay.add(forward);

      play = new JButton("Play moves");
      stop = new JButton("Stop");
      infoDisplay.add(play);
      infoDisplay.add(stop);

      JPanel infoWrapper = new JPanel(new BorderLayout());
      infoWrapper.add(infoDisplay, BorderLayout.PAGE_END);

      // Set up the main panel.

      statusLine = new JTextArea(7, 30);
      statusLine.setEditable(false);

      lastBoard = new BoardComponent(new OthelloBoard());
      this.add(lastBoard, BorderLayout.CENTER);
      this.add(infoWrapper, BorderLayout.LINE_END);
      this.add(new JScrollPane(statusLine), BorderLayout.PAGE_END);

      // Build the list of boards and status lines.

      boards = new LinkedList();
      lines = new LinkedList();

      OthelloBoard b = new OthelloBoard();
      OthelloSide side = OthelloSide.BLACK;

      boards.addLast(b);
      lines.addLast("Initial board position.");

      Iterator iter = moves.iterator();

      while (iter.hasNext())
      {
         Move m = (Move)iter.next();

         b = b.copy();
         b.move(m, side);

         boards.addLast(b);

         if (side == OthelloSide.BLACK)
         {
            lines.add(side + " (" + blackAI + ") moved at: " + m);
         }
         else
         {
            lines.add(side + " (" + whiteAI + ") moved at: " + m);
         }

         side = side.opposite();
      }

      boards.addLast(b.copy());
      lines.add("Final result: " + finalResult.toString());

      // Add the event handlers for the buttons.

      forward.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent evn)
         {
            synchronized (lock)
            {
               if (currentPos < boards.size() - 1)
               {
                  currentPos++;
               }
            }
            updateDisplay();
         }
      } );

      previous.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent evn)
         {
            synchronized (lock)
            {
               if (currentPos > 0)
               {
                  currentPos--;
               }
            }
            updateDisplay();
         }
      } );

      play.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent evn)
         {
            synchronized (lock)
            {
               if (currentPos == boards.size() - 1)
               {
                  currentPos = 0;
               }

               autoPlay = true;
               delayedUpdateDisplay();
            }
         }
      } );

      stop.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent evn)
         {
            autoPlay = false;
            delayedUpdateDisplay();
         }
      } );

      // Set the initial state of the log file player.

      currentPos = 0;
      updateDisplay();

      // Set up the thread to auto play the game.

      Thread t = new Thread(new Runnable() {
         public void run()
         {
            while (true)
            {
               try
               {
                  Thread.sleep(DELAY);
               }
               catch (InterruptedException exn)
               {
                  // Ignored.
               }

               synchronized (lock)
               {
                  if (autoPlay && currentPos < boards.size() - 1)
                  {
                     currentPos++;
                  }
                  else
                  {
                     autoPlay = false;
                  }

                  delayedUpdateDisplay();
               }
            }
         }
      } );
      t.start();
   }

   /////////////////////////////////////////////////////////////////////////
   // INSTANCE METHODS.

   /**
    * Parses a set of lines, adding the moves and final result comments
    * to the provided arguments.
    **/
   public void parseLines
      (LinkedList lines, LinkedList moves, StringBuffer finalResult)
   {
      Iterator iter = lines.iterator();

      while (iter.hasNext())
      {
         String line = (String)iter.next();

         if (line.equals("null"))
         {
            // Must have a null move (a pass).

            moves.addLast(null);
         }
         else if (line.charAt(0) == '(' && line.length() == 5)
         {
            // Parse the move.

            int x = line.charAt(1) - '0';
            int y = line.charAt(3) - '0';
            moves.addLast(new Move(x, y));
         }
         else
         {
            // Assume this is a line dealing with the final result.

            finalResult.append("\n" + line);
         }
      }
   }

   /**
    * Causes {@link #updateDisplay()} to be called from within the
    * event handling thread.
    **/
   protected void delayedUpdateDisplay()
   {
      EventQueue.invokeLater(new Runnable() {
         public void run()
         {
            updateDisplay();
         }
      } );
   }

   /**
    * Updates the display based on the value of {@link #currentPos}.
    * Must be called from the event handling thread.
    **/
   protected void updateDisplay()
   {
      OthelloBoard b = null;
      String line = null;

      synchronized (lock)
      {
         b = (OthelloBoard)boards.get(currentPos);
         line = (String)lines.get(currentPos);
         forward.setEnabled(currentPos != boards.size() - 1);
         previous.setEnabled(currentPos != 0);
      }

      // Update the board, status line.

      statusLine.setText(line);
      this.remove(lastBoard);
      lastBoard = new BoardComponent(b);
      this.add(lastBoard, BorderLayout.CENTER);

      // Count the number of pieces on the board and update that.

      int black = 0;
      int white = 0;

      for (int i = 0; i < 8; i++)
      {
         for (int j = 0; j < 8; j++)
         {
            if (b.get(OthelloSide.WHITE, i, j))
            {
               white++;
            }
            else if (b.get(OthelloSide.BLACK, i, j))
            {
               black++;
            }
         }
      }

      blackPieces.setText(new Integer(black).toString());
      whitePieces.setText(new Integer(white).toString());

      // Update the auto play buttons.

      if (autoPlay)
      {
         stop.setEnabled(true);
         play.setEnabled(false);
      }
      else
      {
         stop.setEnabled(false);
         play.setEnabled(true);
      }
   }

   /////////////////////////////////////////////////////////////////////////
   // MAIN METHOD.

   /**
    * Runs the log file player program.  The first two arguments should be the
    * classname of the black and white AIs, in that order.  The optional third
    * argument should be the directory containing the log file.  If not
    * specified, {@link #LOG_DIR} is used.
    **/
   public static void main(String[] args)
   {
      // Make sure we got enough arguments.

      if (args.length < 2 || args.length > 3)
      {
         System.out.println("Arguments are: <blackAI> <whiteAI> [log dir]");
         System.exit(1);
      }

      // Parse out the arguments.

      String blackAI = args[0];
      String whiteAI = args[1];
      String logDir = (args.length == 3) ? args[2] : LOG_DIR;

      // Prepare to parse the log file.

      File logFile = new File(logDir, blackAI + " vs. " + whiteAI + ".log");
      LinkedList lines = new LinkedList();

      // Get all the lines in the log file.

      try
      {
         FileReader stream = new FileReader(logFile);
         BufferedReader reader = new BufferedReader(stream);
         String line = reader.readLine();
         while (line != null)
         {
            lines.addLast(line);
            line = reader.readLine();
         }
         reader.close();
      }
      catch (IOException exn)
      {
         System.out.println("*** Error while reading the log file. ***");
         exn.printStackTrace();
         System.exit(1);
      }

      // Create the display.

      JFrame frame = new JFrame(blackAI + " vs. " + whiteAI);
      LogFilePlayer display = new LogFilePlayer(lines, blackAI, whiteAI);
      frame.setContentPane(display);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.pack();
      frame.show();
   }
}

