package net.i2p.router.transport.ntcp;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import net.i2p.router.RouterContext;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * Pool of running threads which will transform the next I2NP message into
 * something ready to be transferred over an NTCP connection, including the
 * encryption of the data read.
 *
 */
class Writer {
    private final Log _log;
    private final Set<NTCPConnection> _pendingConnections;
    private final Set<NTCPConnection> _liveWrites;
    private final Set<NTCPConnection> _writeAfterLive;
    private final List<Runner> _runners;
    
    public Writer(RouterContext ctx) {
        _log = ctx.logManager().getLog(getClass());
        _pendingConnections = new LinkedHashSet<NTCPConnection>(16);
        _runners = new ArrayList<Runner>(5);
        _liveWrites = new HashSet<NTCPConnection>(5);
        _writeAfterLive = new HashSet<NTCPConnection>(5);
    }
    
    public synchronized void startWriting(int numWriters) {
        for (int i = 1; i <=numWriters; i++) {
            Runner r = new Runner();
            I2PThread t = new I2PThread(r, "NTCP writer " + i + '/' + numWriters, true);
            _runners.add(r);
            t.start();
        }
    }
    public synchronized void stopWriting() {
        while (!_runners.isEmpty()) {
            Runner r = _runners.remove(0);
            r.stop();
        }
        synchronized (_pendingConnections) {
            _writeAfterLive.clear();
            _pendingConnections.notifyAll();
        }
    }
    
    public void wantsWrite(NTCPConnection con, String source) {
        //if (con.getCurrentOutbound() != null)
        //    throw new RuntimeException("Current outbound message already in play on " + con);
        boolean already = false;
        boolean pending = false;
        synchronized (_pendingConnections) {
            if (_liveWrites.contains(con)) {
                _writeAfterLive.add(con);
                already = true;
            } else {
                pending = _pendingConnections.add(con);
                // only notify here if added?
            }
            _pendingConnections.notify();
        }
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("wantsWrite: " + con + " already live? " + already + " added to pending? " + pending + ": " + source);
    }

    public void connectionClosed(NTCPConnection con) {
        synchronized (_pendingConnections) {
            _writeAfterLive.remove(con);
            _pendingConnections.remove(con);
            // necessary?
            _pendingConnections.notify();
        }
    }
    
    private class Runner implements Runnable {
        
        /** a scratch space to serialize and encrypt messages */
        private final NTCPConnection.PrepBuffer _prepBuffer;
        
        private volatile boolean _stop;

        public Runner() {
            _prepBuffer = new NTCPConnection.PrepBuffer();
        }

        public void stop() { _stop = true; }

        public void run() {
            if (_log.shouldLog(Log.INFO)) _log.info("Starting writer");
            NTCPConnection con = null;
            while (!_stop) {
                try {
                    synchronized (_pendingConnections) {
                        boolean keepWriting = (con != null) && _writeAfterLive.remove(con);
                        if (keepWriting) {
                            // keep on writing the same one
                            if (_log.shouldLog(Log.DEBUG))
                                _log.debug("Keep writing on the same connection: " + con);
                        } else {
                            _liveWrites.remove(con);
                            con = null;
                            if (_pendingConnections.isEmpty()) {
                                if (_log.shouldLog(Log.DEBUG))
                                    _log.debug("Done writing, but nothing pending, so wait");
                                _pendingConnections.wait();
                            } else {
                                Iterator<NTCPConnection> iter = _pendingConnections.iterator();
                                con = iter.next();
                                iter.remove();
                                _liveWrites.add(con);
                                if (_log.shouldLog(Log.DEBUG))
                                    _log.debug("Switch to writing on: " + con);
                            }
                        }
                    }
                } catch (InterruptedException ie) {}
                if (!_stop && (con != null)) {
                    try {
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Prepare next write on: " + con);
                        _prepBuffer.init();
                        con.prepareNextWrite(_prepBuffer);
                    } catch (RuntimeException re) {
                        _log.log(Log.CRIT, "Error in the ntcp writer on " + con, re);
                    }
                }
            }
            if (_log.shouldLog(Log.INFO)) _log.info("Stopping writer");
        }
    }
}
