package net.i2p.client;

/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't  make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.util.Clock;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * ATalk - anonymous talk, demonstrating a trivial I2P usage scenario.
 * Run this class with no arguments for a manual.
 *
 * @author jrandom
 */
public class ATalk implements I2PSessionListener, Runnable {
    /** logging hook - status messages are piped to this */
    private final static Log _log = new Log(ATalk.class);
    
    /** platform independent newline */
    private final static String NL = System.getProperty("line.separator");
    
    /** the current session */
    private I2PSession _session;
    
    /** who am i */
    private Destination _myDestination;
    
    /** who are you? */
    private Destination _peerDestination;
    
    /** location of my secret key file */
    private String _myKeyFile;
    
    /** location of their public key */
    private String _theirDestinationFile;
    
    /** where the application reads input from.  currently set to standard input */
    private BufferedReader _in;
    
    /** where the application sends output to.  currently set to standard output */
    private BufferedWriter _out;
    
    /** string that messages must begin with to be treated as files */
    private final static String FILE_COMMAND = ".file: ";
    
    /** the, erm, manual */
    private final static String MANUAL = "ATalk: Anonymous Talk, a demo program for the Invisible Internet Project SDK"
                                         + NL
                                         + "To generate a new destination:"
                                         + NL
                                         + "\tATalk [fileToSavePrivateKeyIn] [fileToSavePublicKeyIn]"
                                         + NL
                                         + "To talk to another destination:"
                                         + NL
                                         + "\tATalk [myPrivateKeyFile] [peerPublicKey] [shouldLogToScreen]"
                                         + NL
                                         + "shouldLogToScreen is 'true' or 'false', depending on whether you want log info on the screen"
                                         + NL
                                         + "When talking to another destination, messages are sent after you hit return"
                                         + NL
                                         + "To send a file, send a message saying:"
                                         + NL
                                         + "\t"
                                         + FILE_COMMAND
                                         + "[filenameToSend]"
                                         + NL
                                         + "The peer will then recieve the file and be notified of where it has been saved"
                                         + NL
                                         + "To end the talk session, enter a period on a line by itself and hit return"
                                         + NL;
    
    public final static String PROP_CONFIG_LOCATION = "configFile";
    
    private static final SimpleDateFormat _fmt = new SimpleDateFormat("hh:mm:ss.SSS");

    /** Construct the talk engine, but don't connect yet */
    public ATalk(String myKeyFile, String theirDestFile) {
        _myKeyFile = myKeyFile;
        _theirDestinationFile = theirDestFile;
    }

    /** Actually start up the connection to the I2P network.
     * Successful connect does not mean the peer is online or reachable.
     *
     * @throws IOException if there is a problem reading in the keys from the files specified
     * @throws DataFormatException if the key files are not in the valid format
     * @throws I2PSessionException if there is a problem contacting the I2P router
     */
    public void connect() throws IOException, I2PSessionException, DataFormatException {
        I2PClient client = I2PClientFactory.createClient();
        File myFile = new File(_myKeyFile);
        Properties props = new Properties();
        String configLocation = System.getProperty(PROP_CONFIG_LOCATION, "atalk.config");
        try {
            props.load(new FileInputStream(configLocation));
        } catch (FileNotFoundException fnfe) {
            _log.warn("Unable to load up the ATalk config file " + configLocation);
        }
        // Provide any router or client API configuration here.
        if (!props.containsKey(I2PClient.PROP_TCP_HOST))
            props.setProperty(I2PClient.PROP_TCP_HOST, "localhost");
        if (!props.containsKey(I2PClient.PROP_TCP_PORT))
            props.setProperty(I2PClient.PROP_TCP_PORT, "7654");
        if (!props.containsKey(I2PClient.PROP_RELIABILITY))
            props.setProperty(I2PClient.PROP_RELIABILITY, I2PClient.PROP_RELIABILITY_BEST_EFFORT);
        _session = client.createSession(new FileInputStream(myFile), props);
        _session.setSessionListener(this);
        _session.connect();

        File peerDestFile = new File(_theirDestinationFile);
        _peerDestination = new Destination();
        _peerDestination.readBytes(new FileInputStream(peerDestFile));
        return;
    }

    /** Actual bulk processing of the application, reading in user input, 
     * sending messages, and displaying results.  When this function exits, the
     * application is complete.
     *
     */
    public void run() {
        try {
            connect();
            _in = new BufferedReader(new InputStreamReader(System.in));
            _out = new BufferedWriter(new OutputStreamWriter(System.out));

            _out.write("Starting up anonymous talk session" + NL);

            while (true) {
                String line = _in.readLine();
                if ((line == null) || (line.trim().length() <= 0)) continue;
                if (".".equals(line)) {
                    boolean ok = _session.sendMessage(_peerDestination, ("Peer disconnected at " + now()).getBytes());
                    // ignore ok, we're closing
                    break;
                }
                if (line.startsWith(FILE_COMMAND) && (line.trim().length() > FILE_COMMAND.length())) {
                    try {
                        String file = line.substring(FILE_COMMAND.length());
                        boolean sent = sendFile(file);
                        if (!sent) {
                            _out.write("Failed sending the file: " + file + NL);
                        }
                    } catch (IOException ioe) {
                        _out.write("Error sending the file: " + ioe.getMessage() + NL);
                        _log.error("Error sending the file", ioe);
                    }
                } else {
                    boolean ok = _session.sendMessage(_peerDestination, ("[" + now() + "] " + line).getBytes());
                    if (!ok) {
                        _out.write("Failed sending message.  Peer disconnected?" + NL);
                    }
                }
            }
        } catch (IOException ioe) {
            _log.error("Error running", ioe);
        } catch (I2PSessionException ise) {
            _log.error("Error communicating", ise);
        } catch (DataFormatException dfe) {
            _log.error("Peer destination file is not valid", dfe);
        } finally {
            try {
                _log.debug("Exiting anonymous talk session");
                if (_out != null) _out.write("Exiting anonymous talk session");
            } catch (IOException ioe) {
                // ignored
            }
            if (_session != null) {
                try {
                    _session.destroySession();
                } catch (I2PSessionException ise) {
                    // ignored
                }
            }
            try {
                Thread.sleep(5000);
            } catch (InterruptedException ie) { // nop!
            }
        }
    }

    private String now() {
        Date now = new Date(Clock.getInstance().now());
        return _fmt.format(now);
    }

    /** Send the given file to the current peer.  This works by sending a message
     * saying ".file: filename\nbodyOfFile", where filename is the name of the file
     * (which the recipient will be shown), and the bodyOfFile is the set of raw 
     * bytes in the file.
     *
     * @throws IOException if the file could not be found or read
     * @return false if the file could not be sent to the peer
     */
    private boolean sendFile(String filename) throws IOException, I2PSessionException {
        _log.debug("Sending file [" + filename + "]");
        ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
        baos.write((FILE_COMMAND + filename + "\n").getBytes());
        FileInputStream fin = new FileInputStream(filename);
        byte buf[] = new byte[4096];
        try {
            while (true) {
                int len = fin.read(buf);
                if (len == -1) break;
                baos.write(buf, 0, len);
            }
        } catch (IOException ioe) {
            _log.debug("Failed reading the file", ioe);
            return false;
        }
        baos.close();
        byte val[] = baos.toByteArray();
        _log.debug("Sending " + filename + " with a full payload of " + val.length);
        try {
            boolean rv = _session.sendMessage(_peerDestination, val);
            _log.debug("Sending " + filename + " complete: rv = " + rv);
            return rv;
        } catch (Throwable t) {
            _log.error("Error sending file", t);
            return false;
        }
    }

    /** I2PSessionListener.messageAvailable requires this method to be called whenever
     * I2P wants to tell the session that a message is available.  ATalk always grabs
     * the message immediately and either processes it as a "send file" command (passing
     * it off to handleRecieveFile(..) or simply displays the
     * message to the user.
     *
     */
    public void messageAvailable(I2PSession session, int msgId, long size) {
        _log.debug("Message available: id = " + msgId + " size = " + size);
        try {
            byte msg[] = session.receiveMessage(msgId);
            // inefficient way to just read the first line of text, but its easy 
            BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(msg)));
            String line = reader.readLine();
            if (line.startsWith(FILE_COMMAND)) {
                handleRecieveFile(line, msg);
            } else {
                // not a file command, so just plop 'er out on the screen
                _out.write(now() + " --> " + new String(msg));
                _out.write(NL);
                _out.flush();
            }
        } catch (I2PSessionException ise) {
            _log.error("Error fetching available message", ise);
        } catch (IOException ioe) {
            _log.error("Error writing out the message", ioe);
        }
    }

    /** React to a file being sent our way from the peer via {@link #sendFile sendFile} 
     * by saving the file to a temporary location and displaying where, what, and how large 
     * it is.
     *
     * @param firstline the first line of the message that, according to the sendFile 
     *			implementation, contains the command and the filename that it was stored 
     *			at on the peer's computer
     * @param msg the entire message recieved, including the firstline
     */
    private void handleRecieveFile(String firstline, byte msg[]) throws IOException {
        _log.debug("handleRecieveFile called");
        File f = File.createTempFile("recieve", ".dat", new File("."));
        FileOutputStream fos = new FileOutputStream(f);
        int lineLen = firstline.getBytes().length + "\n".getBytes().length;
        int lenToCopy = msg.length - lineLen;
        byte buf[] = new byte[lenToCopy];
        System.arraycopy(msg, lineLen, buf, 0, lenToCopy);
        fos.write(buf);
        fos.close();
        String name = firstline.substring(FILE_COMMAND.length());
        _out.write("Recieved a file called [" + name + "] of size [" + lenToCopy + "] bytes, saved as ["
                   + f.getAbsolutePath() + "]" + NL);
        _out.flush();
    }

    /** driver */
    public static void main(String args[]) {
        I2PAppContext context = new I2PAppContext();
        if (args.length == 2) {
            String myKeyFile = args[0];
            String myDestinationFile = args[1];
            boolean success = generateKeys(myKeyFile, myDestinationFile);
            if (success)
                _log.debug("Keys generated (private key file: " + myKeyFile + " destination file: " + myDestinationFile
                           + ")");
            else
                _log.debug("Keys generation failed");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException ie) { // nop
            }
        } else if (args.length == 3) {
            _log.debug("Starting chat");
            String myKeyfile = args[0];
            String peerDestFile = args[1];
            String shouldLog = args[2];
            if (Boolean.TRUE.toString().equalsIgnoreCase(shouldLog))
                context.logManager().setDisplayOnScreen(true);
            else
                context.logManager().setDisplayOnScreen(false);
            String logFile = args[2];
            Thread talkThread = new I2PThread(new ATalk(myKeyfile, peerDestFile));
            talkThread.start();
        } else {
            System.out.println(MANUAL);
            try {
                Thread.sleep(5000);
            } catch (InterruptedException ie) { // nop
            }
            System.exit(-1);
        }
    }

    /** Generate a new Destination, saving that destination and the associated 
     * private keys in the privKeyFile, and also saving the destination without
     * any private keys to destinationFile.
     *
     * @param privKeyFile private key file, including the destination and the various
     *			    private keys, as defined by {@link I2PClient#createDestination I2PClient.createDestination}
     * @param destinationFile file in which the Destination is serialized in
     */
    private static boolean generateKeys(String privKeyFile, String destinationFile) {
        try {
            Destination d = I2PClientFactory.createClient().createDestination(new FileOutputStream(privKeyFile));
            FileOutputStream fos = new FileOutputStream(destinationFile);
            d.writeBytes(fos);
            fos.flush();
            fos.close();
            return true;
        } catch (IOException ioe) {
            _log.error("Error generating keys", ioe);
        } catch (I2PException ipe) {
            _log.error("Error generating keys", ipe);
        }
        return false;
    }

    /** required by {@link I2PSessionListener I2PSessionListener} to notify of disconnect */
    public void disconnected(I2PSession session) {
        _log.debug("Disconnected");
    }

    /** required by {@link I2PSessionListener I2PSessionListener} to notify of error */
    public void errorOccurred(I2PSession session, String message, Throwable error) {
        _log.debug("Error occurred: " + message, error);
    }

    /** required by {@link I2PSessionListener I2PSessionListener} to notify of abuse */
    public void reportAbuse(I2PSession session, int severity) {
        _log.debug("Abuse reported of severity " + severity);
    }
}

