package net.i2p.client.streaming.impl;

import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;

import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.I2PSessionMuxedListener;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketManager.DisconnectListener;
import net.i2p.util.Log;

/**
 * Receive raw information from the I2PSession and turn it into
 * Packets, if we can.
 *<p>
 * I2PSession -&gt; MessageHandler -&gt; PacketHandler -&gt; ConnectionPacketHandler -&gt; MessageInputStream
 */
class MessageHandler implements I2PSessionMuxedListener {
    private final ConnectionManager _manager;
    private final I2PAppContext _context;
    private final Log _log;
    private final Set<I2PSocketManager.DisconnectListener> _listeners;
    private final AtomicBoolean _restartPending = new AtomicBoolean();
    
    public MessageHandler(I2PAppContext ctx, ConnectionManager mgr) {
        _manager = mgr;
        _context = ctx;
        _listeners = new CopyOnWriteArraySet<DisconnectListener>();
        _log = ctx.logManager().getLog(MessageHandler.class);
        _context.statManager().createRateStat("stream.packetReceiveFailure", "When do we fail to decrypt or otherwise receive a packet sent to us?", "Stream", new long[] { 60*60*1000, 24*60*60*1000 });
    }
        
    /** Instruct the client that the given session has received a message with
     * size # of bytes.
     * This shouldn't be called anymore since we are registering as a muxed listener.
     * @param session session to notify
     * @param msgId message number available
     * @param size size of the message
     */
    public void messageAvailable(I2PSession session, int msgId, long size) {
        messageAvailable(session, msgId, size, I2PSession.PROTO_UNSPECIFIED,
                         I2PSession.PORT_UNSPECIFIED, I2PSession.PORT_UNSPECIFIED);
    }

    /** Instruct the client that the given session has received a message with
     * size # of bytes.
     * @param session session to notify
     * @param msgId message number available
     * @param size size of the message
     */
    public void messageAvailable(I2PSession session, int msgId, long size, int proto, int fromPort, int toPort) {
        byte data[];
        try {
            data = session.receiveMessage(msgId);
        } catch (I2PSessionException ise) {
            _context.statManager().addRateData("stream.packetReceiveFailure", 1);
            if (_log.shouldLog(Log.WARN))
                _log.warn("Error receiving the message", ise);
            return;
        }
        if (data == null) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Received null data on " + session + " proto: " + proto +
                          " fromPort: " + fromPort + " toPort: " + toPort);
            return;
        }
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Received " + data.length + " bytes on " + session +
                       " (" + _manager + ')' +
                       " proto: " + proto +
                       " fromPort: " + fromPort + " toPort: " + toPort);
        Packet packet = new Packet(session);
        try {
            packet.readPacket(data, 0, data.length);
            packet.setRemotePort(fromPort);
            packet.setLocalPort(toPort);
            _manager.getPacketHandler().receivePacket(packet);
        } catch (IndexOutOfBoundsException ioobe) {
            _context.statManager().addRateData("stream.packetReceiveFailure", 1);
            if (_log.shouldWarn())
                _log.warn("Received an invalid packet", ioobe);
        } catch (IllegalArgumentException iae) {
            _context.statManager().addRateData("stream.packetReceiveFailure", 1);
            if (_log.shouldLog(Log.WARN))
                _log.warn("Received an invalid packet", iae);
        }
    }

    /** Instruct the client that the session specified seems to be under attack
     * and that the client may wish to move its destination to another router.
     * @param session session to report abuse to
     * @param severity how bad the abuse is
     */
    public void reportAbuse(I2PSession session, int severity) {
        if (_log.shouldLog(Log.ERROR))
            _log.error("Abuse reported with severity " + severity);
        _manager.disconnectAllHard();
    }

    /**
     * Notify the client that the session has been terminated
     *
     * As of 0.9.54, this does not clear the listeners, so
     * they will be notified again after a subsequent connection and disconnection.
     *
     * @param session that has been terminated
     */
    public void disconnected(I2PSession session) {
        if (_log.shouldLog(Log.WARN))
            _log.warn("I2PSession disconnected");
        _manager.disconnectAllHard();
        // kill anybody waiting in accept()
        if (_restartPending.compareAndSet(true, false)) {
            _manager.getConnectionHandler().setRestartPending();
        } else {
            _manager.getConnectionHandler().setActive(false);
        }
        
        for (I2PSocketManager.DisconnectListener lsnr : _listeners) {
            lsnr.sessionDisconnected();
        }
    }

    /**
     * Notify the client that some error occurred
     *
     * @param session of the client
     * @param message to send to the client about the error
     * @param error the actual error
     */
    public void errorOccurred(I2PSession session, String message, Throwable error) {
        _restartPending.set(message.contains("restart"));
        if (_log.shouldLog(Log.WARN))
            _log.warn("error occurred: " + message, error); 
        //_manager.disconnectAllHard();
    }
    
    public void addDisconnectListener(I2PSocketManager.DisconnectListener lsnr) { 
            _listeners.add(lsnr);
    }
    public void removeDisconnectListener(I2PSocketManager.DisconnectListener lsnr) {
            _listeners.remove(lsnr);
    }
}
