/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 flash.tools.debugger.concrete;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.EnumMap;

import flash.tools.debugger.SessionManager;
import flash.util.Trace;

/**
 * Implements the lower portion of Flash Player debug protocol.  This class is able to
 * communicate with the Flash Player sending and receiving any and all messages neccessary
 * in order to continue a debug session with the Player.
 * 
 * It does not understand the context of messages that it receives and merely provides
 * a channel for formatting and unformatting the messages.
 *  
 * The messages are defined on the flash side in core/debugtags.h and handled in the 
 * code under core/playerdebugger.cpp
 * 
 * Messages that are received via this class are packaged in a DMessage and then
 * provided to any listeners if requested.   Filtering of incoming messages 
 * at this level is not supported.
 */
public class DProtocol implements Runnable
{
	public static final int DEBUG_PORT = 7935;
	
	/* We connect to AIR in the case of AIR on Android over USB */
	public static final int DEBUG_CONNECT_PORT = 7936;

	private final BufferedInputStream	m_in;
	private final BufferedOutputStream	m_out;
	private final EnumMap<ListenerIndex, DProtocolNotifierIF> m_listeners; // WARNING: accessed from multiple threads
	private long						m_msgRx;		// WARNING: accessed from multiple threads; use synchronized (this)
	private long						m_msgTx;		// WARNING: accessed from multiple threads; use synchronized (this)
	private volatile boolean			m_stopRx;		// WARNING: accessed from multiple threads
	private volatile Thread				m_rxThread;		// WARNING: accessed from multiple threads
	private volatile Exception m_disconnectCause;
	private volatile Socket m_socket;
	private boolean m_detectBrokenSocket;

	public enum ListenerIndex
	{
		PlayerSession,

		/**
		 * The DMessageCounter must always be the LAST listener, so that the message has
		 * been fully processed before we wake up any threads that were waiting until a
		 * message comes in.
		 */
		MessageCounter
	}

	public DProtocol(BufferedInputStream in, BufferedOutputStream out)
	{
		m_in = in;
		m_out = out;
		m_listeners = new EnumMap<ListenerIndex, DProtocolNotifierIF>(ListenerIndex.class);
		m_msgRx = 0;
		m_msgTx = 0;
		m_stopRx = false;
		m_rxThread = null;
		m_socket = null;
		m_detectBrokenSocket = false;
		// Create a message counter, which will listen to us for messages
		addListener(ListenerIndex.MessageCounter, new DMessageCounter());
	}
	
	public DProtocol(BufferedInputStream in, BufferedOutputStream out, 
			Socket s, boolean detectBrokenSocket)
	{
		this(in, out);
		m_socket = s;
		m_detectBrokenSocket = detectBrokenSocket;
	}
	
	
	/**
	 * Set the base socket options
	 * @throws SocketException
	 */
	static void applyBaseSocketSettings(Socket s) throws SocketException 
	{
		// For performance reasons, it is very important that we setTcpNoDelay(true),
		// thus disabling Nagle's algorithm.  Google for TCP_NODELAY or Nagle
		// for more information.
		//
		// In addition, we now use a BufferedOutputStream instead of an OutputStream.
		//
		// These changes result in a huge speedup on the Mac.
		s.setTcpNoDelay(true);		
	}
	
	static DProtocol createDProtocolFromSocket(Socket s, boolean detectBrokenSocket) throws IOException
	{
		BufferedInputStream in = new BufferedInputStream(s.getInputStream());
		BufferedOutputStream out = new BufferedOutputStream(s.getOutputStream());

		DProtocol dp = new DProtocol(in, out, s, detectBrokenSocket);
		return dp;
	}

	/**
     * Build a DProtocol object from a the given socket connection.
     */
	static DProtocol createFromSocket(Socket s) throws IOException
	{
		applyBaseSocketSettings(s);
		return createDProtocolFromSocket(s, false);
	}
	
	/**
     * Build a DProtocol object from a the given socket connection
     * and applies socket specific settings set in SessionManager
     * like socket timeout.
     */
	static DProtocol createFromSocket(Socket s, SessionManager sessionManager) throws IOException
	{
		applyBaseSocketSettings(s);
		int socketTimeout = sessionManager.getPreference(SessionManager.PREF_SOCKET_TIMEOUT);
		boolean checkSocket = false;
		if (socketTimeout > 0)
		{
			s.setSoTimeout(socketTimeout);
			checkSocket = true;
		}
		return createDProtocolFromSocket(s, checkSocket);
	}

	/**
	 * Allow outside entities to listen for incoming DMessages.
	 * 
	 * @param index
	 *            the index of this listener. Listeners have a strictly defined
	 *            order.
	 * @param n
	 *            the listener
	 */
	public boolean addListener(ListenerIndex index, DProtocolNotifierIF n)
	{
		synchronized (m_listeners)
		{
			m_listeners.put(index, n);
		}
		return true;
	}

	public long messagesReceived()		{ synchronized (this) { return m_msgRx; } }
	public long messagesSent()			{ synchronized (this) { return m_msgTx; } }

	/**
	 * Entry point for our receive thread 
	 */
	public void run()
	{
		try
		{
			m_stopRx = false;
			listenForMessages();
		}
		catch(Exception ex) 
		{  
			m_disconnectCause = ex;
			if (Trace.error &&
				!(ex instanceof SocketException && ex.getMessage().equalsIgnoreCase("socket closed"))) // closed-socket is not an error //$NON-NLS-1$
			{
				ex.printStackTrace();
			}
		}

		/* notify our listeners that we are no longer listening;  game over */
		DProtocolNotifierIF[] listeners;
		synchronized (m_listeners)
		{
			listeners = m_listeners.values().toArray(new DProtocolNotifierIF[m_listeners.size()]); // copy the list to avoid multithreading problems
		}
		for (int i=0; i<listeners.length; ++i)
		{
			DProtocolNotifierIF elem = listeners[i];
			try
			{
				elem.disconnected();
			}
			catch(Exception exc) /* catch unchecked exceptions */
			{
				if (Trace.error)
					exc.printStackTrace();
			}
		}

		// final notice that this thread is dead! 
		m_rxThread = null;
		m_socket = null;
	}

	/** 
	 * Create and start up a thread for our receiving messages.  
	 */
	public boolean bind()
	{
		/* create a new thread object for us which just listens to incoming messages */
		boolean worked = true;
		if (m_rxThread == null)
		{
			getMessageCounter().clearInCounts();
			getMessageCounter().clearOutCounts();

			m_rxThread = new Thread(this, "DJAPI message listener"); //$NON-NLS-1$
			m_rxThread.setDaemon(true);
			m_rxThread.start();
		}
		else
			worked = false;

		return worked;
	}

	/**
	 * Shutdown our receive thread 
	 */
	public boolean unbind()
	{
		boolean worked = true;
		if (m_rxThread == null)
			worked = false;
		else
			m_stopRx = true;

		return worked;
	}

	/**
	 * Main rx loop which waits for commands and then issues them to anyone listening.
     */
	void listenForMessages() throws IOException
	{
		DProtocolNotifierIF[] listeners = new DProtocolNotifierIF[0];

		while(!m_stopRx)
		{
			/* read the data */
			try
			{
				DMessage msg = rxMessage();

				/* Now traverse our list of interested parties and let them deal with the message */
				synchronized (m_listeners)
				{
					listeners = m_listeners.values().toArray(listeners); // copy the array to avoid multithreading problems
				}
				for (int i=0; i<listeners.length; ++i)
				{
					DProtocolNotifierIF elem = listeners[i];
					try
					{
						elem.messageArrived(msg, this);
					}
					catch (Exception exc) /* catch unchecked exceptions */
					{
//						if (Trace.error) 
//						{
							System.err.println("Error in listener parsing incoming message :"); //$NON-NLS-1$
							System.err.println(msg.inToString(16));
							exc.printStackTrace(); 
//						}
					}
					msg.reset();  /* allow others to reparse the message */
				}

				/* now dispose with the message */
				DMessageCache.free(msg);
			}
			catch(InterruptedIOException iio)
			{ 
				// this is a healthy exception that we simply ignore, since it means we haven't seen
				// data for a while; is all.
			}
		}
	}

	/**
	 * Transmit the message down the socket.
	 * 
	 * This function is not synchronized; it is only called from one place, which is
	 * PlayerSession.sendMessage().  That function is synchronized.
	 */
	void txMessage(DMessage message) throws IOException
	{
		int size = message.getSize();
		int command = message.getType();

        //System.out.println("txMessage: " + DMessage.outTypeName(command) + " size=" + size);

        writeDWord(size);
		writeDWord(command);
		writeData(message.getData(), size);

		m_out.flush();
		synchronized (this) { m_msgTx++; }
		getMessageCounter().messageSent(message);
	}

	class SendThread extends Thread {
		public IOException exception = null;
		public volatile boolean completed = false;
		
		@Override
		public void run() {
			try {
				DMessage dm = DMessageCache.alloc(4);
				dm.setType(DMessage.OutSetSquelch);
				dm.putDWord(1);
				txMessage(dm);
				DMessageCache.free(dm);
				this.completed = true;
			}
			catch (IOException e) {
				this.exception = e;
			}
		}
	}
	/** 
     * Get the next message on the input stream, using the context contained within 
     * the message itself to demark its end
     */
	private DMessage rxMessage() throws IOException
	{
        int size = -1;
		int command = 0;

		try 
		{
			size = (int)readDWord();
			command = (int)readDWord();
		}
		catch (SocketTimeoutException e) 
		{
			if (!m_detectBrokenSocket)
				throw e;
			//schedule a simple message to be sent for
			//heartbeat check 
			/**
			 * Our logic kicks in after PREF_SOCKET_TIMEOUT 
			 * milliseconds to try and detect broken connection by writing 
			 * a squelch message to the player. If the write 
			 * succeeds, we assume everything is normal 
			 * (we don't wait for an ack). Otherwise, we save the error
			 * that clients of FDB can use.
			 * 
			 * On Mac, the write() blocks which is why it must
			 * be done in a separate thread. The thread may take
			 * upto five minutes to die even after interrupt().
			 * 
			 * On Windows, the write() succeeds, but we later get
			 * a recv abort.
			 */
			int oldBufferSize = -1;
			
			if (m_socket != null) {
				oldBufferSize = m_socket.getSendBufferSize();
				m_socket.setSendBufferSize(1);				
			}
			
			SendThread t = new SendThread();
			t.start();
			long waitBegin = System.currentTimeMillis();
			
			while (true) {
				try {
					t.join(1000);
					if (t.completed)
						break;
				} catch (InterruptedException e1) {
					break;
				}
				long waitEnd = System.currentTimeMillis();
				if (waitEnd - waitBegin > 10000)
					break;
			}
			boolean success = true;
			if (t.isAlive()) {
				t.interrupt();
				success = false;
			}
			if ((m_socket != null) && (oldBufferSize > 0)) {
				m_socket.setSendBufferSize(oldBufferSize);
			}
			if (!t.completed) {
				success = false;
			}
			if (t.exception != null) {
				throw t.exception;
			}
			if (success)
				throw e;
			else
				throw new SocketException("Broken pipe"); //$NON-NLS-1$
		}
        //System.out.println("rxMessage: " + DMessage.inTypeName(command) + " size=" + size);

		if (size < 0)
			throw new IOException("socket closed"); //$NON-NLS-1$

//		if (DMessage.inTypeName(command).startsWith("InUnknown")) {
//			System.out.println("Ignoring unknown message");
//			size = 0; 
//		}
		
		/** 
		 * Ask our message cache for a message
		 */
		DMessage message = DMessageCache.alloc(size);
		byte[] messageContent = message.getData();
		int offset = 0;

		/* block until we get the entire message, which may come in pieces */
		while (offset < size)
			offset += m_in.read(messageContent, offset, size - offset);

		/* now we have the data of the message, set its type and we are done */
		message.setType(command);
		synchronized (this) { m_msgRx++; }
		return message;
	}

	void writeDWord(long dw) throws IOException
	{
		byte b0 = (byte)(dw & 0xff);
		byte b1 = (byte)((dw >> 8) & 0xff);
		byte b2 = (byte)((dw >> 16) & 0xff);
		byte b3 = (byte)((dw >> 24) & 0xff);

		m_out.write(b0);
		m_out.write(b1);
		m_out.write(b2);
		m_out.write(b3);
	}

	void writeData(byte[] data, long size) throws IOException
	{
		if (size > 0)
			m_out.write(data, 0, (int)size);
	}


	/**
	 * Extract the next 4 bytes, which form a 32b integer, from the stream
	 */
	long readDWord() throws IOException
	{
		int b0 = m_in.read();
		int b1 = m_in.read();
		int b2 = m_in.read();
		int b3 = m_in.read();
		
		long value = ((b3 << 24) & 0xff000000) | ((b2 << 16) & 0xff0000) | ((b1 << 8) & 0xff00) | (b0 & 0xff);
		return value;
	}

	public DMessageCounter getMessageCounter()
	{
		synchronized (m_listeners)
		{
			return (DMessageCounter) m_listeners.get(ListenerIndex.MessageCounter);
		}
	}

	public Exception getDisconnectCause() {
		return m_disconnectCause;
	}
	
}
