public class

Junction

extends Junction
/*
 * Copyright (C) 2010 Stanford University
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package edu.stanford.junction.provider.jx;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URI;

import org.json.JSONException;
import org.json.JSONObject;

import edu.stanford.junction.JunctionException;
import edu.stanford.junction.api.activity.ActivityScript;
import edu.stanford.junction.api.activity.JunctionActor;
import edu.stanford.junction.api.messaging.MessageHeader;
import edu.stanford.junction.provider.jx.JXServer.Log;
import edu.stanford.junction.provider.jx.json.JsonHandler;
import edu.stanford.junction.provider.jx.json.JsonSocketHandler;

public class Junction extends edu.stanford.junction.Junction {
	private static String TAG = "jx_client";
	public static String JX_SYS_MSG = "jxsysmsg";
	public static String JX_SCRIPT = "ascript";
	public static String JX_JOINED = "joined";
	public static String JX_CREATOR = "creator";
	public static String JX_NS = "jx";
	
	private final URI mAcceptedInvitation;
	private final String mSession;
	private ActivityScript mActivityScript;
	private Object mJoinLock = new Object();
	private boolean mJoinComplete = false;
	private boolean mActivityCreator = false;
	
	private ConnectedThread mConnectedThread;
	private JXServer mSwitchboardServer = null;
	
	public Junction(URI uri, ActivityScript script, final JunctionActor actor) throws JunctionException {
		this.setActor(actor);
		
		mAcceptedInvitation = uri;
		mActivityScript = script;
		mSession = uri.getPath().substring(1);
		String host = uri.getHost();
		int port = uri.getPort();
		if (port == -1) port = JXServer.SERVER_PORT;
		
		// TODO: one connection per host (multiple subscriptions through one socket)
		// handle in Provider
		try {
			try {
				String my_ip = JunctionProvider.getLocalIpAddress();
				if (my_ip.equals(host)) {
					Log.d(TAG, "Starting local switchboard service");
					mSwitchboardServer = new JXServer();
					mSwitchboardServer.start();
				}
			} catch (Exception e) {
				Log.e(TAG, "Could not start local switchboard service", e);
			}
			
			Socket socket = new Socket(host, port);
			mConnectedThread = new ConnectedThread(socket);
			mConnectedThread.start();
		} catch (IOException e) {
			Log.e(TAG, "Error connecting to socket", e);
		}
		
		int MAX_TIME = 20000;
		synchronized(mJoinLock) {
			if (!mJoinComplete) {
				try {
					mJoinLock.wait(MAX_TIME);
				} catch (InterruptedException e) {
					// Ignored
				}
			}
		}
		if (!mJoinComplete) {
			throw new JunctionException("Timeout while joining Junction session.");
		}
		
		triggerActorJoin(mActivityCreator);
	}
	
	@Override
	public void disconnect() {
		if (mConnectedThread != null) {
			mConnectedThread.cancel();
		}
		
		if (mSwitchboardServer != null) {
			mSwitchboardServer.stop();
		}
	}

	@Override
	public URI getAcceptedInvitation() {
		return mAcceptedInvitation;
	}

	@Override
	public ActivityScript getActivityScript() {
		return mActivityScript;
	}

	@Override
	public URI getBaseInvitationURI() {
		try {
			return new URI("junction://localhost#jx");
		} catch (Exception e) {
			return null;
		}
	}

	@Override
	public String getSessionID() {
		return mSession;
	}

	@Override
	public String getSwitchboard() {
		return mAcceptedInvitation.getHost();
	}

	@Override
	public void doSendMessageToActor(String actorID, JSONObject message) {
		try {
			JSONObject jx;
			if (message.has(NS_JX)) {
				jx = message.getJSONObject(NS_JX);
			} else {
				jx = new JSONObject();
				message.put(NS_JX, jx);
			}
			
			JSONObject send = new JSONObject();
			send.put("action", "send_a");
			send.put("session", mSession);
			send.put("actor", actorID);
			jx.put(JX_SYS_MSG, send);
			jx.put("from", getActor().getActorID());
			mConnectedThread.sendJson(message);
		} catch (Exception e) {
			Log.e(TAG, "Failed to send message", e);
		}
	}

	@Override
	public void doSendMessageToRole(String role, JSONObject message) {
		// TODO
		doSendMessageToSession(message);
	}

	@Override
	public void doSendMessageToSession(JSONObject message) {
		try {
			JSONObject jx;
			if (message.has(NS_JX)) {
				jx = message.getJSONObject(NS_JX);
			} else {
				jx = new JSONObject();
				message.put(NS_JX, jx);
			}
			
			JSONObject send = new JSONObject();
			send.put("action", "send_s");
			send.put("session", mSession);
			
			jx.put(JX_SYS_MSG, send);
			jx.put("from", getActor().getActorID());
			mConnectedThread.sendJson(message);
		} catch (Exception e) {
			Log.e(TAG, "Failed to send message", e);
		}
	}
	
	
    /**
     * This thread runs during a connection with a remote device.
     * It handles all incoming and outgoing transmissions.
     */
    private class ConnectedThread extends Thread {
        private final Socket mmSocket;
        private final InputStream mmInStream;
        private final OutputStream mmOutStream;
        private final JsonHandler mJsonHelper;

        public ConnectedThread(Socket socket) {
            Log.d(TAG, "create ConnectedThread");
            
            mmSocket = socket;
            InputStream tmpIn = null;
            OutputStream tmpOut = null;

            try {
                tmpIn = socket.getInputStream();
                tmpOut = socket.getOutputStream();
            } catch (IOException e) {
                Log.e(TAG, "temp sockets not created", e);
            }

            mmInStream = tmpIn;
            mmOutStream = tmpOut;
            mJsonHelper = new JsonSocketHandler(mmInStream, mmOutStream);
            
            connect();
        }
        
        public void connect() {
        	JSONObject join = new JSONObject();
        	
        	// Header info
        	try {
	        	byte[] header = "JUNCTION".getBytes();
	        	mmOutStream.write(header, 0, header.length);
	        	mmOutStream.flush();
	        	
	        	// TODO: fix socket timing issues and remove me
	        	try {
	        		Thread.sleep(800);
	        	}catch (Exception e) {}
        	} catch (IOException e) {
        		Log.e(TAG, "Error writing connection header");
        		return;
        	}
        	
        	// Join request
        	try {
        		JSONObject greeting = new JSONObject();
        		greeting.put("join", mSession);
        		greeting.put("id", getActor().getActorID());
        		if (mActivityScript != null) {
        			greeting.put("script", mActivityScript.getJSON());
        		}
        		
        		JSONObject envelop = new JSONObject();
        		envelop.put(JX_SYS_MSG, greeting);
        		join.put(NS_JX, envelop);
        	} catch (JSONException e) {
				throw new AssertionError("Bad JSON");
			}
        	
        	try {
        		mJsonHelper.sendJson(join);
        	} catch (IOException e) {
        		Log.e(TAG, "Error writing activity script", e);
        	}
        }

        public void run() {
            Log.d(TAG, "BEGIN mConnectedThread");
            
            // Keep listening to the InputStream while connected
            while (true) {
                try {
                    JSONObject json = mJsonHelper.jsonFromStream();
                    if (json == null) {
                    	break;
                    }

                    if (json.has(NS_JX)) {
                    	JSONObject sys = json.getJSONObject(NS_JX);
                    	if (sys.has(JX_SYS_MSG)) {
                    		if (sys.has(JX_JOINED)) {
                    			if (sys.has(JX_SCRIPT)) {
                    				mActivityScript = new ActivityScript(sys.getJSONObject(JX_SCRIPT));
                    			}
                    			if (sys.has(JX_CREATOR) && sys.getBoolean(JX_CREATOR)) {
                    				mActivityCreator = true;
                    			} else {
                    				mActivityCreator = false;
                    			}
                    		}
                    		
                    		synchronized (mJoinLock) {
                    			mJoinComplete = true;
                    			mJoinLock.notify();
                    		}
                    		json = null;
                    	}
                    }
                    
                    if (json != null) {
                    	String from = "[Unknown]";
	                    if (json.has(NS_JX) && json.optJSONObject(NS_JX).has("from")) {
	                    	from = json.optJSONObject(NS_JX).optString("from");
	                    }
	                    MessageHeader header = new MessageHeader(Junction.this, json, from);
	                    triggerMessageReceived(header, json);
                    }
                } catch (IOException e) {
                    Log.e(TAG, "disconnected", e);
                    //connectionLost();
                    break;
                } catch (JSONException e) {
                     	Log.e(TAG, "JSON error", e);
                }
            }
            
            // No longer listening.
            cancel();
        }

        public void sendJson(JSONObject json) {
        	try {
        		mJsonHelper.sendJson(json);
        	} catch (IOException e) {
        		Log.e(TAG, "Error sending json", e);
        	}
        }
        
        public void cancel() {
            try {
                mmSocket.close();
            } catch (IOException e) {
                Log.e(TAG, "close() of connect socket failed", e);
            }
        }
    }
	
}