/* TrackerInfo - Holds information returned by a tracker, mainly the peer list.
   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.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEValue;
import org.klomp.snark.bencode.InvalidBEncodingException;

/**
 *  The data structure for the tracker response.
 *  Handles both traditional and compact formats.
 *  Compact format 1 - a list of hashes - early format for testing
 *  Compact format 2 - One big string of concatenated hashes - official format
 */
class TrackerInfo
{
  private final String failure_reason;
  private final int interval;
  private final Set<Peer> peers;
  private int complete;
  private int incomplete;

  /** @param metainfo may be null */
  public TrackerInfo(InputStream in, byte[] my_id, byte[] infohash, MetaInfo metainfo, I2PSnarkUtil util)
    throws IOException
  {
    this(new BDecoder(in), my_id, infohash, metainfo, util);
  }

  private TrackerInfo(BDecoder be, byte[] my_id, byte[] infohash, MetaInfo metainfo, I2PSnarkUtil util)
    throws IOException
  {
    this(be.bdecodeMap().getMap(), my_id, infohash, metainfo, util);
  }

  private TrackerInfo(Map<String, BEValue> m, byte[] my_id, byte[] infohash, MetaInfo metainfo, I2PSnarkUtil util)
    throws IOException
  {
    BEValue reason = m.get("failure reason");
    if (reason != null)
      {
        failure_reason = reason.getString();
        interval = -1;
        peers = null;
      }
    else
      {
        failure_reason = null;
        BEValue beInterval = m.get("interval");
        if (beInterval == null)
          throw new InvalidBEncodingException("No interval given");
        else
          interval = beInterval.getInt();

        BEValue bePeers = m.get("peers");
        if (bePeers == null) {
          peers = Collections.emptySet();
        } else {
            Set<Peer> p;
            try {
              // One big string (the official compact format)
              p = getPeers(bePeers.getBytes(), my_id, infohash, metainfo, util);
            } catch (InvalidBEncodingException ibe) {
              // List of Dictionaries or List of Strings
              p = getPeers(bePeers.getList(), my_id, infohash, metainfo, util);
            }
            peers = p;
        }

        BEValue bev = m.get("complete");
        if (bev != null) try {
          complete = bev.getInt();
          if (complete < 0)
              complete = 0;
        } catch (InvalidBEncodingException ibe) {}

        bev = m.get("incomplete");
        if (bev != null) try {
          incomplete = bev.getInt();
          if (incomplete < 0)
              incomplete = 0;
        } catch (InvalidBEncodingException ibe) {}
      }
  }

/******
  public static Set<Peer> getPeers(InputStream in, byte[] my_id, MetaInfo metainfo)
    throws IOException
  {
    return getPeers(new BDecoder(in), my_id, metainfo);
  }

  public static Set<Peer> getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo)
    throws IOException
  {
    return getPeers(be.bdecodeList().getList(), my_id, metainfo);
  }
******/

  /** List of Dictionaries or List of Strings */
  private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, byte[] infohash, MetaInfo metainfo, I2PSnarkUtil util)
    throws IOException
  {
    Set<Peer> peers = new HashSet<Peer>(l.size());

    for (BEValue bev : l) {
        PeerID peerID;
        try {
            // Case 1 - non-compact - A list of dictionaries (maps)
            peerID = new PeerID(bev.getMap());
        } catch (InvalidBEncodingException ibe) {
            try {
                // Case 2 - compact - A list of 32-byte binary strings (hashes)
                // This was just for testing and is not the official format
                peerID = new PeerID(bev.getBytes(), util);
            } catch (InvalidBEncodingException ibe2) {
                // don't let one bad entry spoil the whole list
                //Snark.debug("Discarding peer from list: " + ibe, Snark.ERROR);
                continue;
            }
        }
        peers.add(new Peer(peerID, my_id, infohash, metainfo));
      }

    return peers;
  }

  private static final int HASH_LENGTH = 32;

  /**
   *  One big string of concatenated 32-byte hashes
   *  @since 0.8.1
   */
  private static Set<Peer> getPeers(byte[] l, byte[] my_id, byte[] infohash, MetaInfo metainfo, I2PSnarkUtil util)
    throws IOException
  {
    int count = l.length / HASH_LENGTH;
    Set<Peer> peers = new HashSet<Peer>(count);

    for (int i = 0; i < count; i++) {
        PeerID peerID;
        byte[] hash = new byte[HASH_LENGTH];
        System.arraycopy(l, i * HASH_LENGTH, hash, 0, HASH_LENGTH);
        try {
            peerID = new PeerID(hash, util);
        } catch (InvalidBEncodingException ibe) {
            // won't happen
            continue;
        }
        peers.add(new Peer(peerID, my_id, infohash, metainfo));
      }

    return peers;
  }

  public Set<Peer> getPeers()
  {
    return peers;
  }

  public int getPeerCount()
  {
    int pc = peers == null ? 0 : peers.size();
    return Math.max(pc, complete + incomplete - 1);
  }

  /** @since 0.9.9 */
  public int getSeedCount()
  {
    return complete;
  }

  /**
   *  Not HTML escaped.
   */
  public String getFailureReason()
  {
    return failure_reason;
  }

  /** in seconds */
  public int getInterval()
  {
    return interval;
  }

    @Override
  public String toString()
  {
    if (failure_reason != null)
      return "TrackerInfo[FAILED: " + failure_reason + "]";
    else
      return "TrackerInfo[interval=" + interval
        + (complete > 0 ? (", complete=" + complete) : "" )
        + (incomplete > 0 ? (", incomplete=" + incomplete) : "" )
        + ", peers=" + peers + "]";
  }
}
