/* TrackerClient - Class that informs a tracker and gets new 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.snarkxl;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.i2p.i2psnarkxl.Client;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

/**
 * Informs metainfo tracker of events and gets new peers for peer
 * coordinator.
 *
 * @author Mark Wielaard (mark@klomp.org)
 */
public class TrackerClient extends I2PThread {
    private static final Log _log = new Log(TrackerClient.class);
    private static final String NO_EVENT = "";
    private static final String STARTED_EVENT = "started";
    private static final String COMPLETED_EVENT = "completed";
    private static final String STOPPED_EVENT = "stopped";
    private static final String NOT_REGISTERED  = "torrent not registered"; //bytemonsoon
    
    private final static int SLEEP = 5; // 5 minutes.
    private final static int DELAY_MIN = 2000; // 2 secs.
    private final static int DELAY_MUL = 1500; // 1.5 secs.
    private final static int MAX_REGISTER_FAILS = 10; // * INITIAL_SLEEP = 15m to register
    private final static int INITIAL_SLEEP = 90*1000;
    private final static int MAX_CONSEC_FAILS = 5;    // slow down after this
    private final static int LONG_SLEEP = 30*60*1000; // sleep a while after lots of fails
    
    private final MetaInfo meta;
    private final PeerCoordinator coordinator;
    private final int port;
    
    private boolean stop;
    private boolean started;
    
    private List trackers;
    
    public TrackerClient(MetaInfo meta, PeerCoordinator coordinator) {
        // Set unique name.
        super("TrackerClient-" + urlencode(coordinator.getID()));
        this.meta = meta;
        this.coordinator = coordinator;
        
        this.port = 6881; //(port == -1) ? 9 : port;
        
        stop = false;
        started = false;
    }
    
    public void start() {
        if (stop) throw new RuntimeException("Dont rerun me, create a copy");
        super.start();
        started = true;
    }
    
    public boolean halted() { return stop; }
    public boolean started() { return started; }
    
    /**
     * Interrupts this Thread to stop it.
     */
    public void halt() {
        stop = true;
        this.interrupt();
    }
    
    private boolean verifyConnected() {
        while (!stop && !I2PSnarkUtil.instance().connected()) {
            boolean ok = I2PSnarkUtil.instance().connect();
            if (!ok) {
                try { Thread.sleep(30*1000); } catch (InterruptedException ie) {}
            }
        }
        return !stop && I2PSnarkUtil.instance().connected();
    }
    
    public void run() {
        String infoHash = urlencode(meta.getInfoHash());
        String peerID = urlencode(coordinator.getID());
        
        _log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash);
        
        // Construct the list of trackers for this torrent,
        // starting with the primary one listed in the metainfo,
        // followed by the secondary open trackers
        // It's painful, but try to make sure if an open tracker is also
        // the primary tracker, that we don't add it twice.
        trackers = new ArrayList(2);
        trackers.add(new Tracker(meta.getAnnounce(), true));
        List tlist = SnarkManager.instance().getOpenTrackers();
        if (tlist != null) {
            for (int i = 0; i < tlist.size(); i++) {
                String url = (String)tlist.get(i);
                if (!url.startsWith("http://")) {
                    _log.error("Bad announce URL: [" + url + "]");
                    continue;
                }
                int slash = url.indexOf('/', 7);
                if (slash <= 7) {
                    _log.error("Bad announce URL: [" + url + "]");
                    continue;
                }
                if (meta.getAnnounce().startsWith(url.substring(0, slash)))
                    continue;
                String dest = I2PSnarkUtil.instance().lookup(url.substring(7, slash));
                if (dest == null) {
                    _log.error("Announce host unknown: [" + url + "]");
                    continue;
                }
                if (meta.getAnnounce().startsWith("http://" + dest))
                    continue;
                if (meta.getAnnounce().startsWith("http://i2p/" + dest))
                    continue;
                trackers.add(new Tracker(url, false));
                _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
            }
        }
        
        long uploaded = coordinator.getUploaded();
        long downloaded = coordinator.getDownloaded();
        long left = coordinator.getLeft();
        
        boolean completed = (left == 0);
        int sleptTime = 0;
        
        try {
            if (!verifyConnected()) return;
            boolean started = false;
            boolean firstTime = true;
            int consecutiveFails = 0;
            Random r = new Random();
            while(!stop) {
                try {
                    // Sleep some minutes...
                    // Sleep the minimum interval for all the trackers, but 60s minimum
                    // except for the first time...
                    int delay;
                    int random = r.nextInt(120*1000);//120
                    if (firstTime) {
                        delay = r.nextInt(30*1000);
                        firstTime = false;
                    } else if (completed && started)
                        delay = 3*SLEEP*60*1000 + random;
                    else if (coordinator.trackerProblems != null && ++consecutiveFails < MAX_CONSEC_FAILS)
                        delay = INITIAL_SLEEP;
                    else
                        // sleep a while, when we wake up we will contact only the trackers whose intervals have passed
                        delay = SLEEP*60*1000 + random;
                    
                    if (delay > 0)
                        Thread.sleep(delay);
                } catch(InterruptedException interrupt) {
                    // ignore
                }
                
                if (stop)
                    break;
                
                if (!verifyConnected()) return;
                
                uploaded = coordinator.getUploaded();
                downloaded = coordinator.getDownloaded();
                left = coordinator.getLeft();
                
                // First time we got a complete download?
                String event;
                if (!completed && left == 0) {
                    completed = true;
                    event = COMPLETED_EVENT;
                } else
                    event = NO_EVENT;
                
                // *** loop once for each tracker
                // Only do a request when necessary.
                sleptTime = 0;
                int maxSeenPeers = 0;
                for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
                    Tracker tr = (Tracker)iter.next();
                    if ((!stop) && (!tr.stop) &&
                            (completed || coordinator.needPeers()) &&
                            (event == COMPLETED_EVENT || System.currentTimeMillis() > tr.lastRequestTime + tr.interval)) {
                        try {
                            if (!tr.started)
                                event = STARTED_EVENT;
                            TrackerInfo info = doRequest(tr, infoHash, peerID,
                                    uploaded, downloaded, left,
                                    event);
                            
                            coordinator.trackerProblems = null;
                            tr.trackerProblems = null;
                            tr.registerFails = 0;
                            tr.consecutiveFails = 0;
                            if (tr.isPrimary)
                                consecutiveFails = 0;
                            started = true;
                            tr.started = true;
                            
                            Set peers = info.getPeers();
                            tr.seenPeers = peers.size();
                            if (coordinator.trackerSeenPeers < tr.seenPeers) // update rising number quickly
                                coordinator.trackerSeenPeers = tr.seenPeers;
                            if ( ((left > 0) && (!completed)) ||  (completed) ) {// fwd and we WANT seeding active! adding (completed) todo usersetting for active seeding
                                // we only want to talk to new people if we need things
                                // from them (duh)
                                List ordered = new ArrayList(peers);
                                Collections.shuffle(ordered);
                                Iterator it = ordered.iterator();
                                while (it.hasNext()) {
                                    Peer cur = (Peer)it.next();
                                    
                                    //toDo make methode for the following checkups of Client.class
                                    // checkup start
                                    
                                    // do not make a new client! a pointer is enough!
                                    
                                    // like source is written we doesn't known if this peer is completed
                                    //but since we have the Client.class we can watch for old informations
                                    Client client = null;
                                    //synchronized(Client.instance().clientsXL) {
                                        client = Client.instance().getClientByPeer(cur);
                                    //}
                                    
                                    if(client != null){
                                        client.pingByTrackerList();// increase counter; update time
                                        if( client.isCompleted() && completed){
                                            //avoid from reconnections seeder/seeder
                                            continue;
                                        }
                                    }
                                    //fwd check if it is banned
                                    if(Client.instance().generalBannedClientsXL.contains((String)(cur.getPeerID().getAddress().toBase64()))){
                                        // address is in the global bannedlist
                                    /* do not delete it
                                     * just ignore it
                                     * we might unban it soon
                                     **/
                                        continue;
                                    }// end of banned check
                                    // checkup end
                                    
                                    // only delay if we actually make an attempt to add peer
                                    if(coordinator.addPeer(cur)) {
                                        int delay = DELAY_MUL;
                                        delay *= ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 10;
                                        delay += DELAY_MIN;
                                        sleptTime += delay;
                                        try { Thread.sleep(delay); } catch (InterruptedException ie) {}
                                    }
                                    
                                    //remove this in client.class and use this for a new torrent class
                                    //client may not be added to list clientXL before first request try it again after addPeer
                                    if(client == null){
                                        
                                        //synchronized(Client.instance().clientsXL) {
                                            client = Client.instance().getClientByPeer(cur);
                                        //}
                                        
                                        if(client != null){
                                            client.pingByTrackerList();// increase counter; update time
                                        }
                                    }
                                    
                                }
                            }
                        } catch (IOException ioe) {
                            // Probably not fatal (if it doesn't last to long...)
                            Snark.debug
                                    ("WARNING: Could not contact tracker at '"
                                    + tr.announce + "': " + ioe, Snark.WARNING);
                            tr.trackerProblems = ioe.getMessage();
                            // don't show secondary tracker problems to the user
                            if (tr.isPrimary)
                                coordinator.trackerProblems = tr.trackerProblems;
                            if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) {
                                // Give a guy some time to register it if using opentrackers too
                                if (trackers.size() == 1) {
                                    stop = true;
                                    coordinator.snark.stopTorrent();
                                } else { // hopefully each on the opentrackers list is really open
                                    if (tr.registerFails++ > MAX_REGISTER_FAILS)
                                        tr.stop = true;
                                }
                            }
                            if (++tr.consecutiveFails == MAX_CONSEC_FAILS) {
                                tr.seenPeers = 0;
                                if (tr.interval < LONG_SLEEP)
                                    tr.interval = LONG_SLEEP;  // slow down
                            }
                        }
                    }
                    if ((!tr.stop) && maxSeenPeers < tr.seenPeers)
                        maxSeenPeers = tr.seenPeers;
                }  // *** end of trackers loop here
                
                // we could try and total the unique peers but that's too hard for now
                coordinator.trackerSeenPeers = maxSeenPeers;
                if (!started)
                    Snark.debug("         Retrying in one minute...", Snark.DEBUG);
            } // *** end of while loop
        } // try
        catch (Throwable t) {
            I2PSnarkUtil.instance().debug("TrackerClient: " + t, Snark.ERROR, t);
            if (t instanceof OutOfMemoryError)
                throw (OutOfMemoryError)t;
        } finally {
            try {
                // try to contact everybody we can
                // We don't need I2CP connection for eepget
                // if (!verifyConnected()) return;
                for (Iterator iter = trackers.iterator(); iter.hasNext(); ) {
                    Tracker tr = (Tracker)iter.next();
                    if (tr.started && (!tr.stop) && tr.trackerProblems == null)
                        doRequest(tr, infoHash, peerID, uploaded,
                                downloaded, left, STOPPED_EVENT);
                }
            } catch(IOException ioe) { /* ignored */ }
        }
        
    }
    
    private TrackerInfo doRequest(Tracker tr, String infoHash,
            String peerID, long uploaded,
            long downloaded, long left, String event)
            throws IOException {
        String s = tr.announce
                + "?info_hash=" + infoHash
                + "&peer_id=" + peerID
                + "&port=" + port
                + "&ip=" + I2PSnarkUtil.instance().getOurIPString() + ".i2p"
                + "&uploaded=" + uploaded
                + "&downloaded=" + downloaded
                + "&left=" + left
                + ((event != NO_EVENT) ? ("&event=" + event) : "");
        Snark.debug("Sending TrackerClient request: " + s, Snark.INFO);
        
        tr.lastRequestTime = System.currentTimeMillis();
        File fetched = I2PSnarkUtil.instance().get(s);
        if (fetched == null) {
            throw new IOException("Error fetching " + s);
        }
        fetched.deleteOnExit();
        
        InputStream in = null;
        try {
            in = new FileInputStream(fetched);
            
            TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
                    coordinator.getMetaInfo());
            Snark.debug("TrackerClient response: " + info, Snark.INFO);
            
            String failure = info.getFailureReason();
            if (failure != null)
                throw new IOException(failure);
            
            tr.interval = info.getInterval() * 1000;
            return info;
        } finally {
            if (in != null) try { in.close(); } catch (IOException ioe) {}
            fetched.delete();
        }
    }
    
    /**
     * Very lazy byte[] to URL encoder.  Just encodes everything, even
     * "normal" chars.
     */
    public static String urlencode(byte[] bs) {
        StringBuffer sb = new StringBuffer(bs.length*3);
        for (int i = 0; i < bs.length; i++) {
            int c = bs[i] & 0xFF;
            sb.append('%');
            if (c < 16)
                sb.append('0');
            sb.append(Integer.toHexString(c));
        }
        
        return sb.toString();
    }
    
    private class Tracker {
        String announce;
        boolean isPrimary;
        long interval;
        long lastRequestTime;
        String trackerProblems;
        boolean stop;
        boolean started;
        int registerFails;
        int consecutiveFails;
        int seenPeers;
        
        public Tracker(String a, boolean p) {
            announce = a;
            isPrimary = p;
            interval = INITIAL_SLEEP;
            lastRequestTime = 0;
            trackerProblems = null;
            stop = false;
            started = false;
            registerFails = 0;
            consecutiveFails = 0;
            seenPeers = 0;
        }
    }
}
