/* Snark - Main snark program startup class.
   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.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger;

import net.i2p.I2PAppContext;
import net.i2p.client.streaming.I2PServerSocket;
import net.i2p.data.Destination;
import net.i2p.util.Log;
import net.i2p.util.SecureFile;

import org.klomp.snark.comments.Comment;
import org.klomp.snark.comments.CommentSet;


/**
 * Main Snark program startup class.
 *
 * @author Mark Wielaard (mark@klomp.org)
 */
public class Snark
  implements StorageListener, CoordinatorListener, ShutdownListener
{
  private final static int MIN_PORT = 6881;
  private final static int MAX_PORT = 6889;

  // Whether or not to ask the user for commands while sharing
  //private static boolean command_interpreter = true;

  private static final String newline = System.getProperty("line.separator");

/****
  private static final String copyright =
  "The Hunting of the Snark Project - Copyright (C) 2003 Mark J. Wielaard"
  + newline + newline
  + "Snark comes with ABSOLUTELY NO WARRANTY.  This is free software, and"
  + newline
  + "you are welcome to redistribute it under certain conditions; read the"
  + newline
  + "COPYING file for details." + newline + newline
  + "This is the I2P port, allowing anonymous bittorrent (http://www.i2p.net/)" + newline
  + "It will not work with normal torrents, so don't even try ;)";
  
  private static final String usage =
  "Press return for help. Type \"quit\" and return to stop.";
  private static final String help =
  "Commands: 'info', 'list', 'quit'.";
****/

  
/****
  private static class OOMListener implements I2PThread.OOMEventListener {
      public void outOfMemory(OutOfMemoryError err) {
          try {
              err.printStackTrace();
              System.out.println("OOM in the snark" + err);
          } catch (Throwable t) {
              System.out.println("OOM in the OOM");
          }
          //System.exit(0);
      }
      
  }
****/
  
/******** No, not maintaining a command-line client

  public static void main(String[] args)
  {
    System.out.println(copyright);
    System.out.println();

    if ( (args.length > 0) && ("--config".equals(args[0])) ) {
        I2PThread.addOOMEventListener(new OOMListener());
        SnarkManager sm = SnarkManager.instance();
        if (args.length > 1)
            sm.loadConfig(args[1]);
        System.out.println("Running in multitorrent mode");
        while (true) {
            try {
                synchronized (sm) {
                    sm.wait();
                }
            } catch (InterruptedException ie) {}
        }
    }
    
    // Parse debug, share/ip and torrent file options.
    Snark snark = parseArguments(args);

    SnarkShutdown snarkhook
      = new SnarkShutdown(snark.storage,
                          snark.coordinator,
                          snark.acceptor,
                          snark.trackerclient,
                          snark);
    //Runtime.getRuntime().addShutdownHook(snarkhook);

    Timer timer = new Timer(true);
    TimerTask monitor = new PeerMonitorTask(snark.coordinator);
    timer.schedule(monitor,
                   PeerMonitorTask.MONITOR_PERIOD,
                   PeerMonitorTask.MONITOR_PERIOD);

    // Start command interpreter
    if (Snark.command_interpreter)
      {
        boolean quit = false;
        
        System.out.println();
        System.out.println(usage);
        System.out.println();
        
        try
          {
            BufferedReader br = new BufferedReader
              (new InputStreamReader(System.in));
            String line = br.readLine();
            while(!quit && line != null)
              {
                line = line.toLowerCase();
                if ("quit".equals(line))
                  quit = true;
                else if ("list".equals(line))
                  {
                    synchronized(snark.coordinator.peers)
                      {
                        System.out.println(snark.coordinator.peers.size()
                                           + " peers -"
                                           + " (i)nterested,"
                                           + " (I)nteresting,"
                                           + " (c)hoking,"
                                           + " (C)hoked:");
                        Iterator it = snark.coordinator.peers.iterator();
                        while (it.hasNext())
                          {
                            Peer peer = (Peer)it.next();
                            System.out.println(peer);
                            System.out.println("\ti: " + peer.isInterested()
                                               + " I: " + peer.isInteresting()
                                               + " c: " + peer.isChoking()
                                               + " C: " + peer.isChoked());
                          }
                      }
                  }
                else if ("info".equals(line))
                  {
                    System.out.println("Name: " + snark.meta.getName());
                    System.out.println("Torrent: " + snark.torrent);
                    System.out.println("Tracker: " + snark.meta.getAnnounce());
                    List files = snark.meta.getFiles();
                    System.out.println("Files: "
                                       + ((files == null) ? 1 : files.size()));
                    System.out.println("Pieces: " + snark.meta.getPieces());
                    System.out.println("Piece size: "
                                       + snark.meta.getPieceLength(0) / 1024
                                       + " KB");
                    System.out.println("Total size: "
                                       + snark.meta.getTotalLength() / (1024 * 1024)
                                       + " MB");
                  }
                else if ("".equals(line) || "help".equals(line))
                  {
                    System.out.println(usage);
                    System.out.println(help);
                  }
                else
                  {
                    System.out.println("Unknown command: " + line);
                    System.out.println(usage);
                  }
                
                if (!quit)
                  {
                    System.out.println();
                    line = br.readLine();
                  }
              }
          }
        catch(IOException ioe)
          {
            System.out.println("ERROR while reading stdin: " + ioe);
          }
        
        // Explicit shutdown.
        //Runtime.getRuntime().removeShutdownHook(snarkhook);
        snarkhook.start();
      }
  }

***********/

  /** max connections */
  public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections";

  /** most of these used to be public, use accessors below instead */
  private String torrent;
  private MetaInfo meta;
  private Storage storage;
  private PeerCoordinator coordinator;
  private ConnectionAcceptor acceptor;
  private TrackerClient trackerclient;
  private final File rootDataDir;
  private final CompleteListener completeListener;
  private volatile boolean stopped;
  private volatile boolean starting;
  private final byte[] id;
  private final byte[] infoHash;
  private String additionalTrackerURL;
  protected final I2PSnarkUtil _util;
  private final Log _log;
  private final PeerCoordinatorSet _peerCoordinatorSet;
  private volatile String trackerProblems;
  private volatile int trackerSeenPeers;
  private volatile boolean _autoStoppable;
  // String indicating main activity
  private volatile String activity = "Not started";
  private long savedUploaded;
  private long _startedTime;
  private CommentSet _comments;
  private final Object _commentLock = new Object();
  private static final AtomicInteger __RPCID = new AtomicInteger();
  private final int _rpcID = __RPCID.incrementAndGet();

  /**
   * from main() via parseArguments() single torrent
   *
   * @deprecated unused
   */
/****
  Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
        StorageListener slistener, CoordinatorListener clistener) { 
    this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); 
  }
****/

  /**
   * single torrent - via router
   *
   * @deprecated unused
   */
/****
  public Snark(I2PAppContext ctx, Properties opts, String torrent,
               StorageListener slistener, boolean start, String rootDir) { 
    this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir);
    String host = opts.getProperty("i2cp.hostname");
    int port = 0;
    String s = opts.getProperty("i2cp.port");
    if (s != null) {
        try {
             port = Integer.parseInt(s);
        } catch (NumberFormatException nfe) {}
    }
    _util.setI2CPConfig(host, port, opts);
    s = opts.getProperty(SnarkManager.PROP_UPBW_MAX);
    if (s != null) {
        try {
             int v = Integer.parseInt(s);
             _util.setMaxUpBW(v);
        } catch (NumberFormatException nfe) {}
    }
    s = opts.getProperty(PROP_MAX_CONNECTIONS);
    if (s != null) {
        try {
             int v = Integer.parseInt(s);
             _util.setMaxConnections(v);
        } catch (NumberFormatException nfe) {}
    }
    if (start)
        this.startTorrent();
  }
****/

  /**
   * multitorrent
   *
   * Will not start itself. Caller must call startTorrent() if desired.
   *
   * @throws RuntimeException via fatal()
   * @throws RouterException via fatalRouter()
   */
  public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
        StorageListener slistener, CoordinatorListener clistener,
        CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
        ConnectionAcceptor connectionAcceptor, String rootDir)
  {
      this(util, torrent, ip, user_port, slistener, clistener, complistener,
           peerCoordinatorSet, connectionAcceptor, rootDir, null);
  }

  /**
   * multitorrent
   *
   * Will not start itself. Caller must call startTorrent() if desired.
   *
   * @param baseFile if null, use rootDir/torrentName; if non-null, use it instead
   * @throws RuntimeException via fatal()
   * @throws RouterException via fatalRouter()
   * @since 0.9.11
   */
  public Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
        StorageListener slistener, CoordinatorListener clistener,
        CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
        ConnectionAcceptor connectionAcceptor, String rootDir, File baseFile)
  {
    if (slistener == null)
      slistener = this;

    completeListener = complistener;
    _util = util;
    _log = util.getContext().logManager().getLog(Snark.class);
    _peerCoordinatorSet = peerCoordinatorSet;
    acceptor = connectionAcceptor;

    this.torrent = torrent;
    this.rootDataDir = new File(rootDir);

    stopped = true;
    activity = "Network setup";

    id = generateID();
    if (_log.shouldLog(Log.INFO))
        _log.info("My peer id: " + PeerID.idencode(id));

/*
 * Don't start a tunnel if the torrent isn't going to be started.
 * If we are starting,
 * startTorrent() will force a connect.
 *
    boolean ok = util.connect();
    if (!ok) fatal("Unable to connect to I2P");
    I2PServerSocket serversocket = util.getServerSocket();
    if (serversocket == null)
        fatal("Unable to listen for I2P connections");
    else {
        Destination d = serversocket.getManager().getSession().getMyDestination();
        debug("Listening on I2P destination " + d.toBase64() + " / " + d.calculateHash().toBase64(), NOTICE);
    }
*/

    // Figure out what the torrent argument represents.
    File f = null;
    InputStream in = null;
    byte[] x_infoHash = null;
    try
      {
        f = new File(torrent);
        if (f.exists())
          in = new FileInputStream(f);
        else
          {
         /**** No, we don't ever fetch a torrent file this way
               and we don't want to block in the constructor
            activity = "Getting torrent";
            File torrentFile = _util.get(torrent, 3);
            if (torrentFile == null) {
                fatal("Unable to fetch " + torrent);
                if (false) return; // never reached - fatal(..) throws
            } else {
                torrentFile.deleteOnExit();
                in = new FileInputStream(torrentFile);
            }
         *****/
             throw new IOException("not found");
          }
        meta = new MetaInfo(in);
        x_infoHash = meta.getInfoHash();
      }
    catch(IOException ioe)
      {
        // OK, so it wasn't a torrent metainfo file.
        if (f != null && f.exists())
          if (ip == null)
            fatal("'" + torrent + "' exists,"
                  + " but is not a valid torrent metainfo file."
                  + System.getProperty("line.separator"), ioe);
          else
            fatal("I2PSnark does not support creating and tracking a torrent at the moment");
        /*
            {
              // Try to create a new metainfo file
             debug
               ("Trying to create metainfo torrent for '" + torrent + "'",
                NOTICE);
             try
               {
                 activity = "Creating torrent";
                 storage = new Storage
                   (f, "http://" + ip + ":" + port + "/announce", slistener);
                 storage.create();
                 meta = storage.getMetaInfo();
               }
             catch (IOException ioe2)
               {
                 fatal("Could not create torrent for '" + torrent + "'", ioe2);
               }
            }
         */
        else
          fatal("Cannot open '" + torrent + "'", ioe);
      } catch (OutOfMemoryError oom) {
          fatalRouter("ERROR - Out of memory, cannot create torrent " + torrent + ": " + oom.getMessage(), oom);
      } finally {
          if (in != null)
              try { in.close(); } catch (IOException ioe) {}
      }    

    infoHash = x_infoHash;  // final
    if (_log.shouldLog(Log.INFO))
        _log.info(meta.toString());
    
    // When the metainfo torrent was created from an existing file/dir
    // it already exists.
    if (storage == null)
      {
        try
          {
            activity = "Checking storage";
            boolean shouldPreserve = completeListener != null && completeListener.getSavedPreserveNamesSetting(this);
            if (baseFile == null) {
                String base = meta.getName();
                if (!shouldPreserve)
                    base = Storage.filterName(base);
                if (_util.getFilesPublic())
                    baseFile = new File(rootDataDir, base);
                else
                    baseFile = new SecureFile(rootDataDir, base);
            }
            storage = new Storage(_util, baseFile, meta, slistener, shouldPreserve);
            if (completeListener != null) {
                storage.check(completeListener.getSavedTorrentTime(this),
                              completeListener.getSavedTorrentBitField(this));
            } else {
                storage.check();
            }
            // have to figure out when to reopen
            // if (!start)
            //    storage.close();
          }
        catch (IOException ioe)
          {
            try { storage.close(); } catch (IOException ioee) {
                ioee.printStackTrace();
            }
            fatal("Could not check or create files for " + getBaseInfo(), ioe);
          }
      }


/*
 * see comment above
 *
    activity = "Collecting pieces";
    coordinator = new PeerCoordinator(id, meta, storage, clistener, this);
    PeerCoordinatorSet set = PeerCoordinatorSet.instance();
    set.add(coordinator);
    ConnectionAcceptor acceptor = ConnectionAcceptor.instance();
    acceptor.startAccepting(set, serversocket);
    trackerclient = new TrackerClient(meta, coordinator);
*/
    
    savedUploaded = (completeListener != null) ? completeListener.getSavedUploaded(this) : 0;
    if (completeListener != null)
        _comments = completeListener.getSavedComments(this);
  }

  /**
   *  multitorrent, magnet, Used by snark-rpc plugin
   *
   *  Will not start itself. Caller must call startTorrent() if desired.
   *
   *  @param ignored used to be autostart
   *  @throws RuntimeException via fatal()
   *  @throws RouterException via fatalRouter()
   *  @since 0.8.4, removed in 0.9.36, restored in 0.9.45 with boolean param now ignored
   */
  protected Snark(I2PSnarkUtil util, String torrent, byte[] ih, String trackerURL,
        CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
        ConnectionAcceptor connectionAcceptor, boolean ignored, String rootDir) {
      this(util, torrent, ih, trackerURL, complistener, peerCoordinatorSet, connectionAcceptor, rootDir);
  }

  /**
   *  multitorrent, magnet
   *
   *  Will not start itself. Caller must call startTorrent() if desired.
   *
   *  @param torrent a fake name for now (not a file name)
   *  @param ih 20-byte info hash
   *  @param trackerURL may be null
   *  @throws RuntimeException via fatal()
   *  @throws RouterException via fatalRouter()
   *  @since 0.8.4
   */
  public Snark(I2PSnarkUtil util, String torrent, byte[] ih, String trackerURL,
        CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet,
        ConnectionAcceptor connectionAcceptor, String rootDir)
  {
    completeListener = complistener;
    _util = util;
    _log = util.getContext().logManager().getLog(Snark.class);
    _peerCoordinatorSet = peerCoordinatorSet;
    acceptor = connectionAcceptor;
    this.torrent = torrent;
    this.infoHash = ih;
    this.additionalTrackerURL = trackerURL;
    this.rootDataDir = rootDir != null ? new File(rootDir) : null;   // null only for FetchAndAdd extension
    savedUploaded = 0;
    stopped = true;
    id = generateID();

    // All we have is an infoHash
    // meta remains null
    // storage remains null
  }

  private static byte[] generateID() {
    // "Taking Three as the subject to reason about--
    // A convenient number to state--
    // We add Seven, and Ten, and then multiply out
    // By One Thousand diminished by Eight.
    //
    // "The result we proceed to divide, as you see,
    // By Nine Hundred and Ninety Two:
    // Then subtract Seventeen, and the answer must be
    // Exactly and perfectly true.

    // Create a new ID and fill it with something random.  First nine
    // zeros bytes, then three bytes filled with snark and then
    // eight random bytes.
    byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17;
    byte[] rv = new byte[20];
    rv[9] = snark;
    rv[10] = snark;
    rv[11] = snark;
    try {
        I2PAppContext.getGlobalContext().random().nextBytes(rv, 12, 8);
    } catch (IllegalStateException ise) {
        // random is shut down
        throw new RouterException("Router shutdown", ise);
    }
    return rv;
  }

  /**
   * Start up contacting peers and querying the tracker.
   * Blocks if tunnel is not yet open.
   *
   * @throws RuntimeException via fatal()
   * @throws RouterException via fatalRouter()
   */
  public synchronized void startTorrent() {
      if (!stopped)
          return;
      starting = true;
      try {
          x_startTorrent();
          _startedTime = _util.getContext().clock().now();
      } finally {
          starting = false;
      }
  }

  private void x_startTorrent() {
    boolean ok = _util.connect();
    if (!ok)
        fatalRouter("Unable to connect to I2P", null);
    if (coordinator == null) {
        I2PServerSocket serversocket = _util.getServerSocket();
        if (serversocket == null)
            fatalRouter("Unable to listen for I2P connections", null);
        else {
            Destination d = serversocket.getManager().getSession().getMyDestination();
            if (_log.shouldLog(Log.INFO))
                _log.info("Listening on I2P destination " + d.toBase64() + " / " + d.calculateHash().toBase64());
        }
        if (_log.shouldLog(Log.INFO))
            _log.info("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient");
        activity = "Collecting pieces";
        coordinator = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
        coordinator.setUploaded(savedUploaded);
        if (_peerCoordinatorSet != null) {
            // multitorrent
            _peerCoordinatorSet.add(coordinator);
        } else {
            // single torrent
            acceptor = new ConnectionAcceptor(_util, new PeerAcceptor(coordinator));
        }
        // TODO pass saved closest DHT nodes to the tracker? or direct to the coordinator?
        trackerclient = new TrackerClient(_util, meta, additionalTrackerURL, coordinator, this);
    }
    // ensure acceptor is running when in multitorrent
    if (_peerCoordinatorSet != null && acceptor != null) {
        acceptor.startAccepting();
    }

    stopped = false;
    if (coordinator.halted()) {
        coordinator.restart();
        if (_peerCoordinatorSet != null)
            _peerCoordinatorSet.add(coordinator);
    }
    if (!trackerclient.started()) {
        trackerclient.start();
    } else if (trackerclient.halted()) {
        if (storage != null) {
            try {
                 storage.reopen();
             }   catch (IOException ioe) {
                 try { storage.close(); } catch (IOException ioee) {
                     ioee.printStackTrace();
                 }
                 fatal("Could not open file for " + getBaseInfo(), ioe);
             }
        }
        trackerclient.start();
    } else {
        if (_log.shouldLog(Log.INFO))
            _log.info("NOT starting TrackerClient???");
    }
  }

  /**
   * Stop contacting the tracker and talking with peers
   */
  public void stopTorrent() {
      stopTorrent(false);
  }

  /**
   * Stop contacting the tracker and talking with peers
   * @param fast if true, limit the life of the unannounce threads
   * @since 0.9.1
   */
  public synchronized void stopTorrent(boolean fast) {
    TrackerClient tc = trackerclient;
    if (tc != null)
        tc.halt(fast);
    PeerCoordinator pc = coordinator;
    if (pc != null)
        pc.halt();
    Storage st = storage;
    if (!fast)
        // HACK: Needed a way to distinguish between user-stop and 
        // shutdown-stop. stopTorrent(true) is in stopAllTorrents().
        // (#766)
        stopped = true;
    if (st != null) {
        // TODO: Cache the config-in-mem to compare vs config-on-disk
        // (needed for auto-save to not double-save in some cases)
        long nowUploaded = getUploaded();
        // If autoStart is enabled, always save the config, so we know
        // whether to start it up next time
        boolean changed = storage.isChanged() || nowUploaded != savedUploaded ||
                          (completeListener != null && completeListener.shouldAutoStart());
        try { 
            storage.close(); 
        } catch (IOException ioe) {
            if (_log.shouldWarn())
                _log.warn("Error closing " + torrent);
            ioe.printStackTrace();
        }
        savedUploaded = nowUploaded;
        // SnarkManager.stopAllTorrents() will save comments at shutdown even if never started...
        if (completeListener != null) {
            if (changed)
                completeListener.updateStatus(this);
            synchronized(_commentLock) {
                if (_comments != null) {
                    synchronized(_comments) {
                        if (_comments.isModified())
                            completeListener.locked_saveComments(this, _comments);
                    }
                }
            }
        }
    }
    if (fast)
        // HACK: See above if(!fast)
        stopped = true;
    if (pc != null && _peerCoordinatorSet != null)
        _peerCoordinatorSet.remove(pc);
    if (_peerCoordinatorSet == null)
        _util.disconnect();
  }

/****
  private static Snark parseArguments(String[] args)
  {
    return parseArguments(args, null, null);
  }
****/

    // Accessors

    /**
     *  @return file name of .torrent file (should be full absolute path), or a fake name if in magnet mode.
     *  @since 0.8.4
     */
    public String getName() {
        return torrent;
    }

    /**
     *  @return base name of torrent [filtered version of getMetaInfo.getName()], or a fake name if in magnet mode
     *  @since 0.8.4
     */
    public String getBaseName() {
        if (storage != null)
            return storage.getBaseName();
        return torrent;
    }

    /**
     *  @return base name for torrent [filtered version of getMetaInfo.getName()],
     *          or a fake name if in magnet mode, followed by path info and error message,
     *          for error logging only
     *  @since 0.9.44
     */
    private String getBaseInfo() {
        if (storage != null)
            return storage.getBaseName() + " at " +
                   storage.getBase() + " - check that device is present and writable";
        return torrent;
    }

    /**
     *  @return always will be valid even in magnet mode
     *  @since 0.8.4
     */
    public byte[] getID() {
        return id;
    }

    /**
     *  @return always will be valid even in magnet mode
     *  @since 0.8.4
     */
    public byte[] getInfoHash() {
        // should always be the same
        if (meta != null)
            return meta.getInfoHash();
        return infoHash;
    }

    /**
     *  @return may be null if in magnet mode
     *  @since 0.8.4
     */
    public MetaInfo getMetaInfo() {
        return meta;
    }

    /**
     *  @return may be null if in magnet mode
     *  @since 0.8.4
     */
    public Storage getStorage() {
        return storage;
    }

    /**
     *  @since 0.8.4
     */
    public boolean isStopped() {
        return stopped;
    }

    /**
     *  Startup in progress.
     *  @since 0.9.1
     */
    public boolean isStarting() {
        return starting && stopped;
    }

    /**
     *  Set startup in progress.
     *  @since 0.9.1
     */
    public void setStarting() {
        starting = true;
    }

    /**
     *  File checking in progress.
     *  @since 0.9.3
     */
    public boolean isChecking() {
        return storage != null && storage.isChecking();
    }

    /**
     *  If checking is in progress, return completion 0.0 ... 1.0,
     *  else return 1.0.
     *  @since 0.9.23
     */
    public double getCheckingProgress() {
        if (storage != null && storage.isChecking())
            return storage.getCheckingProgress();
        else
            return 1.0d;
    }

    /**
     *  Disk allocation (ballooning) in progress.
     *  @since 0.9.3
     */
    public boolean isAllocating() {
        return storage != null && storage.isAllocating();
    }

    /**
     *  @since 0.8.4
     */
    public long getDownloadRate() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.getDownloadRate();
        return 0;
    }

    /**
     *  @since 0.8.4
     */
    public long getUploadRate() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.getUploadRate();
        return 0;
    }

    /**
     *  @since 0.8.4
     */
    public long getDownloaded() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.getDownloaded();
        return 0;
    }

    /**
     *  @since 0.8.4
     */
    public long getUploaded() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.getUploaded();
        return savedUploaded;
    }

    /**
     *  @since 0.8.4
     */
    public int getPeerCount() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.getPeerCount();
        return 0;
    }

    /**
     *  @since 0.8.4
     */
    public List<Peer> getPeerList() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.peerList();
        return Collections.emptyList();
    }

    /**
     *  Not HTML escaped.
     *  @return String returned from tracker, or null if no error
     *  @since 0.8.4
     */
    public String getTrackerProblems() {
        return trackerProblems;
    }

    /**
     *  @param p tracker error string or null
     *  @since 0.8.4
     */
    public void setTrackerProblems(String p) {
        trackerProblems = p;
    }

    /**
     *  @return count returned from tracker
     *  @since 0.8.4
     */
    public int getTrackerSeenPeers() {
        return trackerSeenPeers;
    }

    /**
     *  @since 0.8.4
     */
    public void setTrackerSeenPeers(int p) {
        trackerSeenPeers = p;
    }

    /**
     *  @since 0.8.4
     */
    public void updatePiecePriorities() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            coord.updatePiecePriorities();
    }

    /**
     *  @return total of all torrent files, or total of metainfo file if fetching magnet, or -1
     *  @since 0.8.4
     */
    public long getTotalLength() {
        if (meta != null)
            return meta.getTotalLength();
        // FIXME else return metainfo length if available
        return -1;
    }

    /**
     *  Bytes not yet in storage. Does NOT account for skipped files.
     *  @return exact value. or -1 if no storage yet.
     *          getNeeded() * pieceLength(0) isn't accurate if last piece
     *          is still needed.
     *  @since 0.8.9
     */
    public long getRemainingLength() {
        if (meta != null && storage != null) {
            long needed = storage.needed();
            long length0 = meta.getPieceLength(0);
            long remaining = needed * length0;
            // fixup if last piece is needed
            int last = meta.getPieces() - 1;
            if (last != 0 && !storage.getBitField().get(last))
                remaining -= length0 - meta.getPieceLength(last);
            return remaining;
        }
        return -1;
    }

    /**
     *  Bytes still wanted. DOES account for (i.e. does not include) skipped files.
     *  FIXME -1 when not running.
     *  @return exact value. or -1 if no storage yet or when not running.
     *  @since 0.9.1
     */
    public long getNeededLength() {
        PeerCoordinator coord = coordinator;
        if (coord != null)
            return coord.getNeededLength();
        return -1;
    }

    /**
     *  Bytes not received and set to skipped.
     *  This is not the same as the total of all skipped files,
     *  since pieces may span multiple files.
     *
     *  @return exact value. or 0 if no storage yet.
     *  @since 0.9.24
     */
    public long getSkippedLength() {
        PeerCoordinator coord = coordinator;
        if (coord != null) {
            // fast way
            long r = getRemainingLength();
            if (r <= 0)
                return 0;
            long n = coord.getNeededLength();
            return r - n;
        } else if (storage != null) {
            // slow way
            return storage.getSkippedLength();
        }
        return 0;
    }

    /**
     *  Does not account (i.e. includes) for skipped files.
     *  @return number of pieces still needed (magnet mode or not), or -1 if unknown
     *  @since 0.8.4
     */
    public long getNeeded() {
        if (storage != null)
            return storage.needed();
        if (meta != null)
            // FIXME subtract chunks we have
            return meta.getTotalLength();
        // FIXME fake
        return -1;
    }

    /**
     *  @param p the piece number
     *  @return metainfo piece length or 16K if fetching magnet
     *  @since 0.8.4
     */
    public int getPieceLength(int p) {
        if (meta != null)
            return meta.getPieceLength(p);
        return 16*1024;
    }

    /**
     *  @return number of pieces
     *  @since 0.8.4
     */
    public int getPieces() {
        if (meta != null)
            return meta.getPieces();
        // FIXME else return metainfo pieces if available
        return -1;
    }

    /**
     *  @return true if restarted
     *  @since 0.8.4
     */
    public boolean restartAcceptor() {
        if (acceptor == null)
            return false;
        acceptor.restart();
        return true;
    }

    /**
     *  @return trackerURL string from magnet-mode constructor, may be null
     *  @since 0.8.4
     */
    public String getTrackerURL() {
        return additionalTrackerURL;
    }

    /**
     *  @since 0.9.9
     */
    public boolean isAutoStoppable() { return _autoStoppable; }

    /**
     *  @since 0.9.9
     */
    public void setAutoStoppable(boolean yes) { _autoStoppable = yes; }

  /**
   * Sets debug, ip and torrent variables then creates a Snark
   * instance.  Calls usage(), which terminates the program, if
   * non-valid argument list.  The given listeners will be
   * passed to all components that take one.
   */
/****
  private static Snark parseArguments(String[] args,
                              StorageListener slistener,
                              CoordinatorListener clistener)
  {
    int user_port = -1;
    String ip = null;
    String torrent = null;

    I2PSnarkUtil util = new I2PSnarkUtil(I2PAppContext.getGlobalContext());
    boolean configured = util.configured();
    
    int i = 0;
    while (i < args.length)
      {
****/
/*
        if (args[i].equals("--debug"))
          {
            debug = INFO;
            i++;

            // Try if there is an level argument.
            if (i < args.length)
              {
                try
                  {
                    int level = Integer.parseInt(args[i]);
                    if (level >= 0)
                      {
                        debug = level;
                        i++;
                      }
                  }
                catch (NumberFormatException nfe) { }
              }
          }
        else */
/****
          if (args[i].equals("--port"))
          {
            if (args.length - 1 < i + 1)
              usage("--port needs port number to listen on");
            try
              {
                user_port = Integer.parseInt(args[i + 1]);
              }
            catch (NumberFormatException nfe)
              {
                usage("--port argument must be a number (" + nfe + ")");
              }
            i += 2;
          }
        else if (args[i].equals("--no-commands"))
          {
            //command_interpreter = false;
            i++;
          }
        //else if (args[i].equals("--eepproxy"))
        //  {
        //    String proxyHost = args[i+1];
        //    String proxyPort = args[i+2];
        //    if (!configured)
        //        util.setProxy(proxyHost, Integer.parseInt(proxyPort));
        //    i += 3;
        //  }
        else if (args[i].equals("--i2cp"))
          {
            String i2cpHost = args[i+1];
            String i2cpPort = args[i+2];
            Properties opts = null;
            if (i+3 < args.length) {
                if (!args[i+3].startsWith("--")) {
                    opts = new Properties();
                    StringTokenizer tok = new StringTokenizer(args[i+3], " \t");
                    while (tok.hasMoreTokens()) {
                        String str = tok.nextToken();
                        int split = str.indexOf('=');
                        if (split > 0) {
                            opts.setProperty(str.substring(0, split), str.substring(split+1));
                        }
                    }
                }
            }
            if (!configured)
                util.setI2CPConfig(i2cpHost, Integer.parseInt(i2cpPort), opts);
            i += 3 + (opts != null ? 1 : 0);
          }
        else
          {
            torrent = args[i];
            i++;
            break;
          }
      }

    if (torrent == null || i != args.length)
      if (torrent != null && torrent.startsWith("-"))
        usage("Unknow option '" + torrent + "'.");
      else
        usage("Need exactly one <url>, <file> or <dir>.");

    return new Snark(util, torrent, ip, user_port, slistener, clistener);
  }
  
  private static void usage(String s)
  {
    System.out.println("snark: " + s);
    usage();
  }

  private static void usage()
  {
    System.out.println
      ("Usage: snark [--no-commands] [--port <port>]");
    System.out.println
      ("             [--eepproxy hostname portnum]");
    System.out.println
      ("             [--i2cp routerHost routerPort ['name=val name=val name=val']]");
    System.out.println
      ("             (<url>|<file>)");
    System.out.println
      ("  --no-commands\tDon't read interactive commands or show usage info.");
    System.out.println
      ("  --port\tThe port to listen on for incomming connections");
    System.out.println
      ("        \t(if not given defaults to first free port between "
       + MIN_PORT + "-" + MAX_PORT + ").");
    System.out.println
      ("  --share\tStart torrent tracker on <ip> address or <host> name.");
    System.out.println
      ("  --eepproxy\thttp proxy to use (default of 127.0.0.1 port 4444)");
    System.out.println
      ("  --i2cp\tlocation of your I2P router (default of 127.0.0.1 port 7654)");
    System.out.println
      ("        \toptional settings may be included, such as");
    System.out.println
      ("        \tinbound.length=2 outbound.length=2 inbound.lengthVariance=-1 ");
    System.out.println
      ("  <url>  \tURL pointing to .torrent metainfo file to download/share.");
    System.out.println
      ("  <file> \tEither a local .torrent metainfo file to download");
    System.out.println
      ("         \tor (with --share) a file to share.");
  }
****/

  /**
   * Aborts program abnormally.
   * @throws RuntimeException always
   */
  private void fatal(String s) throws RuntimeException {
    fatal(s, null);
  }

  /**
   * Aborts program abnormally.
   * @throws RuntimeException always
   */
  private void fatal(String s, Throwable t) throws RuntimeException {
    _log.error(s, t);
    stopTorrent();
    if (t != null)
        s += ": " + t;
    if (completeListener != null)
        completeListener.fatal(this, s);
    throw new RuntimeException(s, t);
  }

  /**
   * Throws a unique exception class to blame the router that can be caught by SnarkManager
   * @throws RouterException always
   * @since 0.9.46
   */
  private void fatalRouter(String s, Throwable t) throws RouterException {
    _log.error(s, t);
    stopTorrent();
    if (completeListener != null)
        completeListener.fatal(this, s);
    throw new RouterException(s, t);
  }

  /**
   * A unique exception class to blame the router that can be caught by SnarkManager
   * @since 0.9.46
   */
  static class RouterException extends RuntimeException {
      public RouterException(String s) { super(s); }
      public RouterException(String s, Throwable t) { super(s, t); }
  }


  /** CoordinatorListener - this does nothing */
  public void peerChange(PeerCoordinator coordinator, Peer peer)
  {
    // System.out.println(peer.toString());
  }
  
  /**
   * Called when the PeerCoordinator got the MetaInfo via magnet.
   * CoordinatorListener.
   * Create the storage, tell SnarkManager, and give the storage
   * back to the coordinator.
   *
   * @throws RuntimeException via fatal()
   * @since 0.8.4
   */
  public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) {
      try {
          String base = Storage.filterName(metainfo.getName());
          File baseFile;
          if (_util.getFilesPublic())
              baseFile = new File(rootDataDir, base);
          else
              baseFile = new SecureFile(rootDataDir, base);
          if (baseFile.exists())
              throw new IOException("Data location already exists: " + baseFile);
          // The following two may throw IOE...
          storage = new Storage(_util, baseFile, metainfo, this, false);
          storage.check();
          // ... so don't set meta until here
          meta = metainfo;
          if (completeListener != null) {
              String newName = completeListener.gotMetaInfo(this);
              if (newName != null)
                  torrent = newName;
              // else some horrible problem
          }
          coordinator.setStorage(storage);
      } catch (IOException ioe) {
          if (storage != null) {
              try { storage.close(); } catch (IOException ioee) {}
              // clear storage, we have a mess if we have non-null storage and null metainfo,
              // as on restart, Storage.reopen() will throw an ioe
              storage = null;
          }
          // TODO we're still in an inconsistent state, won't work if restarted
          // (PeerState "disconnecting seed that connects to seeds"
          fatal("Could not create file for " + getBaseInfo(), ioe);
      }
  }


  ///////////// Begin StorageListener methods

  //private boolean allocating = false;

  /** does nothing */
  public void storageCreateFile(Storage storage, String name, long length)
  {
    //if (allocating)
    //  System.out.println(); // Done with last file.

    //System.out.print("Creating file '" + name
    //                 + "' of length " + length + ": ");
    //allocating = true;
  }

  // How much storage space has been allocated
  private long allocated = 0;

  /** does nothing */
  public void storageAllocated(Storage storage, long length)
  {
    //allocating = true;
    //System.out.print(".");
    //allocated += length;
    //if (allocated == meta.getTotalLength())
    //  System.out.println(); // We have all the disk space we need.
  }

  private boolean allChecked;
  private boolean checking;
  //private boolean prechecking = true;

  public void storageChecked(Storage storage, int num, boolean checked)
  {
    //allocating = false;
    if (!allChecked && !checking)
      {
        // Use the MetaInfo from the storage since our own might not
        // yet be setup correctly.
        //MetaInfo meta = storage.getMetaInfo();
        //if (meta != null)
        //  System.out.print("Checking existing "
        //                   + meta.getPieces()
        //                   + " pieces: ");
        checking = true;
      }
    if (!checking) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Got " + (checked ? "" : "BAD ") + "piece: " + num);
        if (completeListener != null)
            completeListener.gotPiece(this);
    }
  }

  public void storageAllChecked(Storage storage)
  {
    //if (checking)
    //  System.out.println();

    allChecked = true;
    checking = false;
    if (storage.isChanged() && completeListener != null) {
        completeListener.updateStatus(this);
        // this saved the status, so reset the variables
        storage.clearChanged();
        savedUploaded = getUploaded();
    }
  }
  
  public void storageCompleted(Storage storage)
  {
    if (_log.shouldLog(Log.INFO))
        _log.info("Completely received " + torrent);
    //storage.close();
    //System.out.println("Completely received: " + torrent);
    if (completeListener != null) {
        completeListener.torrentComplete(this);
        // this saved the status, so reset the variables
        savedUploaded = getUploaded();
        storage.clearChanged();
    }
  }

  public void setWantedPieces(Storage storage)
  {
    if (coordinator != null)
        coordinator.setWantedPieces();
  }

  ///////////// End StorageListener methods


  /** SnarkSnutdown callback unused */
  public void shutdown()
  {
    // Should not be necessary since all non-daemon threads should
    // have died. But in reality this does not always happen.
    //System.exit(0);
  }
  
  /**
   * StorageListener and CoordinatorListener callback
   * @since 0.9.2
   */
  public void addMessage(String message) {
      if (completeListener != null)
          completeListener.addMessage(this, message);
  }


  /** Maintain a configurable total uploader cap
   * coordinatorListener
   */
  final static int MIN_TOTAL_UPLOADERS = 4;
  final static int MAX_TOTAL_UPLOADERS = 20;

  public boolean overUploadLimit(int uploaders) {
    if (_peerCoordinatorSet == null || uploaders <= 0)
      return false;
    int totalUploaders = 0;
    for (PeerCoordinator c : _peerCoordinatorSet) {
      if (!c.halted())
        totalUploaders += c.getInterestedUploaders();
    }
    int limit = _util.getMaxUploaders();
    if (_log.shouldLog(Log.DEBUG))
        _log.debug("Total uploaders: " + totalUploaders + " Limit: " + limit);
    return totalUploaders > limit;
  }

  /**
   *  Is i2psnark as a whole over its limit?
   */
  public boolean overUpBWLimit() {
    if (_peerCoordinatorSet == null)
      return false;
    long total = 0;
    for (PeerCoordinator c : _peerCoordinatorSet) {
      if (!c.halted())
        total += c.getCurrentUploadRate();
    }
    long limit = 1024l * _util.getMaxUpBW();
    if (_log.shouldLog(Log.INFO))
        _log.info("Total up bw: " + total + " Limit: " + limit);
    return total > limit;
  }

  /**
   * Is a particular peer who has this recent download rate (in Bps) over our upstream bandwidth limit?
   */
  public boolean overUpBWLimit(long total) {
    long limit = 1024l * _util.getMaxUpBW();
    return total > limit;
  }

  /**
   *  A unique ID for this torrent, useful for RPC
   *  @return positive value unless you wrap around
   *  @since 0.9.30
   */
  public int getRPCID() {
    return _rpcID;
  }
    
    /**
     * When did we start this torrent
     * For RPC
     * @return 0 if not started before. Not cleared when stopped.
     * @since 0.9.30
     */
    public long getStartedTime() {
        return _startedTime;
    }
    
  /**
   * The current comment set for this torrent.
   * Not a copy.
   * Caller MUST synch on the returned object for all operations.
   *
   * @return may be null if none
   * @since 0.9.31
   */
  public CommentSet getComments() {
      synchronized(_commentLock) {
          return _comments;
      }
  }
    
  /**
   * Add to the current comment set for this torrent,
   * creating it if it didn't previously exist.
   *
   * @return true if the set changed
   * @since 0.9.31
   */
  public boolean addComments(List<Comment> comments) {
      synchronized(_commentLock) {
          if (_comments == null) {
              _comments = new CommentSet(comments);
              return true;
          } else {
              synchronized(_comments) {
                  return _comments.addAll(comments);
              }
          }
      }
  }
}
