/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java)
 * (c) 2003 - 2004 mihi
 */
package net.i2p.i2ptunnel;

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import net.i2p.I2PException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketAddress;
import net.i2p.data.Destination;
import net.i2p.util.EventDispatcher;
import net.i2p.util.Log;
import net.i2p.util.PortMapper;

public class I2PTunnelClient extends I2PTunnelClientBase {

    /**
     * list of Destination objects that we point at
     * @deprecated why protected? Is anybody using out-of-tree? Protected from the beginning (2004)
     */
    @Deprecated
    protected List<Destination> dests;

    /**
     * replacement for dests
     */
    private final List<I2PSocketAddress> _addrs;
    private static final long DEFAULT_READ_TIMEOUT = 5*60*1000; // -1
    protected long readTimeout = DEFAULT_READ_TIMEOUT;

    /**
     * As of 0.9.20 this is fast, and does NOT connect the manager to the router,
     * or open the local socket. You MUST call startRunning() for that.
     *
     * @param destinations peers we target, comma- or space-separated. Since 0.9.9, each dest may be appended with :port
     * @throws IllegalArgumentException if the I2PTunnel does not contain
     *                                  valid config to contact the router
     */
    public I2PTunnelClient(int localPort, String destinations, Logging l, 
                           boolean ownDest, EventDispatcher notifyThis, 
                           I2PTunnel tunnel, String pkf) throws IllegalArgumentException {
        super(localPort, ownDest, l, notifyThis,
              "Standard client on " + tunnel.listenHost + ':' + localPort,
              tunnel, pkf);

        _addrs = new ArrayList<I2PSocketAddress>(1);
        dests = new ArrayList<Destination>(1);
        buildAddresses(destinations);

        if (_addrs.isEmpty()) {
            l.log("No valid target destinations found");
            notifyEvent("openClientResult", "error");
            // Nothing is listening for the above event, so it's useless
            // Maybe figure out where to put a waitEventValue("openClientResult") ??
            // In the meantime, let's do this the easy way

            // Don't close() here, because it does a removeSession() and then
            // TunnelController can't acquire() it to release() it.
            //close(true);
            // Unfortunately, super() built the whole tunnel before we get here.
            throw new IllegalArgumentException("No valid target destinations found");
            //return;
        }

        setName(getLocalPort() + " -> " + destinations);
        notifyEvent("openClientResult", "ok");
    }

    /** @since 0.9.9 moved from constructor */
    private void buildAddresses(String destinations) {
        if (destinations == null)
            return;
        StringTokenizer tok = new StringTokenizer(destinations, ", ");
        synchronized(_addrs) {
            _addrs.clear();
            while (tok.hasMoreTokens()) {
                String destination = tok.nextToken();
                try {
                    // Try to resolve here but only log if it doesn't.
                    // Note that b32 _addrs will often not be resolvable at instantiation time.
                    // We will try again to resolve in clientConnectionRun()
                    I2PSocketAddress addr = new I2PSocketAddress(destination);
                    _addrs.add(addr);
                    if (addr.isUnresolved()) {
                        String name = addr.getHostName();
                        if (name.length() == 60 && name.endsWith(".b32.i2p"))
                            l.log("Warning - Could not resolve " + name +
                                  ", perhaps it is not up, will retry when connecting.");
                        else
                            l.log("Warning - Could not resolve " + name +
                                  ", you must add it to your address book for it to work.");
                    } else {
                        dests.add(addr.getAddress());
                    }
                } catch (IllegalArgumentException iae) {
                     l.log("Bad destination " + destination + " - " + iae);
                }
            }
        }
    }

    public void setReadTimeout(long ms) { readTimeout = ms; }
    public long getReadTimeout() { return readTimeout; }
    
    protected void clientConnectionRun(Socket s) {
        I2PSocket i2ps = null;
        try {
            I2PSocketAddress addr = pickDestination();
            if (addr == null)
                throw new UnknownHostException("No valid destination configured");
            Destination clientDest = addr.getAddress();
            if (clientDest == null)
                throw new UnknownHostException("Could not resolve " + addr.getHostName());
            int port = addr.getPort();
            i2ps = createI2PSocket(clientDest, port);
            i2ps.setReadTimeout(readTimeout);
            I2PTunnelRunner t = new I2PTunnelRunner(s, i2ps, sockLock, null, null, mySockets,
                                (I2PTunnelRunner.FailCallback) null);
            // we are called from an unlimited thread pool, so run inline
            //t.start();
            t.run();
        } catch (IOException ex) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Error connecting", ex);
            //l.log("Error connecting: " + ex.getMessage());
            closeSocket(s);
            if (i2ps != null) {
                synchronized (sockLock) {
                    mySockets.remove(sockLock);
                }
            }
        } catch (I2PException ex) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Error connecting", ex);
            //l.log("Error connecting: " + ex.getMessage());
            closeSocket(s);
            if (i2ps != null) {
                synchronized (sockLock) {
                    mySockets.remove(sockLock);
                }
            }
        }
    }
    
    private final I2PSocketAddress pickDestination() {
        synchronized(_addrs) {
            int size = _addrs.size();
            if (size <= 0) {
                if (_log.shouldLog(Log.ERROR))
                    _log.error("No client targets?!");
                return null;
            }
            if (size == 1) // skip the rand in the most common case
                return _addrs.get(0);
            int index = _context.random().nextInt(size);
            return _addrs.get(index);
        }
    }

    /**
     *  Update the dests then call super.
     *
     *  @since 0.9.9
     */
    @Override
    public void optionsUpdated(I2PTunnel tunnel) {
        if (getTunnel() != tunnel)
            return;
        Properties props = tunnel.getClientOptions();
        // see TunnelController.setSessionOptions()
        String targets = props.getProperty("targetDestination");
        buildAddresses(targets);
        super.optionsUpdated(tunnel);
    }

    /**
     * Actually start working on incoming connections.
     * Overridden to register with port mapper.
     *
     * @since 0.9.27
     */
    @Override
    public void startRunning() {
        super.startRunning();
        if (open) {
            I2PSocketAddress addr = pickDestination();
            if (addr != null) {
                String svc = null;
                String hostname = addr.getHostName();
                if ("smtp.postman.i2p".equals(hostname)) {
                    svc = PortMapper.SVC_SMTP;
                } else if ("pop.postman.i2p".equals(hostname)) {
                    svc = PortMapper.SVC_POP;
                }
                if (svc != null) {
                    _context.portMapper().register(svc, getTunnel().listenHost, getLocalPort());
                }
            }
        }
    }

    /**
     * Overridden to unregister with port mapper
     *
     * @since 0.9.27
     */
    @Override
    public boolean close(boolean forced) {
        int port = getLocalPort();
        int reg = _context.portMapper().getPort(PortMapper.SVC_SMTP);
        if (reg == port) {
            _context.portMapper().unregister(PortMapper.SVC_SMTP);
        }
        reg = _context.portMapper().getPort(PortMapper.SVC_POP);
        if (reg == port) {
            _context.portMapper().unregister(PortMapper.SVC_POP);
        }
        boolean rv = super.close(forced);
        return rv;
    }
}
