/*
 [The "BSD licence"]
 Copyright (c) 2005-2008 Terence Parr
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions
 are met:
 1. Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.
 2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.
 3. The name of the author may not be used to endorse or promote products
    derived from this software without specific prior written permission.

 THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.antlr.runtime.debug;

import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.Token;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.tree.BaseTree;
import org.antlr.runtime.tree.Tree;

import java.io.*;
import java.net.ConnectException;
import java.net.Socket;
import java.util.StringTokenizer;

public class RemoteDebugEventSocketListener implements Runnable {
	static final int MAX_EVENT_ELEMENTS = 8;
	DebugEventListener listener;
	String machine;
	int port;
	Socket channel = null;
	PrintWriter out;
	BufferedReader in;
	String event;
	/** Version of ANTLR (dictates events) */
	public String version;
	public String grammarFileName;
	/** Track the last token index we saw during a consume.  If same, then
	 *  set a flag that we have a problem.
	 */
	int previousTokenIndex = -1;
	boolean tokenIndexesInvalid = false;

	public static class ProxyToken implements Token {
		int index;
		int type;
		int channel;
		int line;
		int charPos;
		String text;
		public ProxyToken(int index) { this.index = index; }		
		public ProxyToken(int index, int type, int channel,
						  int line, int charPos, String text)
		{
			this.index = index;
			this.type = type;
			this.channel = channel;
			this.line = line;
			this.charPos = charPos;
			this.text = text;
		}
		public String getText() {
			return text;
		}
		public void setText(String text) {
			this.text = text;
		}
		public int getType() {
			return type;
		}
		public void setType(int ttype) {
			this.type = ttype;
		}
		public int getLine() {
			return line;
		}
		public void setLine(int line) {
			this.line = line;
		}
		public int getCharPositionInLine() {
			return charPos;
		}
		public void setCharPositionInLine(int pos) {
			this.charPos = pos;
		}
		public int getChannel() {
			return channel;
		}
		public void setChannel(int channel) {
			this.channel = channel;
		}
		public int getTokenIndex() {
			return index;
		}
		public void setTokenIndex(int index) {
			this.index = index;
		}
		public CharStream getInputStream() {
			return null;
		}
		public void setInputStream(CharStream input) {
		}
		public String toString() {
			String channelStr = "";
			if ( channel!=Token.DEFAULT_CHANNEL ) {
				channelStr=",channel="+channel;
			}
			return "["+getText()+"/<"+type+">"+channelStr+","+line+":"+getCharPositionInLine()+",@"+index+"]";
		}
	}

	public static class ProxyTree extends BaseTree {
		public int ID;
		public int type;
		public int line = 0;
		public int charPos = -1;
		public int tokenIndex = -1;
		public String text;
		
		public ProxyTree(int ID, int type, int line, int charPos, int tokenIndex, String text) {
			this.ID = ID;
			this.type = type;
			this.line = line;
			this.charPos = charPos;
			this.tokenIndex = tokenIndex;
			this.text = text;
		}

		public ProxyTree(int ID) { this.ID = ID; }

		public int getTokenStartIndex() { return tokenIndex; }
		public void setTokenStartIndex(int index) {	}
		public int getTokenStopIndex() { return 0; }
		public void setTokenStopIndex(int index) { }
		public Tree dupNode() {	return null; }
		public int getType() { return type; }
		public String getText() { return text; }
		public String toString() {
			return "fix this";
		}
	}

	public RemoteDebugEventSocketListener(DebugEventListener listener,
										  String machine,
										  int port) throws IOException
	{
		this.listener = listener;
		this.machine = machine;
		this.port = port;

        if( !openConnection() ) {
            throw new ConnectException();
        }
	}

	protected void eventHandler() {
		try {
			handshake();
			event = in.readLine();
			while ( event!=null ) {
				dispatch(event);
				ack();
				event = in.readLine();
			}
		}
		catch (Exception e) {
			System.err.println(e);
			e.printStackTrace(System.err);
		}
		finally {
            closeConnection();
		}
	}

    protected boolean openConnection() {
        boolean success = false;
        try {
            channel = new Socket(machine, port);
            channel.setTcpNoDelay(true);
			OutputStream os = channel.getOutputStream();
			OutputStreamWriter osw = new OutputStreamWriter(os, "UTF8");
			out = new PrintWriter(new BufferedWriter(osw));
			InputStream is = channel.getInputStream();
			InputStreamReader isr = new InputStreamReader(is, "UTF8");
			in = new BufferedReader(isr);
            success = true;
        } catch(Exception e) {
            System.err.println(e);
        }
        return success;
    }

    protected void closeConnection() {
        try {
            in.close(); in = null;
            out.close(); out = null;
            channel.close(); channel=null;
        }
        catch (Exception e) {
            System.err.println(e);
            e.printStackTrace(System.err);
        }
        finally {
            if ( in!=null ) {
                try {in.close();} catch (IOException ioe) {
                    System.err.println(ioe);
                }
            }
            if ( out!=null ) {
                out.close();
            }
            if ( channel!=null ) {
                try {channel.close();} catch (IOException ioe) {
                    System.err.println(ioe);
                }
            }
        }

    }

	protected void handshake() throws IOException {
		String antlrLine = in.readLine();
		String[] antlrElements = getEventElements(antlrLine);
		version = antlrElements[1];
		String grammarLine = in.readLine();
		String[] grammarElements = getEventElements(grammarLine);
		grammarFileName = grammarElements[1];
		ack();
		listener.commence(); // inform listener after handshake
	}

	protected void ack() {
        out.println("ack");
		out.flush();
	}

	protected void dispatch(String line) {
        //System.out.println("event: "+line);
        String[] elements = getEventElements(line);
		if ( elements==null || elements[0]==null ) {
			System.err.println("unknown debug event: "+line);
			return;
		}
		if ( elements[0].equals("enterRule") ) {
			listener.enterRule(elements[1], elements[2]);
		}
		else if ( elements[0].equals("exitRule") ) {
			listener.exitRule(elements[1], elements[2]);
		}
		else if ( elements[0].equals("enterAlt") ) {
			listener.enterAlt(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("enterSubRule") ) {
			listener.enterSubRule(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("exitSubRule") ) {
			listener.exitSubRule(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("enterDecision") ) {
			listener.enterDecision(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("exitDecision") ) {
			listener.exitDecision(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("location") ) {
			listener.location(Integer.parseInt(elements[1]),
							  Integer.parseInt(elements[2]));
		}
		else if ( elements[0].equals("consumeToken") ) {
			ProxyToken t = deserializeToken(elements, 1);
			if ( t.getTokenIndex() == previousTokenIndex ) {
				tokenIndexesInvalid = true;
			}
			previousTokenIndex = t.getTokenIndex();
			listener.consumeToken(t);
		}
		else if ( elements[0].equals("consumeHiddenToken") ) {
			ProxyToken t = deserializeToken(elements, 1);
			if ( t.getTokenIndex() == previousTokenIndex ) {
				tokenIndexesInvalid = true;
			}
			previousTokenIndex = t.getTokenIndex();
			listener.consumeHiddenToken(t);
		}
		else if ( elements[0].equals("LT") ) {
			Token t = deserializeToken(elements, 2);
			listener.LT(Integer.parseInt(elements[1]), t);
		}
		else if ( elements[0].equals("mark") ) {
			listener.mark(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("rewind") ) {
			if ( elements[1]!=null ) {
				listener.rewind(Integer.parseInt(elements[1]));
			}
			else {
				listener.rewind();
			}
		}
		else if ( elements[0].equals("beginBacktrack") ) {
			listener.beginBacktrack(Integer.parseInt(elements[1]));
		}
		else if ( elements[0].equals("endBacktrack") ) {
			int level = Integer.parseInt(elements[1]);
			int successI = Integer.parseInt(elements[2]);
			listener.endBacktrack(level, successI==DebugEventListener.TRUE);
		}
		else if ( elements[0].equals("exception") ) {
			String excName = elements[1];
			String indexS = elements[2];
			String lineS = elements[3];
			String posS = elements[4];
			Class excClass = null;
			try {
				excClass = Class.forName(excName);
				RecognitionException e =
					(RecognitionException)excClass.newInstance();
				e.index = Integer.parseInt(indexS);
				e.line = Integer.parseInt(lineS);
				e.charPositionInLine = Integer.parseInt(posS);
				listener.recognitionException(e);
			}
			catch (ClassNotFoundException cnfe) {
				System.err.println("can't find class "+cnfe);
				cnfe.printStackTrace(System.err);
			}
			catch (InstantiationException ie) {
				System.err.println("can't instantiate class "+ie);
				ie.printStackTrace(System.err);
			}
			catch (IllegalAccessException iae) {
				System.err.println("can't access class "+iae);
				iae.printStackTrace(System.err);
			}
		}
		else if ( elements[0].equals("beginResync") ) {
			listener.beginResync();
		}
		else if ( elements[0].equals("endResync") ) {
			listener.endResync();
		}
		else if ( elements[0].equals("terminate") ) {
			listener.terminate();
		}
		else if ( elements[0].equals("semanticPredicate") ) {
			Boolean result = Boolean.valueOf(elements[1]);
			String predicateText = elements[2];
			predicateText = unEscapeNewlines(predicateText);
			listener.semanticPredicate(result.booleanValue(),
									   predicateText);
		}
		else if ( elements[0].equals("consumeNode") ) {
			ProxyTree node = deserializeNode(elements, 1);
			listener.consumeNode(node);
		}
		else if ( elements[0].equals("LN") ) {
			int i = Integer.parseInt(elements[1]);
			ProxyTree node = deserializeNode(elements, 2);
			listener.LT(i, node);
		}
		else if ( elements[0].equals("createNodeFromTokenElements") ) {
			int ID = Integer.parseInt(elements[1]);
			int type = Integer.parseInt(elements[2]);
			String text = elements[3];
			text = unEscapeNewlines(text);
			ProxyTree node = new ProxyTree(ID, type, -1, -1, -1, text);
			listener.createNode(node);
		}
		else if ( elements[0].equals("createNode") ) {
			int ID = Integer.parseInt(elements[1]);
			int tokenIndex = Integer.parseInt(elements[2]);
			// create dummy node/token filled with ID, tokenIndex
			ProxyTree node = new ProxyTree(ID);
			ProxyToken token = new ProxyToken(tokenIndex);
			listener.createNode(node, token);
		}
		else if ( elements[0].equals("nilNode") ) {
			int ID = Integer.parseInt(elements[1]);
			ProxyTree node = new ProxyTree(ID);
			listener.nilNode(node);
		}
		else if ( elements[0].equals("errorNode") ) {
			// TODO: do we need a special tree here?
			int ID = Integer.parseInt(elements[1]);
			int type = Integer.parseInt(elements[2]);
			String text = elements[3];
			text = unEscapeNewlines(text);
			ProxyTree node = new ProxyTree(ID, type, -1, -1, -1, text);
			listener.errorNode(node);
		}
		else if ( elements[0].equals("becomeRoot") ) {
			int newRootID = Integer.parseInt(elements[1]);
			int oldRootID = Integer.parseInt(elements[2]);
			ProxyTree newRoot = new ProxyTree(newRootID);
			ProxyTree oldRoot = new ProxyTree(oldRootID);
			listener.becomeRoot(newRoot, oldRoot);
		}
		else if ( elements[0].equals("addChild") ) {
			int rootID = Integer.parseInt(elements[1]);
			int childID = Integer.parseInt(elements[2]);
			ProxyTree root = new ProxyTree(rootID);
			ProxyTree child = new ProxyTree(childID);
			listener.addChild(root, child);
		}
		else if ( elements[0].equals("setTokenBoundaries") ) {
			int ID = Integer.parseInt(elements[1]);
			ProxyTree node = new ProxyTree(ID);
			listener.setTokenBoundaries(
				node,
				Integer.parseInt(elements[2]),
				Integer.parseInt(elements[3]));
		}
		else {
			System.err.println("unknown debug event: "+line);
		}
	}

	protected ProxyTree deserializeNode(String[] elements, int offset) {
		int ID = Integer.parseInt(elements[offset+0]);
		int type = Integer.parseInt(elements[offset+1]);
		int tokenLine = Integer.parseInt(elements[offset+2]);
		int charPositionInLine = Integer.parseInt(elements[offset+3]);
		int tokenIndex = Integer.parseInt(elements[offset+4]);
		String text = elements[offset+5];
		text = unEscapeNewlines(text);
		return new ProxyTree(ID, type, tokenLine, charPositionInLine, tokenIndex, text);
	}

	protected ProxyToken deserializeToken(String[] elements,
										  int offset)
	{
		String indexS = elements[offset+0];
		String typeS = elements[offset+1];
		String channelS = elements[offset+2];
		String lineS = elements[offset+3];
		String posS = elements[offset+4];
		String text = elements[offset+5];
		text = unEscapeNewlines(text);
		int index = Integer.parseInt(indexS);
		ProxyToken t =
			new ProxyToken(index,
						   Integer.parseInt(typeS),
						   Integer.parseInt(channelS),
						   Integer.parseInt(lineS),
						   Integer.parseInt(posS),
						   text);
		return t;
	}

	/** Create a thread to listen to the remote running recognizer */
	public void start() {
		Thread t = new Thread(this);
		t.start();
	}

	public void run() {
		eventHandler();
	}

	// M i s c

	public String[] getEventElements(String event) {
		if ( event==null ) {
			return null;
		}
		String[] elements = new String[MAX_EVENT_ELEMENTS];
		String str = null; // a string element if present (must be last)
		try {
			int firstQuoteIndex = event.indexOf('"');
			if ( firstQuoteIndex>=0 ) {
				// treat specially; has a string argument like "a comment\n
				// Note that the string is terminated by \n not end quote.
				// Easier to parse that way.
				String eventWithoutString = event.substring(0,firstQuoteIndex);
				str = event.substring(firstQuoteIndex+1,event.length());
				event = eventWithoutString;
			}
			StringTokenizer st = new StringTokenizer(event, "\t", false);
			int i = 0;
			while ( st.hasMoreTokens() ) {
				if ( i>=MAX_EVENT_ELEMENTS ) {
					// ErrorManager.internalError("event has more than "+MAX_EVENT_ELEMENTS+" args: "+event);
					return elements;
				}
				elements[i] = st.nextToken();
				i++;
			}
			if ( str!=null ) {
				elements[i] = str;
			}
		}
		catch (Exception e) {
			e.printStackTrace(System.err);
		}
		return elements;
	}

	protected String unEscapeNewlines(String txt) {
		// this unescape is slow but easy to understand
		txt = txt.replaceAll("%0A","\n");  // unescape \n
		txt = txt.replaceAll("%0D","\r");  // unescape \r
		txt = txt.replaceAll("%25","%");   // undo escaped escape chars
		return txt;
	}

	public boolean tokenIndexesAreInvalid() {
		return false;
		//return tokenIndexesInvalid;
	}

}

