/**
 * Copyright (c) 2002-2012 "Neo Technology,"
 * Network Engine for Objects in Lund AB [http://neotechnology.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.neo4j.shell.impl;

import java.io.Serializable;
import java.rmi.NoSuchObjectException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.neo4j.shell.Console;
import org.neo4j.shell.ShellClient;
import org.neo4j.shell.ShellException;
import org.neo4j.shell.ShellServer;
import org.neo4j.shell.TextUtil;

/**
 * A common implementation of a {@link ShellClient}.
 */
public abstract class AbstractClient implements ShellClient
{
    /**
     * The session key for the prompt key, just like in Bash.
     */
    public static final String PROMPT_KEY = "PS1";

    /**
     * The session key for whether or not to print stack traces for exceptions. 
     */
    public static final String STACKTRACES_KEY = "STACKTRACES";

    /**
     * When displaying node ids this variable is also used for getting an
     * appropriate property value from that node to display as the title.
     * This variable can contain many property keys (w/ regex) separated by
     * comma prioritized in order.
     */
    public static final String TITLE_KEYS_KEY = "TITLE_KEYS";

    /**
     * The maximum length of titles to be displayed.
     */
    public static final String TITLE_MAX_LENGTH = "TITLE_MAX_LENGTH";

    private static final Set<String> EXIT_COMMANDS = new HashSet<String>(
        Arrays.asList( "exit", "quit" ) );

    private Console console;
    private long timeConnection;
    private final Set<String> grabbedKeysFromServer = new HashSet<String>();
    private volatile boolean end;
    private final Collection<String> multiLine = new ArrayList<String>();
    
    public void grabPrompt()
    {
        init();
        while ( !end )
        {
            try
            {
                evaluate( readLine( getPrompt() ) );
            }
            catch ( Exception e )
            {
                if ( this.shouldPrintStackTraces() )
                {
                    e.printStackTrace();
                }
                else
                {
                    this.console.format( getShortExceptionMessage( e ) + "\n" );
                }
            }
        }
        this.shutdown();
    }

    @Override
    public void evaluate( String line ) throws ShellException
    {
        if ( EXIT_COMMANDS.contains( line ) )
        {
            end(); 
            return;
        }
        
        boolean success = false;
        try
        {
            String expandedLine = expandLine( fullLine( line ) );
            String result = getServer().interpretLine( expandedLine, session(), getOutput() );
            if ( result == null || result.trim().length() == 0 )
            {   // One-liner, or end of multi-line
                endMultiLine();
            }
            else if ( result.contains( "c" ) )
            {
                multiLine.add( line );
            }
            else if ( result.contains( "e" ) )
            {
                end();
            }
            success = true;
        }
        catch ( RemoteException e )
        {
            throw ShellException.wrapCause( e );
        }
        finally
        {
            if ( !success ) endMultiLine();
        }
    }

    private void endMultiLine()
    {
        multiLine.clear();
    }
    
    private String fullLine( String line )
    {
        if ( multiLine.isEmpty() ) return line;
        StringBuilder result = new StringBuilder();
        for ( String oneLine : multiLine ) result.append( result.length() > 0 ? "\n" : "" ).append( oneLine );
        return result.append( "\n" + line ).toString();
    }

    @Override
    public void end()
    {
        end = true;
    }
    
    private String expandLine( String line ) throws RemoteException
    {
        // Look for environment variables and expand to the real values
        return TextUtil.templateString( line, this.session().asMap() );
    }

    protected static String getShortExceptionMessage( Exception e )
    {
        return e.getMessage();
    }

    public String getPrompt()
    {
        if ( !multiLine.isEmpty() ) return "> ";
        String result = null;
        try
        {
            result = ( String ) getSessionVariable( PROMPT_KEY, null, true );
        }
        catch ( Exception e )
        {
            try
            {
                result = ( String ) getSessionVariable( PROMPT_KEY, null, false );
            }
            catch ( ShellException e1 )
            {
                throw new RuntimeException( e1 );
            }
        }
        return result;
    }

    public boolean shouldPrintStackTraces()
    {
        Object value = this.session().get( STACKTRACES_KEY );
        return this.getSafeBooleanValue( value, false );
    }

    private boolean getSafeBooleanValue( Object value, boolean def )
    {
        return value == null ? def : Boolean.parseBoolean( value.toString() );
    }

    protected void init()
    {
        try
        {
            ShellServer server = getServer();
            possiblyGrabDefaultVariableFromServer( server, PROMPT_KEY, "$ " );
            possiblyGrabDefaultVariableFromServer( server, TITLE_KEYS_KEY, null );
            possiblyGrabDefaultVariableFromServer( server, TITLE_MAX_LENGTH, null );
            getOutput().println( server.welcome() );

            // Grab a jline console if available, else a standard one.
            console = JLineConsole.newConsoleOrNullIfNotFound( this );
            if ( console == null )
            {
                System.out.println( "Want bash-like features? throw in " +
                        "jLine (http://jline.sourceforge.net) on the classpath" );
                console = new StandardConsole();
            }
            getOutput().println();
        }
        catch ( RemoteException e )
        {
            throw new RuntimeException( e );
        }
    }

    protected void possiblyGrabDefaultVariableFromServer( ShellServer server, String key,
        Serializable defaultValue )
    {
        try
        {
            if ( this.session().get( key ) == null )
            {
                grabbedKeysFromServer.add( key );
                Serializable value = server.getProperty( key );
                if ( value == null ) value = defaultValue;
                if ( value != null ) session().set( key, value );
            }
        }
        catch ( RemoteException e )
        {
            throw new RuntimeException( e );
        }
    }
    
    protected void regrabVariablesFromServer( ShellServer server )
    {
        for ( String key : grabbedKeysFromServer )
        {
            Serializable value = this.session().remove( key );
            possiblyGrabDefaultVariableFromServer( server, key, value );
        }
    }

    protected Serializable getSessionVariable( String key,
        Serializable defaultValue, boolean interpret ) throws ShellException
    {
        try
        {
            Serializable result = this.session().get( key );
            if ( result == null )
            {
                result = defaultValue;
            }
            if ( interpret && result != null )
            {
                result = this.getServer().interpretVariable( key, result, session() );
            }
            return result;
        }
        catch ( RemoteException e )
        {
            throw new RuntimeException( e );
        }
    }

    public String readLine( String prompt )
    {
        return console.readLine( prompt );
    }

    static String[] getExitCommands()
    {
        return EXIT_COMMANDS.toArray( new String[ EXIT_COMMANDS.size() ] );
    }
    
    protected void updateTimeForMostRecentConnection()
    {
        this.timeConnection = System.currentTimeMillis();
    }
    
    public long timeForMostRecentConnection()
    {
        return timeConnection;
    }
    
    public void shutdown()
    {
    }

    protected void tryUnexport( Remote remote )
    {
    	try
    	{
    		UnicastRemoteObject.unexportObject( remote, true );
    	}
    	catch ( NoSuchObjectException e )
    	{
    		System.out.println( "Couldn't unexport:" + remote );
    	}
    }
}

@@ -2302,16 +2302,22 @@
 , %22quit%22
+, null
  ) );%0A%0A 

Quit the shell on EOF (i.e. ^D) as per convention rather than parsing null