/*
 * Mojito Distributed Hash Table (Mojito DHT)
 * Copyright (C) 2006-2007 LimeWire LLC
 *
 * This program 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 2 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
 
package org.limewire.mojito;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.MessageDigest;
import java.util.concurrent.Future;

import org.limewire.io.SecureInputStream;
import org.limewire.io.SecureOutputStream;
import org.limewire.mojito.db.DHTValueType;
import org.limewire.mojito.db.Database;
import org.limewire.mojito.db.StorableModelManager;
import org.limewire.mojito.db.impl.DHTValueImpl;
import org.limewire.mojito.result.BootstrapResult;
import org.limewire.mojito.result.FindNodeResult;
import org.limewire.mojito.result.FindValueResult;
import org.limewire.mojito.result.PingResult;
import org.limewire.mojito.result.StoreResult;
import org.limewire.mojito.routing.RouteTable;
import org.limewire.mojito.routing.Version;
import org.limewire.mojito.routing.RouteTable.SelectMode;
import org.limewire.mojito.routing.impl.LocalContact;
import org.limewire.mojito.settings.LookupSettings;
import org.limewire.mojito.statistics.DHTStats;
import org.limewire.mojito.util.CollectionUtils;

/**
 * Executes various features of the Mojito DHT. <code>CommandHandler</code>
 * is useful in running the Mojito DHT from the command line.
 * Additionally, <code>CommandHandler</code> writes information about 
 * the {@link MojitoDHT} to a {@link PrintWriter}. 
 */
public class CommandHandler {
    
    private static final String[] COMMANDS = {
            "help",
            "info",
            "ping .+ \\d{1,5}",
            "reqstats .+ \\d{1,5} (stats|rt|db)",
            "bootstrap .+ \\d{1,5}",
            "put (key|kuid) (\\w|\\d)+ (value|file) .+",
            "remove (key|kuid) (\\w|\\d)+",
            "get (key|kuid) (\\w|\\d)+",
            "lookup (key|kuid) (\\w|\\d)+",
            "database",
            "publisher",
            "routetable",
            "store .+",
            "load .+",
            "kill",
            "stats",
            "restart",
            "firewalled",
            "exhaustive",
            "id .+",
            "select .+",
            "nextid",
            "rt_gui",
            "arcs_gui",
            "test",
            "bootstrapped"
    };
    
    public static boolean handle(MojitoDHT dht, String command, PrintWriter out) {
        try {
            command = command.trim();
            for (String c : COMMANDS) {
                if (command.matches(c)) {
                    String[] args = command.split(" ");
                    Method m = CommandHandler.class.getDeclaredMethod(args[0], MojitoDHT.class, String[].class, PrintWriter.class);
                    m.invoke(null, dht, args, out);
                    return true;
                }
            }
        } catch (NoSuchMethodException err) {
            err.printStackTrace(out);
        } catch (InvocationTargetException err) {
            err.printStackTrace(out);
        } catch (IllegalAccessException err) {
            err.printStackTrace(out);
        } finally {
            out.flush();
        }
        
        return false;
    }
    
    public static void help(MojitoDHT dht, String[] args, PrintWriter out) {
        for (String c : COMMANDS) {
            out.println(c);
        }
    }
    
    public static void info(MojitoDHT dht, String[] args, PrintWriter out) {
        out.println(dht.toString());
    }
    
    public static void exhaustive(MojitoDHT dht, String[] args, PrintWriter out) {
        boolean current = LookupSettings.EXHAUSTIVE_VALUE_LOOKUP.getValue();
        LookupSettings.EXHAUSTIVE_VALUE_LOOKUP.setValue(
                !LookupSettings.EXHAUSTIVE_VALUE_LOOKUP.getValue());
        
        out.println("Exhaustive: " + current + " -> " + LookupSettings.EXHAUSTIVE_VALUE_LOOKUP.getValue());
    }
    
    public static void firewalled(MojitoDHT dht, String[] args, PrintWriter out) {
        ((LocalContact)dht.getLocalNode()).setFirewalled(!dht.isFirewalled());
        out.println("Firewalled: " + dht.isFirewalled());
    }
    
    public static void database(MojitoDHT dht, String[] args, PrintWriter out) {
        StringBuilder buffer = new StringBuilder("\n");
        Database database = dht.getDatabase();
        buffer.append(database.toString());
        out.println(buffer);
    }
    
    public static void publisher(MojitoDHT dht, String[] args, PrintWriter out) {
        StringBuilder buffer = new StringBuilder("\n");
        StorableModelManager modelManager = dht.getStorableModelManager();
        buffer.append(modelManager.toString());
        out.println(buffer);
    }
    
    public static void routetable(MojitoDHT dht, String[] args, PrintWriter out) {
        StringBuilder buffer = new StringBuilder("\n");
        RouteTable routingTable = dht.getRouteTable();
        buffer.append(routingTable.toString());
        out.println(buffer);
    }
    
    public static Future<PingResult> ping(MojitoDHT dht, String[] args, final PrintWriter out) {
        String host = args[1];
        int port = Integer.parseInt(args[2]);
        
        SocketAddress addr = new InetSocketAddress(host, port);
        
        out.println("Pinging... " + addr);
        /*dht.ping(addr, new PingListener() {
            public void handleResult(Contact result) {
                out.println("Ping to " + result + " succeeded: " + result.getRoundTripTime() + "ms");
                out.flush();
            }
            
            public void handleException(Exception ex) {
                if (ex instanceof DHTException) {
                    DHTException dhtEx = (DHTException)ex;
                    KUID nodeId = dhtEx.getNodeID();
                    SocketAddress address = dhtEx.getSocketAddress();
                    out.println("Ping to " + ContactUtils.toString(nodeId, address) + " failed");
                } else {
                    ex.printStackTrace(out);
                }
                out.flush();
            }
        });*/
        
        Future<PingResult> future = dht.ping(addr);
        try {
            PingResult result = future.get();
            out.println(result);
        } catch (Exception err) {
            err.printStackTrace(out);
        }
        out.flush();
        
        return future;
    }
    
    public static void reqstats(MojitoDHT dht, String[] args, PrintWriter out) {
    }
    
    public static void bootstrap(MojitoDHT dht, String[] args, final PrintWriter out) {
        String host = args[1];
        int port = Integer.parseInt(args[2]);
        
        SocketAddress addr = new InetSocketAddress(host, port);
        
        out.println("Bootstrapping... " + addr);
        try {
            PingResult pong = dht.ping(addr).get();
            BootstrapResult result = dht.bootstrap(pong.getContact()).get();
            out.println("Bootstraping finished:\n" + result);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace(out);
        }
    }
    
    public static void put(MojitoDHT dht, String[] args, final PrintWriter out) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA1");
            
            KUID key = null;
            byte[] value = null;
            
            if (args[1].equals("kuid")) {
                key = KUID.createWithHexString(args[2]);
            } else {
                key = KUID.createWithBytes(md.digest(args[2].getBytes("UTF-8")));
            }
            md.reset();
            
            if (args[3].equals("value")) {
                value = args[4].getBytes("UTF-8");
            } else if (args[3].equals("file")) {
                File file = new File(args[4]);
                byte[] data = new byte[(int)Math.min(1024, file.length())];
                FileInputStream in = new FileInputStream(file);
                in.read(data, 0, data.length);
                in.close();
                value = data;
            }
            md.reset();
            
            out.println("Storing... " + key);
            /*dht.put(key, value, new StoreListener() {
                public void store(KeyValue keyValue, Collection nodes) {
                    StringBuilder buffer = new StringBuilder();
                    buffer.append("STORED KEY_VALUES: ").append(keyValue).append("\n");
                    int i = 0;
                    for (Iterator iter = nodes.iterator(); iter.hasNext();) {
                        Contact node = (Contact) iter.next();
                        buffer.append(i).append(": ").append(node).append("\n");
                        i++;
                    }
                    out.println(buffer.toString());
                    out.flush();
                }
            });*/
            
            StoreResult evt = dht.put(key, new DHTValueImpl(DHTValueType.TEST, Version.ZERO, value)).get();
            StringBuilder buffer = new StringBuilder();
            buffer.append("STORE RESULT:\n");
            buffer.append(evt.toString());
            out.println(buffer.toString());
        } catch (Exception err) {
            err.printStackTrace(out);
        }
    }
    
    public static void remove(MojitoDHT dht, String[] args, final PrintWriter out) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA1");
            
            KUID key = null;
            if (args[1].equals("kuid")) {
                key = KUID.createWithHexString(args[2]);
            } else {
                key = KUID.createWithBytes(md.digest(args[2].getBytes("UTF-8")));
            }
            md.reset();
            
            out.println("Removing... " + key);
            /*dht.remove(key, new StoreListener() {
                public void handleResult(Entry<KeyValue, List<Contact>> result) {
                    StringBuffer buffer = new StringBuffer();
                    buffer.append("REMOVED KEY_VALUES: ").append(result.getKey()).append("\n");
                    buffer.append(CollectionUtils.toString(result.getValue()));
                    out.println(buffer.toString());
                    out.flush();
                }
                
                public void handleException(Exception ex) {
                    ex.printStackTrace(out);
                    out.flush();
                }
            });*/
            
            StoreResult evt = dht.remove(key).get();
            StringBuilder buffer = new StringBuilder();
            buffer.append("REMOVE RESULT:\n");
            buffer.append(evt.toString());
            out.println(buffer.toString());
            
        } catch (Exception e) {
            e.printStackTrace(out);
        }
    }
    
    public static void get(MojitoDHT dht, String[] args, PrintWriter out) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA1");
            
            KUID key = null;
            if (args[1].equals("kuid")) {
                key = KUID.createWithHexString(args[2]);
            } else {
                key = KUID.createWithBytes(md.digest(args[2].getBytes("UTF-8")));
            }
            md.reset();
            
            EntityKey lookupKey = EntityKey.createEntityKey(key, DHTValueType.ANY);
            FindValueResult evt = dht.get(lookupKey).get();
            out.println(evt.toString());
            
        } catch (Exception e) {
            e.printStackTrace(out);
        }
    }
    
    public static void lookup(MojitoDHT dht, String[] args, PrintWriter out) {
    	try {
            MessageDigest md = MessageDigest.getInstance("SHA1");
            
            KUID key = null;
            if (args[1].equals("kuid")) {
                key = KUID.createWithHexString(args[2]);
            } else {
                key = KUID.createWithBytes(md.digest(args[2].getBytes("UTF-8")));
            }
            md.reset();
            
            FindNodeResult evt = ((Context)dht).lookup(key).get();
            out.println(evt.toString());
            
        } catch (Exception e) {
            e.printStackTrace(out);
        }
    }
    
    public static MojitoDHT load(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        File file = new File(args[1]);
        out.println("Loading: " + file);
        ObjectInputStream ois = null;
        
        try {
            ois = new ObjectInputStream(
                    new BufferedInputStream(
                        new SecureInputStream(
                            new FileInputStream(file))));
            RouteTable routeTable = (RouteTable)ois.readObject();
            Database database = (Database)ois.readObject();
            
            MojitoDHT mojito = MojitoFactory.createDHT(dht.getName());
            synchronized (mojito) {
                mojito.setRouteTable(routeTable);
                mojito.setDatabase(database);
            }
            return mojito;
        } finally {
            try {
                if (ois != null) { ois.close(); }
            } catch (IOException ignore) {}
        }
    }
    
    public static void store(MojitoDHT dht, String[] args, PrintWriter out) throws IOException {
        File file = new File(args[1]);
        out.println("Storing: " + file);
        ObjectOutputStream oos = null;
        
        try {
            oos = new ObjectOutputStream(
                    new BufferedOutputStream(
                        new SecureOutputStream(
                            new FileOutputStream(file))));
            synchronized (dht) {
                oos.writeObject(dht.getRouteTable());
                oos.writeObject(dht.getDatabase());
            }
            oos.flush();
        } finally {
            try {
                if (oos != null) { oos.close(); }
            } catch (IOException ignore) {}
        }
    }
    
    public static void stats(MojitoDHT dht, String[] args, PrintWriter out) throws IOException {
        DHTStats stats = ((Context)dht).getDHTStats();
        stats.dump(out, true);
    }
    
    public static void id(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        KUID nodeId = KUID.createWithHexString(args[1]);
        out.println("Setting NodeID to: " + nodeId);
        Method m = dht.getClass().getDeclaredMethod("setLocalNodeID", new Class[]{KUID.class});
        m.setAccessible(true);
        m.invoke(dht, new Object[]{nodeId});
    }
    
    public static void select(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        KUID nodeId = KUID.createWithHexString(args[1]);
        out.println("Selecting: " + nodeId);
        
        RouteTable routeTable = ((Context)dht).getRouteTable();
        out.println(CollectionUtils.toString(routeTable.select(nodeId, 20, SelectMode.ALL)));
    }
    
    public static void nextid(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        ((Context)dht).getLocalNode().nextInstanceID();
    }
    
    public static void kill(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        if (dht.isRunning()) {
            out.println("Stopping " + dht.getName());
            dht.stop();
        }
    }
    
    public static void restart(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        if (!dht.isRunning()) {
            out.println("Starting " + dht.getName());
            dht.start();
        }
    }
    
    @SuppressWarnings("unchecked")
    public static void rt_gui(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        Class clazz = Class.forName("org.limewire.mojito.visual.RouteTableVisualizer");
        Method show = clazz.getDeclaredMethod("show", Context.class);
        show.invoke(null, dht);
    }
    
    @SuppressWarnings("unchecked")
    public static void arcs_gui(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
        Class clazz = Class.forName("org.limewire.mojito.visual.ArcsVisualizer");
        Method show = clazz.getDeclaredMethod("show", Context.class);
        show.invoke(null, dht);
    }
    
    public static void bootstrapped(MojitoDHT dht, String[] args, PrintWriter out) throws Exception {
    	Context context = (Context)dht;
    	
    	boolean bootstrapped = context.isBootstrapped();
    	context.setBootstrapped(!bootstrapped);
    	System.out.println(bootstrapped + " -> " + (!bootstrapped));
    }
}
