/**
 * Copyright (C) 2010-2015 eBusiness Information, Excilys Group
 *
 * 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 org.androidannotations.api;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewDebug;

/**
 * <p>
 * This class can be used to enable the use of HierarchyViewer inside an
 * application. HierarchyViewer is an Android SDK tool that can be used to
 * inspect and debug the user interface of running applications. For security
 * reasons, HierarchyViewer does not work on production builds (for instance
 * phones bought in store.) By using this class, you can make HierarchyViewer
 * work on any device. You must be very careful however to only enable
 * HierarchyViewer when debugging your application.
 * </p>
 * 
 * <p>
 * To use this view server, your application must require the INTERNET
 * permission.
 * </p>
 * 
 * <p>
 * The recommended way to use this API is to register activities when they are
 * created, and to unregister them when they get destroyed:
 * </p>
 * 
 * <pre>
 * public class MyActivity extends Activity {
 * 	public void onCreate(Bundle savedInstanceState) {
 * 		super.onCreate(savedInstanceState);
 * 		// Set content view, etc.
 * 		ViewServer.get(this).addWindow(this);
 * 	}
 * 
 * 	public void onDestroy() {
 * 		super.onDestroy();
 * 		ViewServer.get(this).removeWindow(this);
 * 	}
 * 
 * 	public void onResume() {
 * 		super.onResume();
 * 		ViewServer.get(this).setFocusedWindow(this);
 * 	}
 * }
 * </pre>
 * 
 * <p>
 * In a similar fashion, you can use this API with an InputMethodService:
 * </p>
 * 
 * <pre>
 * public class MyInputMethodService extends InputMethodService {
 * 	public void onCreate() {
 * 		super.onCreate();
 * 		View decorView = getWindow().getWindow().getDecorView();
 * 		String name = &quot;MyInputMethodService&quot;;
 * 		ViewServer.get(this).addWindow(decorView, name);
 * 	}
 * 
 * 	public void onDestroy() {
 * 		super.onDestroy();
 * 		View decorView = getWindow().getWindow().getDecorView();
 * 		ViewServer.get(this).removeWindow(decorView);
 * 	}
 * 
 * 	public void onStartInput(EditorInfo attribute, boolean restarting) {
 * 		super.onStartInput(attribute, restarting);
 * 		View decorView = getWindow().getWindow().getDecorView();
 * 		ViewServer.get(this).setFocusedWindow(decorView);
 * 	}
 * }
 * </pre>
 */
public class ViewServer implements Runnable {
	/**
	 * The default port used to start view servers.
	 */
	private static final int VIEW_SERVER_DEFAULT_PORT = 4939;
	private static final int VIEW_SERVER_MAX_CONNECTIONS = 10;
	private static final String BUILD_TYPE_USER = "user";

	// Debug facility
	private static final String LOG_TAG = "ViewServer";

	private static final String VALUE_PROTOCOL_VERSION = "4";
	private static final String VALUE_SERVER_VERSION = "4";

	// Protocol commands
	// Returns the protocol version
	private static final String COMMAND_PROTOCOL_VERSION = "PROTOCOL";
	// Returns the server version
	private static final String COMMAND_SERVER_VERSION = "SERVER";
	// Lists all of the available windows in the system
	private static final String COMMAND_WINDOW_MANAGER_LIST = "LIST";
	// Keeps a connection open and notifies when the list of windows changes
	private static final String COMMAND_WINDOW_MANAGER_AUTOLIST = "AUTOLIST";
	// Returns the focused window
	private static final String COMMAND_WINDOW_MANAGER_GET_FOCUS = "GET_FOCUS";

	private ServerSocket mServer;
	private final int mPort;

	private Thread mThread;
	private ExecutorService mThreadPool;

	private final List<WindowListener> mListeners = new CopyOnWriteArrayList<ViewServer.WindowListener>();

	private final Map<View, String> mWindows = new HashMap<View, String>();
	private final ReentrantReadWriteLock mWindowsLock = new ReentrantReadWriteLock();

	private View mFocusedWindow;
	private final ReentrantReadWriteLock mFocusLock = new ReentrantReadWriteLock();

	private static ViewServer sServer;

	/**
	 * Returns a unique instance of the ViewServer. This method should only be
	 * called from the main thread of your application. The server will have the
	 * same lifetime as your process.
	 * 
	 * If your application does not have the <code>android:debuggable</code>
	 * flag set in its manifest, the server returned by this method will be a
	 * dummy object that does not do anything. This allows you to use the same
	 * code in debug and release versions of your application.
	 * 
	 * @param context
	 *            A Context used to check whether the application is debuggable,
	 *            this can be the application context
	 * @return the instance of the view server.
	 */
	public static ViewServer get(Context context) {
		ApplicationInfo info = context.getApplicationInfo();
		if (BUILD_TYPE_USER.equals(Build.TYPE) && (info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
			if (sServer == null) {
				sServer = new ViewServer(ViewServer.VIEW_SERVER_DEFAULT_PORT);
			}

			if (!sServer.isRunning()) {
				try {
					sServer.start();
				} catch (IOException e) {
					Log.d(LOG_TAG, "Error:", e);
				}
			}
		} else {
			sServer = new NoopViewServer();
		}

		return sServer;
	}

	private ViewServer() {
		mPort = -1;
	}

	/**
	 * Creates a new ViewServer associated with the specified window manager on
	 * the specified local port. The server is not started by default.
	 * 
	 * @param port
	 *            The port for the server to listen to.
	 * 
	 * @see #start()
	 */
	private ViewServer(int port) {
		mPort = port;
	}

	/**
	 * Starts the server.
	 * 
	 * @return True if the server was successfully created, or false if it
	 *         already exists.
	 * @throws IOException
	 *             If the server cannot be created.
	 * 
	 * @see #stop()
	 * @see #isRunning() see WindowManagerService#startViewServer(int)
	 */
	public boolean start() throws IOException {
		if (mThread != null) {
			return false;
		}

		mThread = new Thread(this, "Local View Server [port=" + mPort + "]");
		mThreadPool = Executors.newFixedThreadPool(VIEW_SERVER_MAX_CONNECTIONS);
		mThread.start();

		return true;
	}

	/**
	 * Stops the server.
	 * 
	 * @return True if the server was stopped, false if an error occurred or if
	 *         the server wasn't started.
	 * 
	 * @see #start()
	 * @see #isRunning() see WindowManagerService#stopViewServer()
	 */
	public boolean stop() {
		if (mThread != null) {
			mThread.interrupt();
			if (mThreadPool != null) {
				try {
					mThreadPool.shutdownNow();
				} catch (SecurityException e) {
					Log.w(LOG_TAG, "Could not stop all view server threads");
				}
			}

			mThreadPool = null;
			mThread = null;

			try {
				mServer.close();
				mServer = null;
				return true;
			} catch (IOException e) {
				Log.w(LOG_TAG, "Could not close the view server");
			}
		}

		mWindowsLock.writeLock().lock();
		try {
			mWindows.clear();
		} finally {
			mWindowsLock.writeLock().unlock();
		}

		mFocusLock.writeLock().lock();
		try {
			mFocusedWindow = null;
		} finally {
			mFocusLock.writeLock().unlock();
		}

		return false;
	}

	/**
	 * Indicates whether the server is currently running.
	 * 
	 * @return True if the server is running, false otherwise.
	 * 
	 * @see #start()
	 * @see #stop() see WindowManagerService#isViewServerRunning()
	 */
	public boolean isRunning() {
		return mThread != null && mThread.isAlive();
	}

	/**
	 * Invoke this method to register a new view hierarchy.
	 * 
	 * @param activity
	 *            The activity whose view hierarchy/window to register
	 * 
	 * @see #addWindow(View, String)
	 * @see #removeWindow(Activity)
	 */
	public void addWindow(Activity activity) {
		String name = activity.getTitle().toString();
		if (TextUtils.isEmpty(name)) {
			name = activity.getClass().getCanonicalName() + "/0x" + System.identityHashCode(activity);
		} else {
			name += "(" + activity.getClass().getCanonicalName() + ")";
		}
		addWindow(activity.getWindow().getDecorView(), name);
	}

	/**
	 * Invoke this method to unregister a view hierarchy.
	 * 
	 * @param activity
	 *            The activity whose view hierarchy/window to unregister
	 * 
	 * @see #addWindow(Activity)
	 * @see #removeWindow(View)
	 */
	public void removeWindow(Activity activity) {
		removeWindow(activity.getWindow().getDecorView());
	}

	/**
	 * Invoke this method to register a new view hierarchy.
	 * 
	 * @param view
	 *            A view that belongs to the view hierarchy/window to register
	 * @param name
	 *            The name of the view hierarchy/window to register
	 * 
	 * @see #removeWindow(View)
	 */
	public void addWindow(View view, String name) {
		mWindowsLock.writeLock().lock();
		try {
			mWindows.put(view.getRootView(), name);
		} finally {
			mWindowsLock.writeLock().unlock();
		}
		fireWindowsChangedEvent();
	}

	/**
	 * Invoke this method to unregister a view hierarchy.
	 * 
	 * @param view
	 *            A view that belongs to the view hierarchy/window to unregister
	 * 
	 * @see #addWindow(View, String)
	 */
	public void removeWindow(View view) {
		mWindowsLock.writeLock().lock();
		try {
			mWindows.remove(view.getRootView());
		} finally {
			mWindowsLock.writeLock().unlock();
		}
		fireWindowsChangedEvent();
	}

	/**
	 * Invoke this method to change the currently focused window.
	 * 
	 * @param activity
	 *            The activity whose view hierarchy/window hasfocus, or null to
	 *            remove focus
	 */
	public void setFocusedWindow(Activity activity) {
		setFocusedWindow(activity.getWindow().getDecorView());
	}

	/**
	 * Invoke this method to change the currently focused window.
	 * 
	 * @param view
	 *            A view that belongs to the view hierarchy/window that has
	 *            focus, or null to remove focus
	 */
	public void setFocusedWindow(View view) {
		mFocusLock.writeLock().lock();
		try {
			mFocusedWindow = view == null ? null : view.getRootView();
		} finally {
			mFocusLock.writeLock().unlock();
		}
		fireFocusChangedEvent();
	}

	/**
	 * Main server loop.
	 */
	@Override
	public void run() {
		try {
			mServer = new ServerSocket(mPort, VIEW_SERVER_MAX_CONNECTIONS, InetAddress.getLocalHost());
		} catch (Exception e) {
			Log.w(LOG_TAG, "Starting ServerSocket error: ", e);
		}

		while (mServer != null && Thread.currentThread() == mThread) {
			// Any uncaught exception will crash the system process
			try {
				Socket client = mServer.accept();
				if (mThreadPool != null) {
					mThreadPool.submit(new ViewServerWorker(client));
				} else {
					try {
						client.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			} catch (Exception e) {
				Log.w(LOG_TAG, "Connection error: ", e);
			}
		}
	}

	private static boolean writeValue(Socket client, String value) {
		boolean result;
		BufferedWriter out = null;
		try {
			OutputStream clientStream = client.getOutputStream();
			out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);
			out.write(value);
			out.write("\n");
			out.flush();
			result = true;
		} catch (Exception e) {
			result = false;
		} finally {
			if (out != null) {
				try {
					out.close();
				} catch (IOException e) {
					result = false;
				}
			}
		}
		return result;
	}

	private void fireWindowsChangedEvent() {
		for (WindowListener listener : mListeners) {
			listener.windowsChanged();
		}
	}

	private void fireFocusChangedEvent() {
		for (WindowListener listener : mListeners) {
			listener.focusChanged();
		}
	}

	private void addWindowListener(WindowListener listener) {
		if (!mListeners.contains(listener)) {
			mListeners.add(listener);
		}
	}

	private void removeWindowListener(WindowListener listener) {
		mListeners.remove(listener);
	}

	private interface WindowListener {
		void windowsChanged();

		void focusChanged();
	}

	private static class UncloseableOuputStream extends OutputStream {
		private final OutputStream mStream;

		UncloseableOuputStream(OutputStream stream) {
			mStream = stream;
		}

		@Override
		public void close() throws IOException {
			// Don't close the stream
		}

		@Override
		public boolean equals(Object o) {
			return mStream.equals(o);
		}

		@Override
		public void flush() throws IOException {
			mStream.flush();
		}

		@Override
		public int hashCode() {
			return mStream.hashCode();
		}

		@Override
		public String toString() {
			return mStream.toString();
		}

		@Override
		public void write(byte[] buffer, int offset, int count) throws IOException {
			mStream.write(buffer, offset, count);
		}

		@Override
		public void write(byte[] buffer) throws IOException {
			mStream.write(buffer);
		}

		@Override
		public void write(int oneByte) throws IOException {
			mStream.write(oneByte);
		}
	}

	private static class NoopViewServer extends ViewServer {
		private NoopViewServer() {
		}

		@Override
		public boolean start() throws IOException {
			return false;
		}

		@Override
		public boolean stop() {
			return false;
		}

		@Override
		public boolean isRunning() {
			return false;
		}

		@Override
		public void addWindow(Activity activity) {
		}

		@Override
		public void removeWindow(Activity activity) {
		}

		@Override
		public void addWindow(View view, String name) {
		}

		@Override
		public void removeWindow(View view) {
		}

		@Override
		public void setFocusedWindow(Activity activity) {
		}

		@Override
		public void setFocusedWindow(View view) {
		}

		@Override
		public void run() {
		}
	}

	private class ViewServerWorker implements Runnable, WindowListener {
		private Socket mClient;
		private boolean mNeedWindowListUpdate;
		private boolean mNeedFocusedWindowUpdate;

		private final Object[] mLock = new Object[0];

		public ViewServerWorker(Socket client) {
			mClient = client;
			mNeedWindowListUpdate = false;
			mNeedFocusedWindowUpdate = false;
		}

		@Override
		public void run() {
			BufferedReader in = null;
			try {
				in = new BufferedReader(new InputStreamReader(mClient.getInputStream()), 1024);

				final String request = in.readLine();

				String command;
				String parameters;

				int index = request.indexOf(' ');
				if (index == -1) {
					command = request;
					parameters = "";
				} else {
					command = request.substring(0, index);
					parameters = request.substring(index + 1);
				}

				boolean result;
				if (COMMAND_PROTOCOL_VERSION.equalsIgnoreCase(command)) {
					result = writeValue(mClient, VALUE_PROTOCOL_VERSION);
				} else if (COMMAND_SERVER_VERSION.equalsIgnoreCase(command)) {
					result = writeValue(mClient, VALUE_SERVER_VERSION);
				} else if (COMMAND_WINDOW_MANAGER_LIST.equalsIgnoreCase(command)) {
					result = listWindows(mClient);
				} else if (COMMAND_WINDOW_MANAGER_GET_FOCUS.equalsIgnoreCase(command)) {
					result = getFocusedWindow(mClient);
				} else if (COMMAND_WINDOW_MANAGER_AUTOLIST.equalsIgnoreCase(command)) {
					result = windowManagerAutolistLoop();
				} else {
					result = windowCommand(mClient, command, parameters);
				}

				if (!result) {
					Log.w(LOG_TAG, "An error occurred with the command: " + command);
				}
			} catch (IOException e) {
				Log.w(LOG_TAG, "Connection error: ", e);
			} finally {
				if (in != null) {
					try {
						in.close();

					} catch (IOException e) {
						e.printStackTrace();
					}
				}
				if (mClient != null) {
					try {
						mClient.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}

		private boolean windowCommand(Socket client, String command, String parameters) {
			boolean success = true;
			BufferedWriter out = null;

			try {
				// Find the hash code of the window
				int index = parameters.indexOf(' ');
				if (index == -1) {
					index = parameters.length();
				}
				final String code = parameters.substring(0, index);
				int hashCode = (int) Long.parseLong(code, 16);

				// Extract the command's parameter after the window description
				if (index < parameters.length()) {
					parameters = parameters.substring(index + 1);
				} else {
					parameters = "";
				}

				final View window = findWindow(hashCode);
				if (window == null) {
					return false;
				}

				// call stuff
				final Method dispatch = ViewDebug.class.getDeclaredMethod("dispatchCommand", View.class, String.class, String.class, OutputStream.class);
				dispatch.setAccessible(true);
				dispatch.invoke(null, window, command, parameters, new UncloseableOuputStream(client.getOutputStream()));

				if (!client.isOutputShutdown()) {
					out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
					out.write("DONE\n");
					out.flush();
				}

			} catch (Exception e) {
				Log.w(LOG_TAG, "Could not send command " + command + " with parameters " + parameters, e);
				success = false;
			} finally {
				if (out != null) {
					try {
						out.close();
					} catch (IOException e) {
						success = false;
					}
				}
			}

			return success;
		}

		private View findWindow(int hashCode) {
			if (hashCode == -1) {
				View window = null;
				mWindowsLock.readLock().lock();
				try {
					window = mFocusedWindow;
				} finally {
					mWindowsLock.readLock().unlock();
				}
				return window;
			}

			mWindowsLock.readLock().lock();
			try {
				for (Entry<View, String> entry : mWindows.entrySet()) {
					if (System.identityHashCode(entry.getKey()) == hashCode) {
						return entry.getKey();
					}
				}
			} finally {
				mWindowsLock.readLock().unlock();
			}

			return null;
		}

		private boolean listWindows(Socket client) {
			boolean result = true;
			BufferedWriter out = null;

			try {
				mWindowsLock.readLock().lock();

				OutputStream clientStream = client.getOutputStream();
				out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);

				for (Entry<View, String> entry : mWindows.entrySet()) {
					out.write(Integer.toHexString(System.identityHashCode(entry.getKey())));
					out.write(' ');
					out.append(entry.getValue());
					out.write('\n');
				}

				out.write("DONE.\n");
				out.flush();
			} catch (Exception e) {
				result = false;
			} finally {
				mWindowsLock.readLock().unlock();

				if (out != null) {
					try {
						out.close();
					} catch (IOException e) {
						result = false;
					}
				}
			}

			return result;
		}

		private boolean getFocusedWindow(Socket client) {
			boolean result = true;
			String focusName = null;

			BufferedWriter out = null;
			try {
				OutputStream clientStream = client.getOutputStream();
				out = new BufferedWriter(new OutputStreamWriter(clientStream), 8 * 1024);

				View focusedWindow = null;

				mFocusLock.readLock().lock();
				try {
					focusedWindow = mFocusedWindow;
				} finally {
					mFocusLock.readLock().unlock();
				}

				if (focusedWindow != null) {
					mWindowsLock.readLock().lock();
					try {
						focusName = mWindows.get(mFocusedWindow);
					} finally {
						mWindowsLock.readLock().unlock();
					}

					out.write(Integer.toHexString(System.identityHashCode(focusedWindow)));
					out.write(' ');
					out.append(focusName);
				}
				out.write('\n');
				out.flush();
			} catch (Exception e) {
				result = false;
			} finally {
				if (out != null) {
					try {
						out.close();
					} catch (IOException e) {
						result = false;
					}
				}
			}

			return result;
		}

		@Override
		public void windowsChanged() {
			synchronized (mLock) {
				mNeedWindowListUpdate = true;
				mLock.notifyAll();
			}
		}

		@Override
		public void focusChanged() {
			synchronized (mLock) {
				mNeedFocusedWindowUpdate = true;
				mLock.notifyAll();
			}
		}

		private boolean windowManagerAutolistLoop() {
			addWindowListener(this);
			BufferedWriter out = null;
			try {
				out = new BufferedWriter(new OutputStreamWriter(mClient.getOutputStream()));
				while (!Thread.interrupted()) {
					boolean needWindowListUpdate = false;
					boolean needFocusedWindowUpdate = false;
					synchronized (mLock) {
						while (!mNeedWindowListUpdate && !mNeedFocusedWindowUpdate) {
							mLock.wait();
						}
						if (mNeedWindowListUpdate) {
							mNeedWindowListUpdate = false;
							needWindowListUpdate = true;
						}
						if (mNeedFocusedWindowUpdate) {
							mNeedFocusedWindowUpdate = false;
							needFocusedWindowUpdate = true;
						}
					}
					if (needWindowListUpdate) {
						out.write("LIST UPDATE\n");
						out.flush();
					}
					if (needFocusedWindowUpdate) {
						out.write("FOCUS UPDATE\n");
						out.flush();
					}
				}
			} catch (Exception e) {
				Log.w(LOG_TAG, "Connection error: ", e);
			} finally {
				if (out != null) {
					try {
						out.close();
					} catch (IOException e) {
						// Ignore
					}
				}
				removeWindowListener(this);
			}
			return true;
		}
	}
}