package org.dhappy.mimis;

import org.apache.cocoon.sax.component.XMLGenerator;
import org.apache.cocoon.sax.component.XMLSerializer;
import org.apache.cocoon.sax.SAXPipelineComponent;
import org.apache.cocoon.pipeline.NonCachingPipeline;
import org.apache.cocoon.pipeline.Pipeline;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; 

import org.neo4j.kernel.EmbeddedGraphDatabase;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.Traverser;
import org.neo4j.graphdb.TraversalPosition;
import org.neo4j.graphdb.Traverser.Order;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.StopEvaluator;
import org.neo4j.graphdb.ReturnableEvaluator;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.NotFoundException;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.SASLAuthentication;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Message;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Stack;
import java.util.Iterator;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

public class Mimis {
    private static final Log log = LogFactory.getLog( Mimis.class );

    protected static EmbeddedGraphDatabase graphDb;
    static Pattern pathSplitter;

    public static GraphDatabaseService getGraph() {
        return getGraph( null );
    }

    protected static GraphDatabaseService getGraph( String dbURI ) {
        if( graphDb != null && dbURI != null
	    && dbURI != graphDb.getStoreDir() ) {
            graphDb.shutdown();
            graphDb = null;
        }
            
        if( graphDb == null ) {
	    // Default database URI
	    dbURI = ( dbURI == null ) ? ".mimis/host/files" : dbURI;
            graphDb = new EmbeddedGraphDatabase( dbURI );
        }
	return graphDb;
    }
    
    public static Holon load( String key ) throws IOException {
        //( key -== "/" ) // key = key[1] == "/" ? key[1:] : key // whole line suceeds or fails
        //&& ( key []: =
        log.debug( "Searching for: " + key );
        //key =: 1 // key = key[1:]
        if( key.startsWith( "/" ) ) { // Load from local filesystem
            key = key.substring( 1 );
            if( key.startsWith( "~/" ) ) {
                key = ( System.getProperty( "user.home" ) + File.separator
                        + key.substring( 2 ) );
            }
            try {
                return load( key, new FileInputStream( key ) );
            } catch( FileNotFoundException fnf ) {
                log.info( "Not Found: " + key );
            }
        }
        return null;
    }

    public static Holon load( String key, File input ) throws IOException {
        Holon position = load( key, new FileInputStream( input ) );
        // ToDo: Save extra metadata
        return position;
    }

    public static Holon load( String key, InputStream input ) throws IOException {
        log.debug( "Input: " + key );
        FileOutputStream output = null;
        try {
            output = new FileOutputStream( "target/pipeline." + input.hashCode() + ".out" );
            return load( key, input, output );
        } finally {
            if( output != null ) {
                output.close();
            }
        }
    }
    
    public static Holon load( String key, InputStream input, OutputStream output ) throws IOException {
        try {
            MutableHolon mark = null;
            log.info( "Loading: " + key );
            try {
            mark = new MutableHolon( getGraph() );
            SaveSpot recorder = new SaveSpot( mark );
            Pipeline<SAXPipelineComponent> pipeline =
                new NonCachingPipeline<SAXPipelineComponent>();
            pipeline.addComponent( new XMLGenerator( input ));
            
            pipeline.addComponent( recorder.getSAXPipeline() );
            pipeline.addComponent( new XMLSerializer() );
            
            pipeline.setup( output );
            pipeline.execute();
            mark.commit();
            } catch(Exception e) {
                log.error( "Load Error: ", e );
            }

            return mark;
        } catch( Exception ex ) {
            throw new IOException( ex );
        }
    }

    static class PathTracker {
        int lastDepth = -1; // Traversal depth starts at 0
        Stack<String> seekPath = new Stack<String>();
        Stack<String> currentPath = new Stack<String>();
        
        public PathTracker( String key ) {
            Mimis.decomposeKey( key, seekPath );
        }

        public void step( int depth, Node location ) {
            String name = "";
            try {
                name = location.getProperty( "name" ).toString();
            } catch( NotFoundException nfe ) {
            }
            step( depth, name );
        }

        public void step( int depth, String name ) {
            if( depth == lastDepth
                && ( name.length() == 0
                     || ( currentPath.size() > 0
                          && currentPath.peek().equals( name ) ) ) ) {
                return;
            }
            
            int delta = depth - lastDepth;
            lastDepth = depth;

            if( delta > 1 ) {
                log.info( "Unexpected Delta: [" + name + "]: " + delta );
            }
            
            // Child of last node and not empty
            if( delta >= 0 && name.length() > 0 ) {
                if( delta == 0 ) { // Sibling
                    currentPath.pop();
                }
                currentPath.push( name );
            } else if( delta < 0 ) { // Traversal returning
                currentPath.pop();
            }

            if( currentPath.size() <= seekPath.size() ) {
                int seekIdx = Math.max( 0, currentPath.size() - 1 );
                log.debug( "list:[" + lastDepth + "][" + name + "] ?== " +
                           seekPath.elementAt( seekIdx ) );
            }
            
        }

        public boolean returnable() {
            log.debug( "list:returnable:equal: " + currentPath.equals( seekPath ) );
            return currentPath.equals( seekPath );
        }

        public boolean viable() {
            log.debug( "list:viable:sublist: " + Collections.indexOfSubList( seekPath, currentPath ) );
            return ( seekPath.size() < currentPath.size()
                     && Collections.indexOfSubList( seekPath, currentPath ) != 0 );
        }
    }

    public static Listing list() {
        return list( null );
    }

    public static Listing list( final Object key ) {
        final String path = key == null ? "" : key.toString();
        log.debug( "list:path = " + path );

        Map config = new HashMap<String, Object>() {{
                put( "order", Traverser.Order.DEPTH_FIRST );
                put( "type", SaveSpot.SaveType.DOCSYSTEM );
                put( "direction", Direction.OUTGOING );
            }};

        if( key == null ) {
            //config.put( "stop", StopEvaluator.DEPTH_ONE );
            config.put( "stop", StopEvaluator.END_OF_GRAPH );
            config.put( "return", ReturnableEvaluator.ALL_BUT_START_NODE );
        } else {
            final PathTracker tracker = new PathTracker( path );
            config.put( "return", new ReturnableEvaluator() {
                public boolean isReturnableNode( TraversalPosition position ) {
                    tracker.step( position.depth(), position.currentNode() );
                    return tracker.returnable();
                } } );
            config.put( "stop", new StopEvaluator() {
                public boolean isStopNode( TraversalPosition position ) {
                    tracker.step( position.depth(), position.currentNode() );
                    return tracker.viable();
                } } );
        }
    
        return new OneOffTraverser( config );
    }

    public static Node createNode( Map<String, Object> config ) {
        Node node = getGraph().createNode();

        for( Map.Entry<String, Object> e : config.entrySet() ) {
            node.setProperty( e.getKey(), e.getValue() );
        }
        return node;
    }

    public static Stack<String> decomposeKey( String key ) {
        return decomposeKey( key, new Stack<String>() );
    }

    public static Stack<String> decomposeKey( String key, Stack<String> path ) {
        if( pathSplitter == null ) {
            impress( new HashMap<String, Object>() {{
                        put( "separators", "\\s+|/|:|\\.|,|-" );
                    }} );
        }
        Matcher match = pathSplitter.matcher( key );
        while( match.find() ) {
            for( int i = 1; i <= match.groupCount(); i++ ) {
                path.push( match.group( i ) );
            }
        }
        return path;
    }

    public static void impress( Map<String, Object> config ) {
        if( config.containsKey( "separators" ) ) {
            String separators = config.get( "separators" ).toString();
            pathSplitter = Pattern.compile( "(" + separators + "|[^" + separators + "]+)" );
        }
    }

    public static void shutdown() {
        getGraph().shutdown();
        graphDb = null;
    }

    public static void main( String[] args ) {
        log.debug( "Starting Mimis" );

        // Used for signalling ending
        final Object hold = new Object();
    
        int idx = 0;
        String scheme = "jabber";
        String userid = "mimis.test@gmail.com";
        String password = "mimistest";
        String server = "talk.google.com";
        String port = "5222";
        String resource = "bot/mimis/";
        
        if( args.length == 1 ) {
            String urn = args[1];

            //[ scheme, uri ] = urn.split[:][1];
            idx = urn.indexOf( ':' );
            scheme = urn.substring( 0, idx );
            String uri = urn.substring( idx + 1 );
            
            //[ connection, resource ] = uri.split[/][1]
            idx = uri.indexOf( '/' );
            String connection = uri.substring( 0, idx );
            resource = uri.substring( idx + 1 );

            //[ credentials, server ] = connection.split[@][-1]
            idx =  connection.lastIndexOf( '@' );
            String credentials = connection.substring( 0, idx );
            server = connection.substring( idx + 1 );

            //[ server, port ] = server.split[':'][1]
            idx = server.indexOf( ':' );
            if( idx < 0 ) idx = server.length();
            port = server.substring( idx + 1 );
            server = server.substring( 0, idx );

            //[ username, password ] = credentials.split[:][1]
            idx = credentials.indexOf( ':' );
            //if( idx < 0 ) idx = credentials.length;
            if( idx < 0 ) idx = credentials.length();
            userid = credentials.substring( 0, idx );
            password = uri.substring( idx + 1 );
        }

        // [ username, domain ] = userid.split[@][1] || [ userid, server ]
        idx = userid.indexOf( '@' );
        String username = idx > 0 ? userid.substring( 0, idx ) : userid;
        String domain = idx > 0 ? userid.substring( idx + 1 ) : server;

        log.info( "Connecting: " +
                  scheme + ":" +
                  username + ":" +
                  password + "@" +
                  domain + ":" +
                  port + "/" +
                  resource + " @ " +
                  server );
        
        //config = new ConnectionConfiguration( server, (int)port, resource );
        ConnectionConfiguration config = new ConnectionConfiguration( server, Integer.valueOf( port ), domain );
        XMPPConnection.DEBUG_ENABLED = true;
        try {
            SASLAuthentication.supportSASLMechanism( "PLAIN", 0 );
            final XMPPConnection connection = new XMPPConnection( config );
            connection.connect();

            connection.login( userid, password, resource );
            
            PacketFilter mimisFilter = new PacketFilter() {
                    public boolean accept( Packet packet ) {
                        return true;
                    }
                };
            
            PacketListener mimisListener = new PacketListener() {
                    int count = 0;

                    public void processPacket( Packet packet ) {
                        count += 1;
                        if( packet instanceof Message ) {
                            Message message = (Message)packet;
                            String thread = message.getThread();
                            String subject = message.getSubject();
                            String body = message.getBody();
                            
                            Message response = new Message();
                            response.setTo( message.getFrom() );
                            response.setThread( thread );

                            Pattern command = Pattern.compile( "(ls|die|load)(?:\\s+(.*\\S))?\\s*$" );
                            Matcher match = command.matcher( body );
                            if( match.matches() ) {
                                String cmd = match.group( 1 );
                                String arg = match.group( 2 );

                                String sub = "packet[" + count + "]:" + cmd + ":" + arg;
                                response.setSubject( sub );
                                log.info( sub );

                                StringBuffer out = new StringBuffer();
                                String msg;

                                if( "ls".equals( cmd ) ) {
                                    try {
                                    Traverser list = list( arg );
                                    msg = "packet[" + count + "]:list[][name]";
                                    out.append( msg + "\n" );
                                    for( Node node : list ) {
                                        try {
                                            msg = "[][name] = " + node.getProperty( "name" );
                                        } catch( NotFoundException e ) {
                                            msg = "[][name] = \\0";
                                        }
                                        out.append( msg + "\n" );
                                    }
                                    log.debug( "Done Listing" );
                                    } catch(Exception e) {
                                        log.error( "List", e );
                                    }
                                } else if( "load".equals( cmd ) ) {
                                    msg = "packet[" + count + "]:load";
                                    try {
                                        load( arg );
                                    } catch( IOException ioe ) {
                                        msg += " = \\0";
                                    }
                                    log.debug( msg );
                                    out.append( msg + "\n" );
                                } else if( "die".equals( cmd ) ) {
                                    msg = "packet[" + count + "]:die";
                                    log.debug( msg );
                                    out.append( msg + "\n" );
                                    synchronized (hold) {
                                        hold.notify();
                                    }
                                } else {
                                    msg = "packet[" + count + "]:" + cmd;
                                    log.debug( msg );
                                    out.append( cmd + "\n" );
                                }
                                response.setBody( out.toString() );
                            } else {
                                log.error( "Unknown: " + body );
                                response.setSubject( "Unknown" );
                                response.setBody( "..." );
                            }
                            connection.sendPacket( response );
                        } else {
                            log.debug( "Packet: " + packet );
                        }
                    } };
            connection.addPacketListener( mimisListener, mimisFilter );
        } catch( XMPPException ex ) {
            log.error( "Connection Error", ex );
        }

        try {
            log.info( "Holding" );
            synchronized (hold) {
                hold.wait();
            }
        } catch( InterruptedException ie ) {
        } finally {
            log.info( "Shutting Down" );
            Mimis.shutdown();
        }
    }

    static class OneOffTraverser implements Listing, Iterator<Node> {
        Transaction tx = graphDb.beginTx();
        Traverser traverser;
        
        Iterator<Node> iterator;
        Node current;
        Node next;
        TraversalPosition currentPosition;
 
        Node start;
        
        public OneOffTraverser( Map<String, Object> config ) {
            impress( config );
            if( start == null ) {
                start = graphDb.getReferenceNode();
            }
            traverser = start.traverse( (Traverser.Order)config.get( "order" ),
                                        (StopEvaluator)config.get( "stop" ),
                                        (ReturnableEvaluator)config.get( "return" ),
                                        (RelationshipType)config.get( "type" ),
                                        (Direction)config.get( "direction" ) );
        }

        public void impress( Map<String, Object> config ) {
            if( config.containsKey( "start" ) && config.containsKey( "tx" ) ) {
                if( tx != null ) {
                    tx.finish();
                }
                start = (Node)config.get( "start" );
                tx = (Transaction)config.get( "tx" );
            }
        }

        public TraversalPosition currentPosition() {
            return currentPosition;
        }

        public Collection<Node> getAllNodes() {
            return null;
        }

        public Iterator<Node> iterator() {
            return this;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

        public void prime() {
            if( iterator == null ) {
                iterator = traverser.iterator();
                next();
            }
        }

        public boolean hasNext() {
            prime();
            boolean hasNext = next != null;
            if( ! hasNext ) {
                log.debug( "Listing Finished" );
                tx.success();
                tx.finish();
            }
            return hasNext;
        }
        
        public Node next() {
            if( iterator == null ) {
                prime();
            }
            current = next;
            currentPosition = traverser.currentPosition();
            next = iterator.hasNext() ? iterator.next() : null;
            return current;
        }

        public Node getNode() {
            return current;
        }

        public Holon top() {
            final Node current = this.current;
            return new Holon() {
                public Node getNode() { return current; }
                public void impress( Map<String, Object> config ) {}
            };
        }

        public void finalize() {
            if( tx != null ) {
                tx.finish();
            }
        }
    }
}