/*
 [The "BSD license"]
 Copyright (c) 2005-2009 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.eclipse.persistence.internal.libraries.antlr.runtime.debug;

import org.eclipse.persistence.internal.libraries.antlr.runtime.RecognitionException;
import org.eclipse.persistence.internal.libraries.antlr.runtime.Token;
import org.eclipse.persistence.internal.libraries.antlr.runtime.CharStream;
import org.eclipse.persistence.internal.libraries.antlr.runtime.tree.BaseTree;
import org.eclipse.persistence.internal.libraries.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;
		}

		@Override
		public String getText() {
			return text;
		}

		@Override
		public void setText(String text) {
			this.text = text;
		}

		@Override
		public int getType() {
			return type;
		}

		@Override
		public void setType(int ttype) {
			this.type = ttype;
		}

		@Override
		public int getLine() {
			return line;
		}

		@Override
		public void setLine(int line) {
			this.line = line;
		}

		@Override
		public int getCharPositionInLine() {
			return charPos;
		}

		@Override
		public void setCharPositionInLine(int pos) {
			this.charPos = pos;
		}

		@Override
		public int getChannel() {
			return channel;
		}

		@Override
		public void setChannel(int channel) {
			this.channel = channel;
		}

		@Override
		public int getTokenIndex() {
			return index;
		}

		@Override
		public void setTokenIndex(int index) {
			this.index = index;
		}

		@Override
		public CharStream getInputStream() {
			return null;
		}

		@Override
		public void setInputStream(CharStream input) {
		}

		@Override
		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; }

		@Override public int getTokenStartIndex() { return tokenIndex; }
		@Override public void setTokenStartIndex(int index) {	}
		@Override public int getTokenStopIndex() { return 0; }
		@Override public void setTokenStopIndex(int index) { }
		@Override public Tree dupNode() {	return null; }
		@Override public int getType() { return type; }
		@Override public String getText() { return text; }
		@Override 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]), elements[2].equals("true"));
		}
		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<? extends RecognitionException> excClass;
			try {
				excClass = Class.forName(excName).asSubclass(RecognitionException.class);
				RecognitionException e = 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,
									   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();
	}

	@Override
	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;
	}

}

