package bingo.player;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.io.*;
import java.rmi.*;
import java.net.*;

import bingo.shared.*;

public class Player extends JPanel
		    implements ActionListener,
			       ItemListener,
			       BallListener {

    protected PlayerParameters params;

    protected static boolean DEBUG = false;

    protected ControlPane controlPane;
    protected GameStatusLabel gameStatusLabel;
    protected LightBoardPane lightBoardPane;
    protected JCheckBox beepButton;

    protected static int SMALLPAD = 5;
    protected static int BIGPAD = 20;

    static String register = "Join next game";
    static char registerKey = 'j';
    static String clear = "Reset";
    static char clearKey = 'r';
    static String beep = "Beep to announce new balls";
    static char beepKey = 'b';
    static String gameStatusTitle = "Game Status";
    static String windowTitle = "BINGO Player";

    private static Toolkit toolkit;

    Registrar registrar;
    int numCardWindows = 0;
    CardWindow[] cardWindows = new CardWindow[3];

    protected Ticket ticket;
    protected PlayerQueue playerQueue;

    public Player() {
	super(false);
	params = new PlayerParameters();

	playerQueue = new PlayerQueue(this);

	controlPane = new ControlPane(this);

        // status from the game
	JPanel statusPane = new JPanel(false);
	statusPane.setBorder(
		BorderFactory.createTitledBorder(
		    gameStatusTitle));
	statusPane.setLayout(new BoxLayout(statusPane, BoxLayout.Y_AXIS));

        gameStatusLabel = new GameStatusLabel();
	gameStatusLabel.setAlignmentX(0.0f);
	statusPane.add(gameStatusLabel);
        lightBoardPane = new LightBoardPane(0);
	lightBoardPane.setAlignmentX(0.0f);
	statusPane.add(lightBoardPane);

	//Choose where the app beeps whenever a ball arrives.
	beepButton = new JCheckBox(beep);
	beepButton.setSelected(params.getShouldBeep());
	beepButton.setMnemonic(beepKey);
	beepButton.addItemListener(this);
	beepButton.setAlignmentX(0.0f); 
	statusPane.add(beepButton);

	setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
	controlPane.setAlignmentX(0.0f); //left align
        add(controlPane);
	statusPane.setAlignmentX(0.0f); //left align
        add(statusPane);

	//XXX hack to turn off old-style event handling:
	//XXX add any kind of listener to this component
	//XXX 1.1 only, I think
	addContainerListener(new ContainerAdapter(){});

	// Get current status.
	playerQueue.postEvent(new StatusRequestEvent(this));

	// Initialize the toolkit.
	toolkit = Toolkit.getDefaultToolkit();
    }

    /* Called from AWT event dispatch thread. */
    public void itemStateChanged(ItemEvent e) {
	if (e.getStateChange() == ItemEvent.SELECTED) {
	    params.setShouldBeep(true);
	} else {
	    params.setShouldBeep(false);
	}
    }

    /* Called from AWT event dispatch thread. */
    public void actionPerformed(ActionEvent e) {
        String factoryName = null;

	String command = e.getActionCommand();
        if (command == register) {
	    params.setNames(controlPane.nameField.getText(),
	                    controlPane.hostField.getText());
	    //We didn't used to do the following, since seed isn't
	    //a saved property, but the alternative (doing
	    //invokeAndWait when you really need the value)
	    //might be prone to deadlock.
	    params.setSeed(Long.parseLong(controlPane.seedField.getText()));
	    playerQueue.postEvent(new RegisterEvent(this));
	} else if (command == clear) {
	    clearGame();
        } else {
	    int numCards;
	    try {
		numCards = Integer.parseInt(command);
	        params.setNumCards(numCards);
	    } catch (Throwable exc) {
		//Ignore the action since we don't understand it.
	    }
	}
    }

    /* Can be invoked from any thread. */
    public void showServerStatus(String message) {
	if (gameStatusLabel != null) {
	    final String msg = message; //so we can access it in inner class
	    SwingUtilities.invokeLater(new Runnable() {
		    public void run() {
	    		gameStatusLabel.setText(msg);
		    }
		});
	} else {
	    System.err.println("Player.gameStatusLabel is null, "
			       + "so couldn't say: "
			       + message);
	} 

	if (DEBUG) {
	    System.out.println("showServerStatus: " + message);
	}
    }

    /**
     * BallListener method. 
     */
    public void noMoreBalls() {
	controlPane.gameOver();  //safe from any thread
	showServerStatus("Game Over."); //safe from any thread
    }

    /**
     * BallListener method.
     */
    public void ballCalled(BingoBall b) {
	final BingoBall ball = b; //for use in inner class
	if (ball.getNumber() != BingoBall.GAME_OVER) {
	    SwingUtilities.invokeLater(new Runnable() {
	        public void run() {
                    lightBoardPane.displayNewBall(ball);
	            if (params.getShouldBeep()) {
			toolkit.beep();
		    }
	        }
	    });
	}
    }

    /* Called from action event handler (AWT event dispatch thread). */
    private void clearGame() {
	ticket = null;
	for (int i = 0; i < numCardWindows; i++) {
	    cardWindows[i].dispose();
	}
	controlPane.reset();
	lightBoardPane.clear();
    }

    /* Called from player queue thread. */
    void handleStatusRequestEvent(StatusRequestEvent event) {
	lookUpRegistrar(params.getHostname()); 
	if (registrar != null) {
	    String statusText = "";
	    try {
                statusText = registrar.whatsHappening();
	    } catch (java.rmi.ConnectException exc) {
		if (DEBUG) {
		    System.err.println("Not connected to Bingo server.");
		    System.err.println("registrar = " + registrar);
		}
		//XXX Update status?  We aren't connected to the Bingo
		//XXX server, but rmiregistry is running.
	    } catch (Exception exc) {
		System.err.println("Unexpected exception on status request.");
                exc.printStackTrace();
		return;
	    }
	    showServerStatus(statusText);  //safe from any thread
	}
    }

    /* Can be safely called from any thread. */
    private void lookUpRegistrar(String host) {
	if (registrar != null) 
	    return;

	try {
	    registrar = (Registrar)Naming.lookup("//"
	                + host + "/Registrar");
	} catch (java.rmi.NotBoundException exc) {
	    if (DEBUG) {
	        System.err.println("Couldn't find BINGO Server running on host "
			           + host + ".");
	        System.err.println("RMI seems to be running fine."); 
	    }
	    //XXX Advise them to start up BINGO server.
	} catch (java.rmi.UnmarshalException exc) {
	    System.err.println("Unmarshal exception on host "
			       + host + ".");
	    System.err.println("Try recompiling everything and starting over?");
	    System.err.println("registrar = " + registrar);
	    //XXX Advise the user?  Fatal error?
	} catch (java.rmi.ConnectException exc) {
	    if (DEBUG) {
	        System.err.println("RMI isn't running on "
				   + host + ".");
	 	System.err.println("Or maybe it is, but it "
				   + "started after this program.");
	    }
	    //XXX Update status?  We aren't connected to the Bingo
	    //XXX server, but rmiregistry is running.  I think.
	} catch (java.rmi.UnknownHostException exc) {
	    System.err.println("Unknown host: "
			       + host);
	    //XXX Update status?  Ask user to enter another host and retry?
	} catch (java.rmi.ConnectIOException exc) {
	    System.err.println("Couldn't get to host "
			       + host + ".");
	    //XXX Update status?  We might have had a network glitch.
	} catch (Exception exc) {
	    System.err.println("Unexpected exception when "
			       + "trying to find registrar.");
	    System.err.println("Exception type: "
			       + exc.toString());
            exc.printStackTrace();
	}
    }

    /* Called from player queue thread. */
    void handleRegisterEvent(RegisterEvent event) {
	if (registrar == null) {
	    lookUpRegistrar(params.getHostname()); 
	}
	if (registrar == null) {
	    return;  //XXX should do this with an exception instead.
	}

	if (ticket == null) {
	    long seed = params.getSeed(); 
	    lookUpRegistrar(params.getHostname()); 

	    //Get the ticket.
	    try {
	        ticket = registrar.mayIPlay(params.getName(),
			   	            params.getNumCards(),
					    seed);
	    } catch (java.rmi.ConnectException exc) {
	        if (DEBUG) {
	            System.err.println("Not connected to Bingo server.");
	            System.err.println("registrar = " + registrar);
	        }
	        //XXX Update status?  We aren't connected to the Bingo
	        //XXX server, but rmiregistry is running.
		return;
	    } catch (Exception exc) {
	        System.err.println("Unexpected exception on register attempt.");
	        exc.printStackTrace();
	        //XXX Update status?  
		return;
	    }

	    //React to registration results.
	    try {
	        if (ticket.ID != Ticket.DENIED) {
		    numCardWindows = ticket.cards.length;
		    final Player player = this;
		    SwingUtilities.invokeLater(new Runnable() {
			    public void run() {
			        controlPane.didRegister();
				repaint();
				//XXX Should delay the following
				//XXX (so there's feedback when you
				//XXX register)?
		                for (int i = 0; i < numCardWindows; i++) {
			            cardWindows[i] = new
				            CardWindow(ticket.cards[i],
					               player);
			            cardWindows[i].pack();
			            cardWindows[i].setVisible(true);
		                }
			    }
		        });
        	    try {
	    	        new BallListenerThread(this).start();
        	    } catch (java.io.IOException e) {
	                if (DEBUG) {
	                    System.err.println("IOException on "
			                       + "BallListenerThread "
					       + "creation/startup.");
        	        }
		    }
	        } else { 
		    showServerStatus(ticket.message);
		    ticket = null;
	        }
	    } catch (NullPointerException exc) {
	        System.err.println("NullPointerException; probably "
			           + "ticket was null");
	    } catch (Exception exc) {
	        System.err.println("Unexpected exception on register attempt.");
	        exc.printStackTrace();
	    }
	}
    }

    /* Called from player queue thread. */
    void handleIWonEvent(IWonEvent event) {
	if (registrar == null) {
            showServerStatus("This player isn't connected to a server: "
                             + "can't tell a server you won.");
	    return;
        }

	try {
	    Answer a = registrar.BINGO(ticket.ID, event.getCard());
	    if (a.didIWin) {
                showDialog(event.getCardWindow(),
		 	   "You won!");
            } else {
		showServerStatus(a.message);
                showDialog(event.getCardWindow(),
                           "You didn't win.");
            }
        } catch (RemoteException e) {
	    //...show status?
	    System.err.println("RMI Exception when "
			       + "informing server of win.");
	}
    }

    protected void showDialog(CardWindow cw, String status) {
	final String statusText = status;
	final CardWindow cardWindow = cw;
	SwingUtilities.invokeLater(new Runnable() {
	    public void run() {
		cardWindow.showStatusDialog(statusText);
	    }
	});
    }

    /** 
     * Requests that handleIWonEvent be called from
     * player queue thread.
     */
    void IWon(CardWindow cw) {
	playerQueue.postEvent(new IWonEvent(this, cw));
    }

    /*
     * Called from either player queue thread or AWT event dispatch
     * thread, as appropriate.
     * XXX Can be cleaner in 1.2.
     */
    protected void processEvent(AWTEvent event) {
        if (event instanceof StatusRequestEvent) {
            if (DEBUG) {
                System.out.println("Player processEvent received StatusRequestEvent");
            }      
            handleStatusRequestEvent((StatusRequestEvent)event);
        } else if (event instanceof RegisterEvent) {
            if (DEBUG) {
                System.out.println("Player processEvent received RegisterEvent");
            }      
            handleRegisterEvent((RegisterEvent)event);
        } else if (event instanceof IWonEvent) { 
            if (DEBUG) {
                System.out.println("Player processEvent received IWonEvent");
            }      
            handleIWonEvent((IWonEvent)event);
        } else {
	    super.processEvent(event);
	}
    }

    public static void main(String[] args) {
	JFrame frame = new JFrame(windowTitle);

	frame.addWindowListener(new WindowAdapter() {
		public void windowClosing(WindowEvent e) {
		    System.exit(0);	//XXX
		}
	    });

	Player player = new Player();
	frame.getContentPane().add("Center", player);
	frame.pack();
	frame.setVisible(true);
    }

    public static void fatalError(String message, Exception e) {
        e.printStackTrace();
        System.err.println(message);
        System.err.println("Exiting.....");
        System.exit(-1);
    }
}
