package bugbeechat;

import com.buglabs.application.IServiceProvider;
import com.buglabs.application.MainApplicationThread;

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Label;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

import com.buglabs.bug.module.bugbee.pub.IBUGBeeControl;
import com.buglabs.bug.module.bugbee.pub.IBUGBeeLEDControl;
import com.buglabs.bug.module.bugbee.pub.IBUGBeePacket;
import com.buglabs.bug.module.lcd.pub.IModuleDisplay;
import com.buglabs.device.IButtonEventProvider;

/**
 * BUGBee-to-BUGBee sample chatting application.
 * 
 * To use, create /home/root/chatboss on one (and only one) of the BUGs.
 * 
 * Once the two instances of the app are talking to each other, you can use the BUGbase
 * hotkey 1 to toggle the display of the matchbox on-screen keyboard. This will let you
 * type text into the top pane of the application's window. Anything you type into this
 * pane will be sent to the other BUG when you press RET.
 * 
 * Can also be used to communicate between a Digi XBee ZB device
 * {@linkplain http://www.digi.com/products/wireless/zigbee-mesh/xbee-zb-module.jsp} and a BUGBee; 
 * if you want to do that please get the XBee ZB's full IEEE Address using the commands ATSH and STSL,
 * then modify {@link NetworkHandler#stateMachineProc()} so that the 
 * {@link BUGBeeChatApplication#STATE_BIND_REQUEST} case statement uses that address rather
 * than an all-zeros address (in my testing the XBee ZB didn't usually accept the bind request unless
 * it was explicitly addressed to it). Note that ATSH and ATSL each make up 4 bytes of the 8 bytes of
 * the XBee ZB's IEEE address so if the value either of them returns is only 3 bytes then you will need to 
 * prepend a zero byte.
 * 
 * TODO: Add on-gui status of bind status (me->peer and peer->me)
 * 
 * TODO: CONTROL: Unable to send data to our peer, status was 0xb9.
 * <- This is "ZApsNoBoundDevice".
 *       
 * TODO: CONTROL: Ignoring unrecognized command id of 0x8047
 * <- This is not in the CC2480 Interface Specification doc
 * 
 * TODO: CONTROL: Ignoring unrecognized command id of 0x8247
 * <- This is not in the CC2480 Interface Specification doc
 *
 * TODO: Add connection-loss detection and recovery
 */
public class BUGBeeChatApplication extends MainApplicationThread implements DataReceiver {	
	/**
	 * The name of the file that is used to indicate which BUG is to
	 * act as the COORDinator of the ZigBee PAN.
	 * TODO: Make it so we don't need this - it's lame.
	 */
	private static final String COORD_FILE = "/home/root/chatboss";
	
	/**
	 * The name of the file that is used to indicate that we
	 * should echo back messages that we receive (principally for
	 * testing).
	 */
	private static final String ECHO_FILE = "/home/root/chatecho";
	
	/**
	 * The name of the file that is used to indicate that we
	 * should send out ping messages periodically (principally for
	 * testing).
	 */
	private static final String PING_FILE = "/home/root/chatping";
	
	/**
	 * Delay in milliseconds between pings; only relevant if PING_FILE exists.
	 */
	private static final int PING_DELAY = 5000;
	
	/**
	 * Debug flag to put log messages into text boxes as well as
	 * the 'last message' status fields.
	 */
	private static final boolean LOG_TO_SCREEN = false;
	
	/**
	 * font to use for our plain labels
	 */
	private static final Font labelFont = new Font("serif", Font.BOLD, 10);
	
	/**
	 * font to use for our status labels
	 */
	private static final Font statusFont = new Font("serif", Font.ITALIC, 10);
	
	/**
	 * font to use for our text areas
	 */
	private static final Font textFont = new Font("serif", Font.PLAIN, 14);
	
	/**
	 * Time, in milliseconds, to wait between polls of the data
	 * and control sockets.
	 * TODO: make blocking calls work properly, since that would be more efficient.
	 * As it is they don't get cleaned up properly in error cases so everything
	 * works better using MSG_DONTWAIT.
	 */
	private static final int THREAD_SLEEP_TIME = 300;
	
	/**
	 * Maximum number of characters to keep in our "Remote -> Local" text
	 * area. Without a cap we'd eventually run out of memory.
	 */
	private static final int MAX_SCROLLBACK = 200;
	
	/*
	 * bog-standard BUG app stuff
	 */
	private IServiceProvider serviceProv;
	private boolean ran = false;
	private IBUGBeeLEDControl led;
	private IBUGBeeControl control;
	private IBUGBeePacket packet;
	private IModuleDisplay display;
	private IButtonEventProvider button;
	private Frame frame;
	
	/**
	 * DateFormat used to display timestamps in the status labels.
	 */
	private final DateFormat dateFormat = new SimpleDateFormat("HH:mm");
	
	/**
	 * Where we display text entered by our user, via the matchbox keyboard (or otherwise)
	 */
	private TextArea local2RemoteText;
	
	/**
	 * State of the local->remote connection
	 */
	private Label local2RemoteStatus;
	
	/**
	 * Where we display text received from our peer.
	 */
	private TextArea remote2LocalText;
	
	/**
	 * State of the local->remote connection
	 */
	private Label remote2LocalStatus;
	
	/**
	 * Class to manage the matchbox virtual keyboard
	 */
	private KeyboardHandler keyboardHandler;
	
	private NetworkHandler network;
	
	private final boolean runAsCoord;
	private final boolean doEcho;
	private final boolean doPing;
	
	public BUGBeeChatApplication(IServiceProvider serviceProv) {
		this.serviceProv = serviceProv;
		
		// read out cheesy file-based "configuration"
		runAsCoord = (new File(COORD_FILE)).exists();
		doEcho = (new File(ECHO_FILE)).exists();
		doPing = (new File(PING_FILE)).exists();
	}
	
	public boolean getRan() {
		return ran;
	}
	
	private void ran() {
		ran = true;
	}
	
	/**
     * This method is invoked as a result of all services
     * becoming available for the application. The list of services is
     * obtained from the getServices() method.
     */
	public void run() {
		try {
			log("RUN: BUGBeeChatApplication");			
			serviceSetup();
			
			// do this before gui setup so gui can ask if we're coord
			network = new NetworkHandler(control, led, packet, runAsCoord, this);
			
			guiSetup();
			keyboardSetup();

			
			network.networkSetup();
			
			log("RUN: Starting main loop");
			// loop in our state machine until we're told to go away
			
			int pingCount = 0;
			long lastTimePinged = System.currentTimeMillis();

			while (!tearDownRequested) {
				network.poll();
				
				if (network.isConnected() && doPing) {
					final long now = System.currentTimeMillis();
					if ((now - lastTimePinged) > PING_DELAY) {
						final String msg; 
						if (runAsCoord) {
							msg = "Parent ping " + pingCount;
						} else {
							msg = "Child ping " + pingCount;
						}
						network.sendDataMessage(msg);
						
						++pingCount;
						
						// don't use now as we want ping end-to-start to be PING_DELAY,
						// rather than ping start-to-start
						lastTimePinged = System.currentTimeMillis();
					}					
				}
				
				try {
					Thread.sleep(THREAD_SLEEP_TIME);
		 		} catch (InterruptedException e) {
				}
			}
			
			log("RUN: BUGBeeChatApplication shutting down");
			network.networkTearDown();
			guiTearDown();			
			serviceTearDown();
			keyboardTearDown();
			log("RUN: BUGBeeChatApplication stopped");	
			
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		log("RUN: Done!");
		ran();
	}

	private void serviceSetup() {
		// hook up to all of our services
		led = (IBUGBeeLEDControl) serviceProv.getService(IBUGBeeLEDControl.class);
		control = (IBUGBeeControl) serviceProv.getService(IBUGBeeControl.class);
		packet = (IBUGBeePacket) serviceProv.getService(IBUGBeePacket.class);
		display = (IModuleDisplay) serviceProv.getService(IModuleDisplay.class);
		button = (IButtonEventProvider) serviceProv.getService(IButtonEventProvider.class);
	}

	private void serviceTearDown() {
		led = null;
		control = null;
		packet = null;
		display = null;
		button = null;
	}
	
	private void keyboardSetup() {
		keyboardHandler = new KeyboardHandler();
		button.addListener(keyboardHandler);
	}

	private void keyboardTearDown() {
		if (keyboardHandler != null) {
			if (button != null) {
				button.removeListener(keyboardHandler);				
			}
			
			// make sure the virtual keyboard is gone
			keyboardHandler.stopKeyboard();
			keyboardHandler = null;
		}
	}
	
	private void guiSetup() {
		// bring up our GUI
		createUI();
		frame.show();
	}

	private void guiTearDown() {
		frame.dispose();
		frame = null;
	}
	
	/**
	 * Create our fabulous user interface.
	 * TODO: figure out how to hide the Remote->Local text box when the
	 * virtual keyboard is on-screen; I cannot get this to work, and with
	 * them both on-screen the Local->Remote box is so squished you can
	 * hardly see what you're "typing".
	 */
	private void createUI() {
		frame = display.getFrame();
		frame.setTitle("BUGBeeChat");
	
		// set up our Local -> Remote gui stuff
		final Panel local2RemoteLabelPanel = new Panel(new GridLayout(1, 2));
		final Label local2RemoteLabel;
		if (runAsCoord) {
			local2RemoteLabel = new Label("Local -> Remote [Parent]");
			
		} else {
			local2RemoteLabel = new Label("Local -> Remote [Child]");			
		}
		local2RemoteLabel.setFont(labelFont);
		local2RemoteLabelPanel.add(local2RemoteLabel);
		
		local2RemoteStatus = new Label("not connected");
		local2RemoteStatus.setFont(statusFont);
		local2RemoteLabelPanel.add(local2RemoteStatus);
		
		local2RemoteText = new TextArea();
		local2RemoteText.setEditable(true);
		local2RemoteText.setFont(textFont);
		local2RemoteText.addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent arg0) {
				if (arg0.getKeyCode() == KeyEvent.VK_ENTER) {
					sendLocalText();					
				}
			}
			public void keyReleased(KeyEvent arg0) {}
			public void keyTyped(KeyEvent arg0) {}});
		
		final Panel local2RemotePanel = new Panel(new BorderLayout());
		local2RemotePanel.add(local2RemoteLabelPanel, BorderLayout.NORTH);
		local2RemotePanel.add(local2RemoteText, BorderLayout.CENTER);
				
		// set up our Remote -> Local gui stuff
		final Panel remote2LocalLabelPanel = new Panel(new GridLayout(1, 2));
		final Label remote2LocalLabel = new Label("Remote -> Local");
		remote2LocalLabel.setFont(labelFont);
		remote2LocalLabelPanel.add(remote2LocalLabel);
		
		remote2LocalStatus = new Label("not connected");
		remote2LocalStatus.setFont(statusFont);
		remote2LocalLabelPanel.add(remote2LocalStatus);
			
		remote2LocalText = new TextArea();
		remote2LocalText.setEditable(true);
		remote2LocalText.setFont(textFont);
		
		// listen for the user pressing Enter in the Local->Remote
		// text box, so we can send the data to Remote.
		remote2LocalText.addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent arg0) {
				if (arg0.getKeyCode() == KeyEvent.VK_ENTER) {
					sendLocalText();					
				}
			}
			public void keyReleased(KeyEvent arg0) {}
			public void keyTyped(KeyEvent arg0) {}});
		
		final Panel remote2LocalPanel = new Panel(new BorderLayout());
		remote2LocalPanel.add(remote2LocalLabelPanel, BorderLayout.NORTH);
		remote2LocalPanel.add(remote2LocalText, BorderLayout.CENTER);
		
		// put it actually on the screen
		frame.setLayout(new GridLayout(2, 1));
		frame.add(local2RemotePanel);
		frame.add(remote2LocalPanel);
		frame.show();
		
		local2RemoteText.requestFocus();
	}
	
	/**
	 * Send the text the user has entered over to our peer.
	 */
	private void sendLocalText() {
		try {
			network.sendDataMessage(local2RemoteText.getText());
			local2RemoteText.setText("");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	
	/**
     * Provides a list of service names that this application depends on.
     *
     */
	public List getServices() {
		final List services = new ArrayList();
		services.add(IBUGBeeLEDControl.class.getName());
		services.add(IBUGBeeControl.class.getName());
		services.add(IBUGBeePacket.class.getName());
		services.add(IModuleDisplay.class.getName());
		services.add(IButtonEventProvider.class.getName());
		return services;
	}
	
	
	
	private String timestampify(final String msg) {
		return dateFormat.format(new Date()) + ": " + msg;
	}
	
	private void log(final String msg) {
		System.out.println(msg);
		
		if (LOG_TO_SCREEN && (local2RemoteText != null)) {
			remote2LocalText.append("[" + msg + "]\n");
		}
	}

	public void receiveData(final String data) {
		remote2LocalText.append(data);
		trimScrollback();
		
		if (doEcho) {
			try {
				network.sendDataMessage(data);
			} catch (IOException e) {
				log("Failed to echo back msg: '" + data + "', error: " + e.getMessage());
			}
		}
	}

	private void trimScrollback() {
		int len = remote2LocalText.getText().length();
		if (len > MAX_SCROLLBACK) {
			remote2LocalText.replaceRange("", 0, len - MAX_SCROLLBACK);
			
			len = remote2LocalText.getText().length();
			remote2LocalText.select(len, len);
		}
	}
	
	public void updateLocal2RemoteStatus(final String msg) {
		local2RemoteStatus.setText(timestampify(msg));
		
		if (LOG_TO_SCREEN) {
			// don't put this in local2Remote or we'll try and send it...
			remote2LocalText.append("[" + timestampify(msg) + "]\n");
		}
	}
	
	public void updateRemote2LocalStatus(final String msg) {
		remote2LocalStatus.setText(timestampify(msg));
		
		if (LOG_TO_SCREEN) {
			remote2LocalText.append("[" + timestampify(msg) + "]\n");
		}
	}
}