/* PeerAcceptor - Accepts incomming connections from peers.
   Copyright (C) 2003 Mark J. Wielaard

   This file is part of Snark.
   
   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, 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

package org.klomp.snark;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.Log;

/**
 * Accepts incomming connections from peers. The ConnectionAcceptor
 * will call the connection() method when it detects an incomming BT
 * protocol connection. The PeerAcceptor will then create a new peer
 * if the PeerCoordinator wants more peers.
 */
class PeerAcceptor
{
  private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerAcceptor.class);
  private final PeerCoordinator coordinator;
  final PeerCoordinatorSet coordinators;

  /** shorten timeout while reading handshake */
  private static final long HASH_READ_TIMEOUT = 45*1000;


  public PeerAcceptor(PeerCoordinator coordinator)
  {
    this.coordinator = coordinator;
    this.coordinators = null;
  }
  
  public PeerAcceptor(PeerCoordinatorSet coordinators)
  {
    this.coordinators = coordinators;
    this.coordinator = null;
  }

  public void connection(I2PSocket socket,
                         InputStream in, OutputStream out)
    throws IOException
  {
    // inside this Peer constructor's handshake is where you'd deal with the other
    // side saying they want to communicate with another torrent - aka multitorrent
    // support, but because of how the protocol works, we can get away with just reading
    // ahead the first $LOOKAHEAD_SIZE bytes to figure out which infohash they want to
    // talk about, and we can just look for that in our list of active torrents.
    byte peerInfoHash[] = null;
    if (in instanceof BufferedInputStream) {
        // multitorrent
        in.mark(LOOKAHEAD_SIZE);
        long timeout = socket.getReadTimeout();
        socket.setReadTimeout(HASH_READ_TIMEOUT);
        try {
            peerInfoHash = readHash(in);
        } catch (IOException ioe) {
            // unique exception so ConnectionAcceptor can blame the peer
            throw new ProtocolException(ioe.toString());
        }
        socket.setReadTimeout(timeout);
        in.reset();
    } else {
        // Single torrent - is this working right?
        try {
          peerInfoHash = readHash(in);
          if (_log.shouldLog(Log.INFO))
              _log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64() 
                        + ": " + Base64.encode(peerInfoHash));
        } catch (IOException ioe) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
            throw ioe;
        }
        in = new SequenceInputStream(new ByteArrayInputStream(peerInfoHash), in);
    }
    if (coordinator != null) {
        // single torrent capability
        if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) {
            if (coordinator.needPeers())
              {
                Peer peer = new Peer(socket, in, out, coordinator.getID(),
                                     coordinator.getInfoHash(), coordinator.getMetaInfo());
                coordinator.addPeer(peer);
              }
            else
              socket.close();
        } else {
          // its for another infohash, but we are only single torrent capable.  b0rk.
            throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) 
                                  + ") while we only support (" + Base64.encode(coordinator.getInfoHash()) + ")");
        }
    } else {
        // multitorrent capable, so lets see what we can handle
        PeerCoordinator cur = coordinators.get(peerInfoHash);
        if (cur != null) {
            if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) {
                if (cur.needPeers())
                  {
                    Peer peer = new Peer(socket, in, out, cur.getID(),
                                         cur.getInfoHash(), cur.getMetaInfo());
                    cur.addPeer(peer);
                    return;
                  }
                else 
                  {
                    if (_log.shouldLog(Log.DEBUG))
                      _log.debug("Rejecting new peer for " + cur.getName());
                    socket.close();
                    return;
                  }
            }
        }
        // this is only reached if none of the coordinators match the infohash
        throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) 
                              + ") while we don't support that hash");
    }
  }

  private static final String PROTO_STR = "BitTorrent protocol";
  private static final int PROTO_STR_LEN = PROTO_STR.length();
  private static final int PROTO_LEN = PROTO_STR_LEN + 1;
  private static final int[] PROTO = new int[PROTO_LEN];
  static {
      PROTO[0] = PROTO_STR_LEN;
      for (int i = 0; i < PROTO_STR_LEN; i++) {
          PROTO[i+1] = PROTO_STR.charAt(i);
      }
  }

  /** 48 */
  private static final int LOOKAHEAD_SIZE = PROTO_LEN +
                                            8 + // blank, reserved
                                            20; // infohash

  /** 
   * Read ahead to the infohash, throwing an exception if there isn't enough data.
   * Also check the first 20 bytes for the correct protocol here and throw IOE if bad,
   * so we don't hang waiting for 48 bytes if it's not a bittorrent client.
   * The 20 bytes are checked again in Peer.handshake().
   */
  private static byte[] readHash(InputStream in) throws IOException {
    for (int i = 0; i < PROTO_LEN; i++) {
        int b = in.read();
        if (b != PROTO[i])
            throw new IOException("Bad protocol 0x" + Integer.toHexString(b) + " at byte " + i);
    }
    DataHelper.skip(in, 8);
    byte buf[] = new byte[20];
    int read = DataHelper.read(in, buf);
    if (read != buf.length)
        throw new IOException("Unable to read the hash (read " + read + ")");
    return buf;
  }

    /** 
     *  A unique exception so we can tell the ConnectionAcceptor about non-BT connections
     *  @since 0.9.1
     */
    public static class ProtocolException extends IOException {
        public ProtocolException(String s) {
            super(s);
        }
    }
}
