/*
 * ClientProcess.java
 *
 Copyright (C) 2008, 2009 fwd
 
   This file is part of I2PSnarkXL.
 
   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
 */

package org.i2p.i2psnarkxl.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;

import org.i2p.i2psnarkxl.template.web.HtmlGenerator;
import org.i2p.i2psnarkxl.peermanager.utils.BTPeerIDByteDecoder;
import org.klomp.snarkxl.I2PSnarkUtil;
import org.klomp.snarkxl.MetaInfo;
import org.klomp.snarkxl.Peer;
import org.klomp.snarkxl.Snark;
import org.klomp.snarkxl.SnarkManager;
import org.klomp.snarkxl.Storage;
import org.klomp.snarkxl.TrackerClient;
import org.klomp.snarkxl.BitField;
import java.net.*;
//import java.io.*;
//import java.util.*;
//import utils.log.Log;
//import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;

//import net.i2p.util.Log;
//import net.i2p.I2PAppContext;
import net.i2p.data.Destination;
//import net.i2p.data.Base64;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import javax.imageio.ImageIO;
//import java.io.IOException;

//import org.klomp.snarkxl.SnarkManager;
//import org.klomp.snarkxl.Peer;
//import org.klomp.snarkxl.Snark;
//import org.klomp.snarkxl.I2PSnarkUtil;

import org.i2p.i2psnarkxl.Client; // XL
import org.i2p.i2psnarkxl.BitfieldImage; // XL
//import org.klomp.snarkxl.web.I2PSnarkXLServlet;
import org.i2p.i2psnarkxl.template.web.HtmlGenerator;
/**
 *
 * spin off a threaded process to serve each servlet client request.
 */


/**
 * Threaded process to serve the client connected to the socket.
 */
public class ClientProcess extends Thread {
    static private String thisComponent ="ClientProcess";
    private I2PAppContext _context;
    private Log _log;
    HttpServletRequest req;
    HttpServletResponse resp;
    private SnarkManager _manager;
    private HtmlGenerator _htmlGenerator;
    private static long _nonce;
    private boolean xlServletUseTemplates = false;
    private String xlServletConfigFile = null;
    private String servletName = null;
    public boolean stopped = false;
    /**
     * Pass the socket as a argument to the constructor
     */
    public ClientProcess(HttpServletRequest _req, HttpServletResponse _resp,String _servletName ,long nonce) throws ServletException {
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(ClientProcess.class);
        //this.client = client;
        _nonce = nonce;
        _manager = SnarkManager.instance();
        _htmlGenerator = HtmlGenerator.instance();
        _htmlGenerator.setManager(_manager);
        servletName = _servletName;
        //xlServletUseTemplates = true;
        xlServletConfigFile = servletName + ".config";
        setHEADER_BEGIN();
        
        req = _req;
        resp = _resp;
        
        stopped = false;
        // Set the thread priority down so that the ServerSocket
        // will be responsive to new clients.
        setDaemon(true);
        setPriority(Thread.MIN_PRIORITY);
        //setPriority( NORM_PRIORITY - 1 );
    }
    
    /**
     *  This thread receives a request from the client for a
     *  a web page file. The file name is found relative to
     *  the directory of this code.
     */
    public void run() {
        if ( req.getParameter("image") != null ) {
            boolean isPeer = false;
            boolean isClient = false;
            boolean isBitfield = false;
            boolean isOurBitfield = false;
            boolean isOurSuperseedBitfield = false;
            boolean isOurSuperseedXORBitfield = false;
            String image = req.getParameter("image") ;
            try{
                if(Integer.valueOf(image) == 1){
                    isPeer = true;
                }else if(Integer.valueOf(image) == 2){
                    isClient = true;
                }else if(Integer.valueOf(image) == 3){
                    isBitfield = true;
                }else if(Integer.valueOf(image) == 4){//
                    isOurBitfield = true;
                }else if(Integer.valueOf(image) == 5){//)
                    isOurSuperseedBitfield = true;
                }else if(Integer.valueOf(image) == 6){//
                    isOurSuperseedXORBitfield = true;
                }
            }catch(NumberFormatException nfe) {
                _log.error("ClientProcess: Parameter image " + nfe);
                stopped=true;
                return;
            }
            try{
                if(isClient){
                    fireImageByClient("", req, resp);
                }else if(isBitfield){
                    fireImageByBitfield("", req, resp);
                }else if(isOurBitfield){
                    fireImageByOurBitfield("", req, resp);
                }else if(isPeer){
                    fireImageByPeer("", req, resp);
                }else if(isOurSuperseedBitfield){
                    this.fireImageBySuperseedBitfield("", req, resp,false);
                }else if(isOurSuperseedXORBitfield){
                    this.fireImageBySuperseedBitfield("", req, resp,true);
                }else{
                    _log.error("ClientProcess: image call without valid number " + req + " " );
                }
            } catch (IOException ioe) {
                _log.warn("ClientProcess " + req + " " + ioe.toString());
                //throw ioe;
            }
        }else{
            //_log.error("ClientProcess req image == null" + req + " " );
            try{
                doService( req, resp);
            }catch(ServletException se){
                stopped=true;
                _log.error("mse: " + se);
            }catch(IOException ie){
                stopped=true;
                _log.error("mie: " + ie);
            }catch(Exception e){
                stopped=true;
                _log.error("mex: " + e.toString());
            }
        }
        stopped=true;
        // On return from run() the thread process will stop.
    }
    
    public boolean isReady(){
        return stopped;
    }
    
    private void fireImageByPeer(String id, HttpServletRequest _req, HttpServletResponse _resp) throws IOException{
        //if(charset == null){
        //    charset = defaultCharset;
        //}
        int width = 200;
        int snarknumber = -1;
        BufferedImage bufImage = getImageBarNull( 1,  1,Color.red);
        String pidS = _req.getParameter("pid");
        String widthS = _req.getParameter("width");
        String snarkS = _req.getParameter("snark");
        try{
            if (widthS != null && snarkS != null) {//  snarkS - java.lang.ArrayIndexOutOfBoundsException: -1
                width = Integer.valueOf(widthS);
                snarknumber = Integer.valueOf(snarkS);
                if(snarknumber < 0){
                    _log.error("HttpServletRequest fireImage: snarknumber < 0 pid: " + pidS + " width: " + widthS + " snark: " + snarkS + " _req: " + _req);
                    return;
                }
            }else{
                _log.error("HttpServletRequest fireImage null: pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                //getImageBarNull( 1,  1);
                //return;
            }
        }catch(NumberFormatException nfe) {
            _log.error("HttpServletRequest fireImage: NumberFormatException pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req + " ERROR: " + nfe);
            //getImageBarNull( 1,  1);
            return;
        }
        
        // change this to a direct getSnark->getPeer methode!!!
        java.util.List snarks = getSortedSnarks();
        if(snarks == null || snarks.size() < snarknumber || snarks.size() == 0)return;
        Snark snark = (Snark)snarks.get(snarknumber);
        Peer cur = null;
        if (snark != null && snarknumber > -1 && snark.coordinator != null) {
            java.util.List peers = snark.coordinator.peerList();
            synchronized(peers) {
                Iterator it = peers.iterator();
                while (it.hasNext()) {
                    //PEER_ITERATOR
                    Peer peer = (Peer)it.next();
                    
                    if (!peer.isConnected()){
                        continue;
                    }
                    if(peer.getPeerID().toString().equals(pidS) ){
                        cur = peer;
                        break;
                    }
                }
            }
        }
        
        Client client = null;
        //synchronized(Client.instance().clientsXL) {
        client = Client.instance().getClientByPeer(cur);
        //}
        
        //_resp.setCharacterEncoding(charset);
        if(_resp != null){
            try{
            _resp.setContentType("image/png");
            _resp.setHeader("Cache-Control", "no-store");
            _resp.setHeader("Pragma", "no-cache");
            _resp.setDateHeader("Expires", 0);
            }catch (Exception ex){// todo why sometimes we get a nullpointer here???
                return;
            }
            
            
            
            ServletOutputStream responseOutputStream = _resp.getOutputStream();
            if(client != null && client.getImageBarByte() != null){
                // _log.error("client " + client.getClientName() + " peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                responseOutputStream.write( client.getImageBarByte());
                //bufferedImage = client.getImageBarImage();
            }else{
                //_log.error(" peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ImageIO.write(bufImage, "png", byteArrayOutputStream);
                byte[] imageData = byteArrayOutputStream.toByteArray();
                imageData = byteArrayOutputStream.toByteArray();
                responseOutputStream.write(imageData);
            }
            responseOutputStream.flush();
            responseOutputStream.close();
            //PrintWriter out = _resp.getWriter();
            //out.write(content.toString());
        }
        return;
    }
    
    private void fireImageByClient(String id, HttpServletRequest _req, HttpServletResponse _resp) throws IOException{
        //if(charset == null){
        //    charset = defaultCharset;
        //}
        int width = 200;
        //int snarknumber = -1;
        BufferedImage bufImage = getImageBarNull( 1,  1,Color.red);
        String clientidhash = _req.getParameter("clientidhash");
        String metainfohash = _req.getParameter("metainfohash");
        
        
        if (clientidhash != null && metainfohash != null) {
            
        }else{
            _log.error("HttpServletRequest fireImageClient: nullpointer clientidhash: " + clientidhash + " metainfohash: " + metainfohash + " _req: " + _req);
            
        }
        Client client = null;
        //synchronized(Client.instance().clientsXL) {
        client = Client.instance().getClientByHash(clientidhash, metainfohash);
        //}
        if(_resp != null){
            try{
            //_resp.setCharacterEncoding(charset);
            _resp.setContentType("image/png");
            _resp.setHeader("Cache-Control", "no-store");
            _resp.setHeader("Pragma", "no-cache");
            _resp.setDateHeader("Expires", 0);
            
            }catch (Exception ex){// todo why sometimes we get a nullpointer here???
                return;
            }
            
            
            ServletOutputStream responseOutputStream = _resp.getOutputStream();
            if(client != null && client.getImageBarByte() != null){
                // _log.error("client " + client.getClientName() + " peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                responseOutputStream.write( client.getImageBarByte());
                //bufferedImage = client.getImageBarImage();
            }else{
                //_log.error(" peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ImageIO.write(bufImage, "png", byteArrayOutputStream);
                byte[] imageData = byteArrayOutputStream.toByteArray();
                imageData = byteArrayOutputStream.toByteArray();
                responseOutputStream.write(imageData);
            }
            responseOutputStream.flush();
            responseOutputStream.close();
            //PrintWriter out = _resp.getWriter();
            //out.write(content.toString());
        }
        client = null;
        return;
    }
    //toDo make for all fireImage... methodes a util class with less methodes. see also BitfieldImage.class and Client.class
    private void fireImageByOurBitfield(String id, HttpServletRequest _req, HttpServletResponse _resp) throws IOException{
        //if(charset == null){
        //    charset = defaultCharset;
        //}
        int width = 200;
        int snarknumber = -1;
        //BufferedImage bufferedImage = getImageBarNull( 1,  1,Color.red);
        //String clientidhash = _req.getParameter("clientidhash");
        String metainfohash = _req.getParameter("metainfohash");
        
        
        if ( metainfohash != null) {
            
        }else{
            _log.error("HttpServletRequest fireImageClient: nullpointer clientidhash: " + " metainfohash: " + metainfohash + " _req: " + _req);
            
        }
        Snark snark = _manager.getTorrentByMetaInfoHash(metainfohash);
        byte[] bitfieldByte = null;
        BufferedImage bufImage = null;
        if(snark != null && snark.storage != null && snark.storage.complete()){
            bufImage = getImageBarNull( 1,  1,Color.blue);
        }else if(snark != null && snark.coordinator != null && snark.coordinator.isCompleted()){
            bufImage = getImageBarNull( 1,  1,Color.blue);
        }else  if(snark != null && snark.storage != null){
            bitfieldByte = snark.storage.getBitField().getFieldBytes();
            bufImage = BitfieldImage.instance().getOurImageBar(bitfieldByte);
        }else{
            bitfieldByte = _manager.getTorrentBitFieldByteByMetaInfoHash(metainfohash);// our own bitfield
            //_log.error(" metainfohash " + metainfohash + " byte is = " + Base64.encode(bitfieldByte) );
            bufImage = BitfieldImage.instance().getOurImageBar(bitfieldByte);
        }
        
        if(_resp != null){
            //_resp.setCharacterEncoding(charset);
            try{
            _resp.setContentType("image/png");
            _resp.setHeader("Cache-Control", "no-store");
            _resp.setHeader("Pragma", "no-cache");
            _resp.setDateHeader("Expires", 0);
            }catch (Exception ex){// todo why sometimes we get a nullpointer here???
                return;
            }
            
            
            
            
            ServletOutputStream responseOutputStream = _resp.getOutputStream();
            if(true ){
                //_log.error(" peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ImageIO.write(bufImage, "png", byteArrayOutputStream);
                byte[] imageData = byteArrayOutputStream.toByteArray();
                imageData = byteArrayOutputStream.toByteArray();
                responseOutputStream.write(imageData);
            }
            responseOutputStream.flush();
            responseOutputStream.close();
            //PrintWriter out = _resp.getWriter();
            //out.write(content.toString());
            //bitfield = null;
        }
        return;
    }
    
    //toDo make for all fireImage... methodes a util class with less methodes. see also BitfieldImage.class and Client.class
    private void fireImageBySuperseedBitfield(String id, HttpServletRequest _req, HttpServletResponse _resp,boolean isXOR) throws IOException{
        //if(charset == null){
        //    charset = defaultCharset;
        //}
        int width = 200;
        int snarknumber = -1;
        //BufferedImage bufferedImage = getImageBarNull( 1,  1,Color.red);
        //String clientidhash = _req.getParameter("clientidhash");
        String metainfohash = _req.getParameter("metainfohash");
        
        
        if ( metainfohash != null) {
            
        }else{
            _log.error("HttpServletRequest fireImageClient: nullpointer clientidhash: " + " metainfohash: " + metainfohash + " _req: " + _req);
            
        }
        Snark snark = _manager.getTorrentByMetaInfoHash(metainfohash);
        byte[] bitfieldByte = null;
        BufferedImage bufImage = null;
        if(snark != null && snark.storage != null ){
            synchronized(snark.storage.bitfieldSuperseed){
                
                if(!isXOR){
                    bitfieldByte = snark.storage.getSuperseedBitField();// our own Superseedbitfield
                    bufImage = BitfieldImage.instance().getOurImageBar(bitfieldByte);
                }else{
                    bitfieldByte =  snark.storage.getXORSuperseedBitField();// our own SuperseedXORbitfield
                    bufImage = BitfieldImage.instance().getOurImageBar(bitfieldByte);
                    //_log.error(" metainfohash " + metainfohash + " byte is = " + Base64.encode(bitfieldByte) );
                }
            }
        }
        
        if(_resp != null){
            try{
            //_resp.setCharacterEncoding(charset);
            _resp.setContentType("image/png");
            _resp.setHeader("Cache-Control", "no-store");
            _resp.setHeader("Pragma", "no-cache");
            _resp.setDateHeader("Expires", 0);
            
            }catch (Exception ex){// todo why sometimes we get a nullpointer here???
                return;
            }
            
            
            ServletOutputStream responseOutputStream = _resp.getOutputStream();
            if(bufImage != null ){
                //_log.error(" peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ImageIO.write(bufImage, "png", byteArrayOutputStream);
                byte[] imageData = byteArrayOutputStream.toByteArray();
                imageData = byteArrayOutputStream.toByteArray();
                responseOutputStream.write(imageData);
            }
            responseOutputStream.flush();
            responseOutputStream.close();
            //PrintWriter out = _resp.getWriter();
            //out.write(content.toString());
            //bitfield = null;
        }
        return;
    }
    
    //available pieces of all clients except completed
    private void fireImageByBitfield(String id, HttpServletRequest _req, HttpServletResponse _resp) throws IOException{
        //if(charset == null){
        //    charset = defaultCharset;
        //}
        int width = 200;
        int snarknumber = -1;
        BufferedImage bufImage = getImageBarNull( 1,  1,Color.red);
        //String clientidhash = _req.getParameter("clientidhash");
        String metainfohash = _req.getParameter("metainfohash");
        
        
        if ( metainfohash != null) {
            
        }else{
            _log.error("HttpServletRequest fireImageClient: nullpointer clientidhash: " + " metainfohash: " + metainfohash + " _req: " + _req);
            
        }
        BitfieldImage bitfield = BitfieldImage.instance().getBitfieldByMetainfoHash( metainfohash);
        
        if(_resp != null){
            try{
            //_resp.setCharacterEncoding(charset);
            _resp.setContentType("image/png");
            _resp.setHeader("Cache-Control", "no-store");
            _resp.setHeader("Pragma", "no-cache");
            _resp.setDateHeader("Expires", 0);
            }catch (Exception ex){// todo why sometimes we get a nullpointer here???
                return;
            }
            
            
            
            ServletOutputStream responseOutputStream = _resp.getOutputStream();
            byte[] availableImageBarByte = null;
            if(bitfield != null){
                availableImageBarByte = bitfield.getAvailableImageBarByte() ;
            }
            // _log.error("client " + client.getClientName() + " peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
            if( availableImageBarByte != null){
                responseOutputStream.write(availableImageBarByte);
                //bufferedImage = client.getImageBarImage();
            }else{
                //_log.error(" peer: " + cur + " fireImage:  pid: " + pidS + " width: " + widthS + " snark: " + snarkS  + " _req: " + _req);
                
                ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                ImageIO.write(bufImage, "png", byteArrayOutputStream);
                byte[] imageData = byteArrayOutputStream.toByteArray();
                imageData = byteArrayOutputStream.toByteArray();
                responseOutputStream.write(imageData);
            }
            responseOutputStream.flush();
            responseOutputStream.close();
            //PrintWriter out = _resp.getWriter();
            //out.write(content.toString());
        }
        bitfield = null;
        return;
    }
    
    public static BufferedImage getImageBarNull(int width, int height, Color color){
        //width and height currently not in use. maybe i take the values out
        BufferedImage bufImage= new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        
        Graphics2D g2d = bufImage.createGraphics();
        g2d.setColor(color);//Color.red);
        g2d.fill(new Rectangle2D.Float(0, 0, bufImage.getWidth(), bufImage.getHeight()));
        g2d.dispose();
        //g2.setBackground(Color.blue);
        return bufImage;// Image Toolkit.getDefaultToolkit().createImage(bufferedImage.getSource());
    }
    
    
    private java.util.List getSortedSnarks() {
        Set files = _manager.listTorrentFiles();
        TreeSet fileNames = new TreeSet(files); // sorts it alphabetically
        ArrayList rv = new ArrayList(fileNames.size());
        for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) {
            String name = (String)iter.next();
            Snark snark = _manager.getTorrent(name);
            if (snark != null)
                rv.add(snark);
        }
        return rv;
    }
    /*
    public String getIDString(byte[] idExt) {
        // before calling this methode check that  (!snark.coordinator.halted() ))
     
        Destination address = I2PSnarkUtil.instance().getDestination( I2PSnarkUtil.instance().getOurIPString());
        int nonZero = 0;
        for (int i = 0; i < idExt.length; i++) {
            if (idExt[i] != 0) {
                nonZero = i;
                break;
            }
        }
        return Base64.encode(idExt, nonZero, idExt.length-nonZero).substring(0,4) + "@" + address.toBase64().substring(0,6);
    }*/
    
    public synchronized void doService(HttpServletRequest _req, HttpServletResponse _resp) throws ServletException, IOException {
        _req.setCharacterEncoding("UTF-8");
        _resp.setCharacterEncoding("UTF-8");
        _resp.setContentType("text/html; charset=UTF-8");
        long stats[] = {0,0,0,0};
        //since workaround "username" in the init-method this shouldn't be anymore neccessary
        //todo watch this and remove after testing this enough!
        /*
        if(!xlServletInit){
            String requestURI = _req.getRequestURI();
            int start = requestURI.indexOf("/",0);
            int end = -1;
            if(start != -1){
                end = requestURI.indexOf("/",start + 1);
                if(end > start){
                    xlServletConfigFile = requestURI.substring(start + 1, end) + ".config";
                    this.nickname = requestURI.substring(start + 1, end);
                    _manager.setNickname(nickname);
                    _manager.loadConfig(xlServletConfigFile);
                    xlServletInit = true;
                    _manager.setXLServletInit(xlServletInit);
                    setHEADER_BEGIN();
                }
            }
        }
         */
        
        String theme = _req.getParameter("themes");
        //theme = _req.getParameter("themes");
        //if(_req.getParameter("image") == null){
        
        
        if((xlServletUseTemplates && _htmlGenerator.getThemes() != null) || theme != null){
            //use templates | use theme == 'null' for first that is found
            
            //check if some info pages are called
            String clients = _req.getParameter("clients");
            String configs = _req.getParameter("configs");
            if(clients != null){
                _htmlGenerator.showClientsPage(theme, _req, _resp);
            }else if(configs != null){
                _htmlGenerator.showConfigsPage(theme, _req, _resp);
            }else{
                _htmlGenerator.showTheme(theme, _req, _resp);
            }
        }else{
            
            String nonce = _req.getParameter("nonce");
            if ( (nonce != null) && (nonce.equals(String.valueOf(_nonce))) )
                processRequest(_req);
            
            String peerParam = _req.getParameter("p");
            String peerString;
            if (peerParam == null) {
                peerString = "";
            } else {
                peerString = "?p=" + peerParam;
            }
            
            PrintWriter out = _resp.getWriter();
            out.write(HEADER_BEGIN);
            //dwf
            out.write("<meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n");
            out.write("<meta servlet-path=\"" + _req.getServletPath() + "\">\n");
            out.write("<meta servlet-configFile=\"" + xlServletConfigFile + "\">\n");
            // we want it to go to the base URI so we don't refresh with some funky action= value
            out.write("<meta http-equiv=\"refresh\" content=\"60;" + _req.getRequestURI() + peerString + "\">\n");
            out.write(HEADER);
            
            out.write("<table border=\"0\" width=\"100%\">\n");
            out.write("<tr><td width=\"20%\" class=\"snarkTitle\" valign=\"top\" align=\"left\">");
            out.write("I2PSnarkXL<br />\n");
            out.write("<table border=\"0\" width=\"100%\">\n");
            out.write("<tr><td><a href=\"" + _req.getRequestURI() + peerString + "\" class=\"snarkRefresh\">Refresh</a>\n");
            out.write("<td><a href=\"http://forum.i2p/viewforum.php?f=21\" class=\"snarkRefresh\">Forum</a>\n");
            int count = 0;
            Map trackers = _manager.getTrackers();
            for (Iterator iter = trackers.keySet().iterator(); iter.hasNext(); ) {
                String name = (String)iter.next();
                String baseURL = (String)trackers.get(name);
                int e = baseURL.indexOf('=');
                if (e < 0)
                    continue;
                baseURL = baseURL.substring(e + 1);
                if (count++ % 2 == 0)
                    out.write("<tr>");
                out.write("<td><a href=\"" + baseURL + "\" class=\"snarkRefresh\">" + name + "</a>\n");
            }
            if (count % 2 == 1)
                out.write("<td>&nbsp;\n");
            out.write("</table>\n");
            out.write("</td><td width=\"80%\" height=\"80\" class=\"snarkMessages\" valign=\"top\" align=\"left\"> <div style=\"overflow: auto; vertical-align: top; width: 100%; height: 80;\"> <pre>");
            java.util.List msgs = _manager.getMessages();
            for (int i = msgs.size()-1; i >= 0; i--) {
                String msg = (String)msgs.get(i);
                out.write(msg + "\n");
            }
            //test templates start
            ArrayList themes = _htmlGenerator.getThemes();
            if(themes != null){
                out.write("Found " + themes.size() + " themes: ");
                for (int i = 0; i < themes.size(); i++) {
                    String t = (String)themes.get(i);
                    out.write(t);
                    if(i >= 0 && i < themes.size()-1){
                        out.write(", ");
                    }
                }
                out.write(".\n");
            }
            //test templates end
            out.write("</div> </pre></td></tr></table>\n");
            
            java.util.List snarks = getSortedSnarks(_req);
            String uri = _req.getRequestURI();
            out.write(TABLE_HEADER);
            if (I2PSnarkUtil.instance().connected() && snarks.size() > 0) {
                if (peerParam != null)
                    out.write("(<a href=\"" + _req.getRequestURI() + "\">Hide Peers</a>)<br />\n");
                else
                    out.write("(<a href=\"" + _req.getRequestURI() + "?p=1" + "\">Show Peers</a>)<br />\n");
            }
            out.write(TABLE_HEADER2);
            out.write("<th align=\"left\" valign=\"top\">");
            if (I2PSnarkUtil.instance().connected())
                out.write("<a href=\"" + uri + "?action=StopAll&nonce=" + _nonce +
                        "\" title=\"Stop all torrents and the i2p tunnel\">Stop All</a>");
            else if (snarks.size() > 0)
                out.write("<a href=\"" + uri + "?action=StartAll&nonce=" + _nonce +
                        "\" title=\"Start all torrents and the i2p tunnel\">Start All</a>");
            else
                out.write("&nbsp;");
            out.write("</th></tr></thead>\n");
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                boolean showDebug = "2".equals(peerParam);
                boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.meta.getInfoHash()).equals(peerParam);
                displaySnark(out, snark, uri, i, stats, showPeers, showDebug);
            }
            if (snarks.size() <= 0) {
                out.write(TABLE_EMPTY);
            } else if (snarks.size() >= 1) {
                out.write(TABLE_TOTAL);
                out.write("    <th align=\"right\" valign=\"top\">" + formatSize(stats[0]) + "</th>\n" +
                        "    <th align=\"right\" valign=\"top\">" + formatSize(stats[1]) + "</th>\n" +
                        "    <th align=\"right\" valign=\"top\">" + formatSize(stats[2]) + "ps</th>\n" +
                        "    <th align=\"right\" valign=\"top\">" + formatSize(stats[3]) + "ps</th>\n" +
                        "    <th>&nbsp;</th></tr>\n" +
                        "</tfoot>\n");
            }
            
            out.write(TABLE_FOOTER);
            
            writeAddForm(out, _req);
            if (true) // seeding needs to register the torrent first, so we can't start it automatically (boo, hiss)
                writeSeedForm(out, _req);
            writeConfigForm(out, _req);
            out.write(FOOTER);
        }
        // }
    }
    
    /**
     * Do what they ask, adding messages to _manager.addMessage as necessary
     */
    private void processRequest(HttpServletRequest _req) {
        String action = _req.getParameter("action");
        if (action == null) {
            // noop
        } else if ("Add torrent".equals(action)) {
            String newFile = _req.getParameter("newFile");
            String newURL = _req.getParameter("newURL");
            File f = null;
            if ( (newFile != null) && (newFile.trim().length() > 0) )
                f = new File(newFile.trim());
            if ( (f != null) && (!f.exists()) ) {
                _manager.addMessage("Torrent file " + newFile +" does not exist");
            }
            if ( (f != null) && (f.exists()) ) {
                File local = new File(_manager.getDataDir(), f.getName());
                String canonical = null;
                try {
                    canonical = local.getCanonicalPath();
                    
                    if (local.exists()) {
                        if (_manager.getTorrent(canonical) != null)
                            _manager.addMessage("Torrent already running: " + newFile);
                        else
                            _manager.addMessage("Torrent already in the queue: " + newFile);
                    } else {
                        boolean ok = FileUtil.copy(f.getAbsolutePath(), local.getAbsolutePath(), true);
                        if (ok) {
                            _manager.addMessage("Copying torrent to " + local.getAbsolutePath());
                            _manager.addTorrent(canonical);
                        } else {
                            _manager.addMessage("Unable to copy the torrent to " + local.getAbsolutePath() + " from " + f.getAbsolutePath());
                        }
                    }
                } catch (IOException ioe) {
                    _log.warn("hrm: " + local, ioe);
                }
            } else if ( (newURL != null) && (newURL.trim().length() > "http://.i2p/".length()) ) {
                _manager.addMessage("Fetching " + newURL);
                I2PThread fetch = new I2PThread(new FetchAndAdd(_manager, newURL), "Fetch and add");
                fetch.start();
            } else {
                // no file or URL specified
            }
        } else if ("Stop".equals(action)) {
            String torrent = _req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, false);
                            break;
                        }
                    }
                }
            }
        } else if ("Start".equals(action)) {
            String torrent = _req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            snark.startTorrent();//toDo this needs to long make a Thread.start()
                            _manager.addMessage("Starting up torrent " + name);
                            break;
                        }
                    }
                }
            }
        } else if ("Remove".equals(action)) {
            String torrent = _req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, true);
                            // should we delete the torrent file?
                            // yeah, need to, otherwise it'll get autoadded again (at the moment
                            File f = new File(name);
                            f.delete();
                            _manager.addMessage("Torrent file deleted: " + f.getAbsolutePath());
                            break;
                        }
                    }
                }
            }
        } else if ("Delete".equals(action)) {
            String torrent = _req.getParameter("torrent");
            if (torrent != null) {
                byte infoHash[] = Base64.decode(torrent);
                if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
                    for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
                        String name = (String)iter.next();
                        Snark snark = _manager.getTorrent(name);
                        if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) {
                            _manager.stopTorrent(name, true);
                            File f = new File(name);
                            f.delete();
                            _manager.addMessage("Torrent file deleted: " + f.getAbsolutePath());
                            List files = snark.meta.getFiles();
                            String dataFile = snark.meta.getName();
                            f = new File(_manager.getDataDir(), dataFile);
                            if (files == null) { // single file torrent
                                if (f.delete())
                                    _manager.addMessage("Data file deleted: " + f.getAbsolutePath());
                                else
                                    _manager.addMessage("Data file could not be deleted: " + f.getAbsolutePath());
                                break;
                            }
                            for (int i = 0; i < files.size(); i++) { // pass 1 delete files
                                // multifile torrents have the getFiles() return lists of lists of filenames, but
                                // each of those lists just contain a single file afaict...
                                File df = Storage.getFileFromNames(f, (List) files.get(i));
                                if (df.delete())
                                    _manager.addMessage("Data file deleted: " + df.getAbsolutePath());
                                else
                                    _manager.addMessage("Data file could not be deleted: " + df.getAbsolutePath());
                            }
                            for (int i = files.size() - 1; i >= 0; i--) { // pass 2 delete dirs - not foolproof,
                                // we could sort and do a strict bottom-up
                                File df = Storage.getFileFromNames(f, (List) files.get(i));
                                df = df.getParentFile();
                                if (df == null || !df.exists())
                                    continue;
                                if(df.delete())
                                    _manager.addMessage("Data dir deleted: " + df.getAbsolutePath());
                            }
                            break;
                        }
                    }
                }
            }
        } else if ("Save configuration".equals(action)) {
            String dataDir = _req.getParameter("dataDir");
            boolean autoStart = _req.getParameter("autoStart") != null;
            String seedPct = _req.getParameter("seedPct");
            String eepHost = _req.getParameter("eepHost");
            String eepPort = _req.getParameter("eepPort");
            String i2cpHost = _req.getParameter("i2cpHost");
            String i2cpPort = _req.getParameter("i2cpPort");
            String i2cpOpts = _req.getParameter("i2cpOpts");
            String upLimit = _req.getParameter("upLimit");
            String upBW = _req.getParameter("upBW");
            boolean useOpenTrackers = _req.getParameter("useOpenTrackers") != null;
            String openTrackers = _req.getParameter("openTrackers");
            _manager.updateConfig(dataDir, autoStart, seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts, upLimit, upBW, useOpenTrackers, openTrackers);
        } else if ("Create torrent".equals(action)) {
            String baseData = _req.getParameter("baseFile");
            if (baseData != null) {
                File baseFile = new File(_manager.getDataDir(), baseData);
                String announceURL = _req.getParameter("announceURL");
                String announceURLOther = _req.getParameter("announceURLOther");
                if ( (announceURLOther != null) && (announceURLOther.trim().length() > "http://.i2p/announce".length()) )
                    announceURL = announceURLOther;
                
                if (announceURL == null || announceURL.length() <= 0)
                    _manager.addMessage("Error creating torrent - you must select a tracker");
                else if (baseFile.exists()) {
                    try {
                        Storage s = new Storage(baseFile, announceURL, null);
                        s.create();
                        s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
                        MetaInfo info = s.getMetaInfo();
                        File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent");
                        if (torrentFile.exists())
                            throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath());
                        _manager.saveTorrentStatus(info, s.getBitField()); // so addTorrent won't recheck
                        // DirMonitor could grab this first, maybe hold _snarks lock?
                        FileOutputStream out = new FileOutputStream(torrentFile);
                        out.write(info.getTorrentData());
                        out.close();
                        _manager.addMessage("Torrent created for " + baseFile.getName() + ": " + torrentFile.getAbsolutePath());
                        // now fire it up, but don't automatically seed it
                        _manager.addTorrent(torrentFile.getCanonicalPath(), true);
                        _manager.addMessage("Many I2P trackers require you to register new torrents before seeding - please do so before starting " + baseFile.getName());
                    } catch (IOException ioe) {
                        _manager.addMessage("Error creating a torrent for " + baseFile.getAbsolutePath() + ": " + ioe.getMessage());
                    }
                } else {
                    _manager.addMessage("Cannot create a torrent for the nonexistent data: " + baseFile.getAbsolutePath());
                }
            }
        } else if ("StopAll".equals(action)) {
            _manager.addMessage("Stopping all torrents and closing the I2P tunnel");
            List snarks = getSortedSnarks(_req);
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                if (!snark.stopped)
                    _manager.stopTorrent(snark.torrent, false);
            }
            if (I2PSnarkUtil.instance().connected()) {
                I2PSnarkUtil.instance().disconnect();
                _manager.addMessage("I2P tunnel closed");
            }
        } else if ("StartAll".equals(action)) {
            _manager.addMessage("Opening the I2P tunnel and starting all torrents");
            List snarks = getSortedSnarks(_req);
            for (int i = 0; i < snarks.size(); i++) {
                Snark snark = (Snark)snarks.get(i);
                if (snark.stopped)
                    snark.startTorrent();
            }
        }
    }
    
    private List getSortedSnarks(HttpServletRequest _req) {
        Set files = _manager.listTorrentFiles();
        TreeSet fileNames = new TreeSet(files); // sorts it alphabetically
        ArrayList rv = new ArrayList(fileNames.size());
        for (Iterator iter = fileNames.iterator(); iter.hasNext(); ) {
            String name = (String)iter.next();
            Snark snark = _manager.getTorrent(name);
            if (snark != null)
                rv.add(snark);
        }
        return rv;
    }
    
    private static final int MAX_DISPLAYED_FILENAME_LENGTH = 60;
    private static final int MAX_DISPLAYED_ERROR_LENGTH = 40;
    private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, boolean showDebug) throws IOException {
        String filename = snark.torrent;
        File f = new File(filename);
        filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
        int i = filename.lastIndexOf(".torrent");
        if (i > 0)
            filename = filename.substring(0, i);
        if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH)
            filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "...";
        long total = snark.meta.getTotalLength();
        // Early typecast, avoid possibly overflowing a temp integer
        long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0);
        if (remaining > total)
            remaining = total;
        long downBps = 0;
        long upBps = 0;
        if (snark.coordinator != null) {
            downBps = snark.coordinator.getDownloadRate();
            upBps = snark.coordinator.getUploadRate();
        }
        long remainingSeconds;
        if (downBps > 0)
            remainingSeconds = remaining / downBps;
        else
            remainingSeconds = -1;
        boolean isRunning = !snark.stopped;
        long uploaded = 0;
        if (snark.coordinator != null) {
            uploaded = snark.coordinator.getUploaded();
            stats[0] += snark.coordinator.getDownloaded();
        }
        stats[1] += uploaded;
        if (isRunning) {
            stats[2] += downBps;
            stats[3] += upBps;
        }
        
        boolean isValid = snark.meta != null;
        boolean singleFile = snark.meta.getFiles() == null;
        
        String err = null;
        int curPeers = 0;
        int knownPeers = 0;
        if (snark.coordinator != null) {
            err = snark.coordinator.trackerProblems;
            curPeers = snark.coordinator.getPeerCount();
            knownPeers = snark.coordinator.trackerSeenPeers;
        }
        
        String statusString = "Unknown";
        if (err != null) {
            if (isRunning && curPeers > 0 && !showPeers)
                statusString = "<a title=\"" + err + "\">TrackerErr</a> (" +
                        curPeers + "/" + knownPeers +
                        " <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">peers</a>)";
            else if (isRunning)
                statusString = "<a title=\"" + err + "\">TrackerErr (" + curPeers + "/" + knownPeers + " peers)";
            else {
                if (err.length() > MAX_DISPLAYED_ERROR_LENGTH)
                    err = err.substring(0, MAX_DISPLAYED_ERROR_LENGTH) + "...";
                statusString = "TrackerErr<br />(" + err + ")";
            }
        } else if (remaining <= 0) {
            if (isRunning && curPeers > 0 && !showPeers)
                statusString = "Seeding (" +
                        curPeers + "/" + knownPeers +
                        " <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">peers</a>)";
            else if (isRunning)
                statusString = "Seeding (" + curPeers + "/" + knownPeers + " peers)";
            else
                statusString = "Complete";
        } else {
            if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
                statusString = "OK (" +
                        curPeers + "/" + knownPeers +
                        " <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">peers</a>)";
            else if (isRunning && curPeers > 0 && downBps > 0)
                statusString = "OK (" + curPeers + "/" + knownPeers + " peers)";
            else if (isRunning && curPeers > 0 && !showPeers)
                statusString = "Stalled (" +
                        curPeers + "/" + knownPeers +
                        " <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">peers</a>)";
            else if (isRunning && curPeers > 0)
                statusString = "Stalled (" + curPeers + "/" + knownPeers + " peers)";
            else if (isRunning)
                statusString = "No Peers (0/" + knownPeers + ")";
            else
                statusString = "Stopped";
        }
        
        String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
        out.write("<tr class=\"" + rowClass + "\">");
        out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentStatus " + rowClass + "\">");
        out.write(statusString + "</td>\n\t");
        out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentName " + rowClass + "\">");
        
        if (remaining == 0)
            out.write("<a href=\"file:///" + _manager.getDataDir().getAbsolutePath() + File.separatorChar + snark.meta.getName()
            + "\" title=\"Download the completed file\">");
        out.write(filename);
        if (remaining == 0)
            out.write("</a>");
        // temporarily hardcoded for postman and anonymity, requires bytemonsoon patch for lookup by info_hash
        String announce = snark.meta.getAnnounce();
        if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr")) {
            Map trackers = _manager.getTrackers();
            for (Iterator iter = trackers.keySet().iterator(); iter.hasNext(); ) {
                String name = (String)iter.next();
                String baseURL = (String)trackers.get(name);
                if (!baseURL.startsWith(announce))
                    continue;
                int e = baseURL.indexOf('=');
                if (e < 0)
                    continue;
                baseURL = baseURL.substring(e + 1);
                out.write("&nbsp;&nbsp;&nbsp;(<a href=\"" + baseURL + "details.php?dllist=1&filelist=1&info_hash=");
                out.write(TrackerClient.urlencode(snark.meta.getInfoHash()));
                out.write("\" title=\"" + name + " Tracker\">Details</a>)");
                break;
            }
        }
        out.write("</td>\n\t");
        
        out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentETA " + rowClass + "\">");
        if(isRunning && remainingSeconds > 0)
            out.write(DataHelper.formatDuration(remainingSeconds*1000)); // (eta 6h)
        out.write("</td>\n\t");
        out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
        if (remaining > 0)
            out.write(formatSize(total-remaining) + "/" + formatSize(total)); // 18MB/3GB
        else
            out.write(formatSize(total)); // 3GB
        out.write("</td>\n\t");
        out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentUploaded " + rowClass
                + "\">" + formatSize(uploaded) + "</td>\n\t");
        out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentRate\">");
        if(isRunning && remaining > 0)
            out.write(formatSize(downBps) + "ps");
        out.write("</td>\n\t");
        out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentRate\">");
        if(isRunning)
            out.write(formatSize(upBps) + "ps");
        out.write("</td>\n\t");
        out.write("<td valign=\"top\" align=\"left\" class=\"snarkTorrentAction " + rowClass + "\"nowrap>");
        String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash());
        if (showPeers)
            parameters = parameters + "&p=1";
        if (isRunning) {
            out.write("<a href=\"" + uri + "?action=Stop" + parameters
                    + "\" title=\"Stop the torrent\">Stop</a>");
        } else {
            if (isValid)
                out.write("<a href=\"" + uri + "?action=Start" + parameters
                        + "\" title=\"Start the torrent\">Start</a><br />");
            out.write("<a href=\"" + uri + "?action=Remove" + parameters
                    + "\" title=\"Remove the torrent from the active list, deleting the .torrent file\">Remove</a>&nbsp;");
            out.write("<a href=\"" + uri + "?action=Delete" + parameters
                    + "\" title=\"Delete the .torrent file and the associated data file(s)\">Delete</a> ");// i lost a few files because this button was exact below start button
        }
        out.write("</td>\n</tr>\n");
        if(showPeers && isRunning && curPeers > 0) {
            java.util.List peers = snark.coordinator.peerList();
            Iterator it = peers.iterator();
            while (it.hasNext()) {
                Peer peer = (Peer)it.next();
                if (!peer.isConnected())
                    continue;
                out.write("<tr class=\"" + rowClass + "\">");
                out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
                out.write("</td>\n\t");
                out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
                String ch = peer.toString().substring(0, 4);
                String client;
                /*
                if ("AwMD".equals(ch))
                    client = "I2PSnark";
                else if ("BFJT".equals(ch))
                    client = "I2PRufus";
                else if ("TTMt".equals(ch))
                    client = "I2P-BT";
                else if ("LUFa".equals(ch))
                    client = "Azureus";
                else if ("CwsL".equals(ch))
                    client = "I2PSnarkXL";
                 else if ("AkZV".equals(ch))
                    client = "Robert";
                else
                    client = "Unknown";
                 **/
                client = getClientDescription(peer.getPeerID().getID());
                out.write("<font size=-1>" + client + "</font>&nbsp;&nbsp;<tt>" + peer.toString().substring(5, 9) + "</tt>");
                if (showDebug)
                    out.write(" inactive " + (peer.getInactiveTime() / 1000) + "s");
                out.write("</td>\n\t");
                out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
                out.write("</td>\n\t");
                out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
                float pct = (float) (100.0 * (float) peer.getCompleted() / snark.meta.getPieces());
                if (pct == 100.0)
                    out.write("<font size=-1>Seed</font>");
                else {
                    String ps = String.valueOf(pct);
                    if (ps.length() > 5)
                        ps = ps.substring(0, 5);
                    out.write("<font size=-1>" + ps + "%</font>");
                }
                out.write("</td>\n\t");
                out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
                out.write("</td>\n\t");
                out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
                if (remaining > 0) {
                    if (peer.isInteresting() && !peer.isChoked()) {
                        out.write("<font color=#008000>");
                        out.write("<font size=-1>" + formatSize(peer.getDownloadRate()) + "ps</font></font>");
                    } else {
                        out.write("<font color=#a00000><font size=-1><a title=\"");
                        if (!peer.isInteresting())
                            out.write("Uninteresting\">");
                        else
                            out.write("Choked\">");
                        out.write(formatSize(peer.getDownloadRate()) + "ps</a></font></font>");
                    }
                }
                out.write("</td>\n\t");
                out.write("<td valign=\"top\" align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
                if (pct != 100.0) {
                    if (peer.isInterested() && !peer.isChoking()) {
                        out.write("<font color=#008000>");
                        out.write("<font size=-1>" + formatSize(peer.getUploadRate()) + "ps</font></font>");
                    } else {
                        out.write("<font color=#a00000><font size=-1><a title=\"");
                        if (!peer.isInterested())
                            out.write("Uninterested\">");
                        else
                            out.write("Choking\">");
                        out.write(formatSize(peer.getUploadRate()) + "ps</a></font></font>");
                    }
                }
                out.write("</td>\n\t");
                out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
                out.write("</td></tr>\n\t");
                if (showDebug)
                    out.write("<tr><td colspan=\"8\" align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">" + peer.getSocket() + "</td></tr>");
            }
        }
    }
    
    private void writeAddForm(PrintWriter out, HttpServletRequest _req) throws IOException {
        String uri = _req.getRequestURI();
        String newURL = _req.getParameter("newURL");
        if ( (newURL == null) || (newURL.trim().length() <= 0) ) newURL = "http://";
        String newFile = _req.getParameter("newFile");
        if ( (newFile == null) || (newFile.trim().length() <= 0) ) newFile = "";
        
        out.write("<span class=\"snarkNewTorrent\">\n");
        // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
        out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
        out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
        out.write("<span class=\"snarkConfigTitle\">Add Torrent:</span><br />\n");
        out.write("From URL&nbsp;: <input type=\"text\" name=\"newURL\" size=\"80\" value=\"" + newURL + "\" /> \n");
        // not supporting from file at the moment, since the file name passed isn't always absolute (so it may not resolve)
        //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n");
        out.write("<input type=\"submit\" value=\"Add torrent\" name=\"action\" /><br />\n");
        out.write("<span class=\"snarkAddInfo\">Alternately, you can copy .torrent files to " + _manager.getDataDir().getAbsolutePath() + "<br />\n");
        out.write("Removing that .torrent file will cause the torrent to stop.<br /></span>\n");
        out.write("</form>\n</span>\n");
    }
    
    private void writeSeedForm(PrintWriter out, HttpServletRequest _req) throws IOException {
        String uri = _req.getRequestURI();
        String baseFile = _req.getParameter("baseFile");
        if (baseFile == null)
            baseFile = "";
        
        out.write("<span class=\"snarkNewTorrent\"><hr />\n");
        // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file
        out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
        out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
        out.write("<span class=\"snarkConfigTitle\">Create Torrent:</span><br />\n");
        //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br />\n");
        out.write("Data to seed: " + _manager.getDataDir().getAbsolutePath() + File.separatorChar
                + "<input type=\"text\" name=\"baseFile\" size=\"20\" value=\"" + baseFile
                + "\" title=\"File to seed (must be within the specified path)\" /><br />\n");
        out.write("Tracker: <select name=\"announceURL\"><option value=\"\">Select a tracker</option>\n");
        Map trackers = _manager.getTrackers();
        for (Iterator iter = trackers.keySet().iterator(); iter.hasNext(); ) {
            String name = (String)iter.next();
            String announceURL = (String)trackers.get(name);
            int e = announceURL.indexOf('=');
            if (e > 0)
                announceURL = announceURL.substring(0, e);
            out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
        }
        out.write("</select>\n");
        out.write("or <input type=\"text\" name=\"announceURLOther\" size=\"50\" value=\"http://\" " +
                "title=\"Custom tracker URL\" /> ");
        out.write("<input type=\"submit\" value=\"Create torrent\" name=\"action\" />\n");
        out.write("</form>\n</span>\n");
    }
    
    private void writeConfigForm(PrintWriter out, HttpServletRequest _req) throws IOException {
        String uri = _req.getRequestURI();
        String dataDir = _manager.getDataDir().getAbsolutePath();
        boolean autoStart = _manager.shouldAutoStart();
        boolean useOpenTrackers = _manager.shouldUseOpenTrackers();
        String openTrackers = _manager.getOpenTrackerString();
        //int seedPct = 0;
        
        out.write("<form action=\"" + uri + "\" method=\"POST\">\n");
        out.write("<span class=\"snarkConfig\"><hr />\n");
        out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" />\n");
        out.write("<span class=\"snarkConfigTitle\">Configuration:</span><br />\n");
        out.write("Data directory: <input type=\"text\" size=\"40\" name=\"dataDir\" value=\"" + dataDir + "\" ");
        out.write("title=\"Directory to store torrents and data\" disabled=\"true\" /> <i>(Edit " + xlServletConfigFile + " and restart to change)</i><br />\n");
        out.write("Auto start: <input type=\"checkbox\" name=\"autoStart\" value=\"true\" "
                + (autoStart ? "checked " : "")
                + "title=\"If true, automatically start torrents that are added\" />");
        //Auto add: <input type="checkbox" name="autoAdd" value="true" title="If true, automatically add torrents that are found in the data directory" />
        //Auto stop: <input type="checkbox" name="autoStop" value="true" title="If true, automatically stop torrents that are removed from the data directory" />
        //out.write("<br />\n");
/*
        out.write("Seed percentage: <select name=\"seedPct\" disabled=\"true\" >\n\t");
        if (seedPct <= 0)
            out.write("<option value=\"0\" selected=\"true\">Unlimited</option>\n\t");
        else
            out.write("<option value=\"0\">Unlimited</option>\n\t");
        if (seedPct == 100)
            out.write("<option value=\"100\" selected=\"true\">100%</option>\n\t");
        else
            out.write("<option value=\"100\">100%</option>\n\t");
        if (seedPct == 150)
            out.write("<option value=\"150\" selected=\"true\">150%</option>\n\t");
        else
            out.write("<option value=\"150\">150%</option>\n\t");
        out.write("</select><br />\n");
 */
        out.write("Total uploader limit: <input type=\"text\" name=\"upLimit\" value=\""
                + I2PSnarkUtil.instance().getMaxUploaders() + "\" size=\"3\" maxlength=\"3\" /> peers<br />\n");
        out.write("Up bandwidth limit: <input type=\"text\" name=\"upBW\" value=\""
                + I2PSnarkUtil.instance().getMaxUpBW() + "\" size=\"3\" maxlength=\"3\" /> KBps <i>(Router Up BW / 2 recommended)</i><br />\n");
        
        out.write("Use open trackers also: <input type=\"checkbox\" name=\"useOpenTrackers\" value=\"true\" "
                + (useOpenTrackers ? "checked " : "")
                + "title=\"If true, uses open trackers in addition\" /> ");
        out.write("Announce URLs: <input type=\"text\" name=\"openTrackers\" value=\""
                + openTrackers + "\" size=\"50\" /><br />\n");
        
        //out.write("<hr />\n");
        out.write("EepProxy host: <input type=\"text\" name=\"eepHost\" value=\""
                + I2PSnarkUtil.instance().getEepProxyHost() + "\" size=\"15\" /> ");
        out.write("port: <input type=\"text\" name=\"eepPort\" value=\""
                + I2PSnarkUtil.instance().getEepProxyPort() + "\" size=\"5\" maxlength=\"5\" /><br />\n");
        out.write("I2CP host: <input type=\"text\" name=\"i2cpHost\" value=\""
                + I2PSnarkUtil.instance().getI2CPHost() + "\" size=\"15\" /> ");
        out.write("port: <input type=\"text\" name=\"i2cpPort\" value=\"" +
                + I2PSnarkUtil.instance().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" /> <br />\n");
        StringBuffer opts = new StringBuffer(64);
        Map options = new TreeMap(I2PSnarkUtil.instance().getI2CPOptions());
        for (Iterator iter = options.keySet().iterator(); iter.hasNext(); ) {
            String key = (String)iter.next();
            String val = (String)options.get(key);
            opts.append(key).append('=').append(val).append(' ');
        }
        out.write("I2CP opts: <input type=\"text\" name=\"i2cpOpts\" size=\"80\" value=\""
                + opts.toString() + "\" /><br />\n");
        out.write("<input type=\"submit\" value=\"Save configuration\" name=\"action\" />\n");
        out.write("</span>\n");
        out.write("</form>\n");
    }
    
// rounding makes us look faster :)
    private String formatSize(long bytes) {
        if (bytes < 5*1024)
            return bytes + "B";
        else if (bytes < 5*1024*1024)
            return ((bytes + 512)/1024) + "KB";
        else if (bytes < 5*1024*1024*1024l)
            return ((bytes + 512*1024)/(1024*1024)) + "MB";
        else
            return ((bytes + 512*1024*1024)/(1024*1024*1024)) + "GB";
    }
    
    /**
     * Get a client description (name and version) from the given peerID byte array.
     * @param peer_id peerID sent in handshake
     * @return description
     */
    private String getClientDescription( byte[] peer_id ) {
        return BTPeerIDByteDecoder.decode( peer_id );
    }
    
    private String HEADER_BEGIN = "<html>\n" +
            "<head>\n" +
            "<title>" + "Unset" + " - anonymous bittorrent</title>\n";
    
    private void setHEADER_BEGIN(){
        HEADER_BEGIN = "<html>\n" +
                "<head>\n" +
                "<title>" + _manager.getNickname() + " " + _manager.getVersionXL() + " - anonymous bittorrent</title>\n";
        
    }
    
    
    private static final String HEADER = "<style>\n" +
            "body {\n" +
            "	background-color: #C7CFB4;\n" +
            "}\n" +
            ".snarkTitle {\n" +
            "	text-align: left;\n" +
            "	float: left;\n" +
            "	margin: 0px 0px 5px 5px;\n" +
            "	display: inline;\n" +
            "	font-size: 16pt;\n" +
            "	font-weight: bold;\n" +
            "}\n" +
            ".snarkRefresh {\n" +
            "                  font-size: 10pt;\n" +
            "}\n" +
            ".snarkMessages {\n" +
            "	border: none;\n" +
            "                  background-color: #CECFC6;\n" +
            "                  font-family: monospace;\n" +
            "                  font-size: 10pt;\n" +
            "                  font-weight: 100;\n" +
            "                  width: 100%;\n" +
            "                  text-align: left;\n" +
            "                  margin: 0px 0px 0px 0px;\n" +
            "                  border: 0px;\n" +
            "                  padding: 5px;\n" +
            "                  border-width: 0px;\n" +
            "                  border-spacing: 0px;\n" +
            "                  max-height: 12em;\n" +
            "}\n" +
            "table {\n" +
            "	margin: 0px 0px 0px 0px;\n" +
            "	border: 0px;\n" +
            "	padding: 0px;\n" +
            "	border-width: 0px;\n" +
            "	border-spacing: 0px;\n" +
            "}\n" +
            "th {\n" +
            "	background-color: #C7D5D5;\n" +
            "	padding: 0px 7px 0px 3px;\n" +
            "}\n" +
            "td {\n" +
            "	padding: 0px 7px 0px 3px;\n" +
            "}\n" +
            ".snarkTorrentEven {\n" +
            "	background-color: #E7E7E7;\n" +
            "}\n" +
            ".snarkTorrentOdd {\n" +
            "	background-color: #DDDDCC;\n" +
            "}\n" +
            ".snarkNewTorrent {\n" +
            "	font-size: 10pt;\n" +
            "}\n" +
            ".snarkAddInfo {\n" +
            "	font-size: 10pt;\n" +
            "}\n" +
            ".snarkConfigTitle {\n" +
            "	font-size: 12pt;\n" +
            "                  font-weight: bold;\n" +
            "}\n" +
            ".snarkConfig {\n" +
            "                  font-size: 10pt;\n" +
            "}\n" +
            "</style>\n" +
            "</head>\n" +
            "<body>\n";
    
    
    private static final String TABLE_HEADER = "<table border=\"0\" class=\"snarkTorrents\" width=\"100%\" cellpadding=\"0 10px\">\n" +
            "<thead>\n" +
            "<tr><th align=\"left\" valign=\"top\">Status \n";
    
    private static final String TABLE_HEADER2 = "</th>\n" +
            "    <th align=\"left\" valign=\"top\">Torrent</th>\n" +
            "    <th align=\"right\" valign=\"top\">ETA</th>\n" +
            "    <th align=\"right\" valign=\"top\">Downloaded</th>\n" +
            "    <th align=\"right\" valign=\"top\">Uploaded</th>\n" +
            "    <th align=\"right\" valign=\"top\">Down Rate</th>\n" +
            "    <th align=\"right\" valign=\"top\">Up Rate</th>\n";
    
    private static final String TABLE_TOTAL =  "<tfoot>\n" +
            "<tr><th align=\"left\" valign=\"top\">Totals</th>\n" +
            "    <th>&nbsp;</th>\n" +
            "    <th>&nbsp;</th>\n";
    
    private static final String TABLE_EMPTY  = "<tr class=\"snarkTorrentEven\">" +
            "<td class=\"snarkTorrentEven\" align=\"left\"" +
            "    valign=\"top\" colspan=\"8\">No torrents</td></tr>\n";
    
    private static final String TABLE_FOOTER = "</table>\n";
    
    private static final String FOOTER = "</body></html>";
    
    
}


class FetchAndAdd implements Runnable {
    private SnarkManager _manager;
    private String _url;
    public FetchAndAdd(SnarkManager mgr, String url) {
        _manager = mgr;
        _url = url;
    }
    public void run() {
        _url = _url.trim();
        // 3 retries
        File file = I2PSnarkUtil.instance().get(_url, false, 3);
        try {
            if ( (file != null) && (file.exists()) && (file.length() > 0) ) {
                _manager.addMessage("Torrent fetched from " + _url);
                FileInputStream in = null;
                try {
                    in = new FileInputStream(file);
                    MetaInfo info = new MetaInfo(in);
                    String name = info.getName();
                    name = name.replace('/', '_');
                    name = name.replace('\\', '_');
                    name = name.replace('&', '+');
                    name = name.replace('\'', '_');
                    name = name.replace('"', '_');
                    name = name.replace('`', '_');
                    name = name + ".torrent";
                    File torrentFile = new File(_manager.getDataDir(), name);
                    
                    String canonical = torrentFile.getCanonicalPath();
                    
                    if (torrentFile.exists()) {
                        if (_manager.getTorrent(canonical) != null)
                            _manager.addMessage("Torrent already running: " + name);
                        else
                            _manager.addMessage("Torrent already in the queue: " + name);
                    } else {
                        FileUtil.copy(file.getAbsolutePath(), canonical, true);
                        _manager.addTorrent(canonical);
                    }
                } catch (IOException ioe) {
                    _manager.addMessage("Torrent at " + _url + " was not valid: " + ioe.getMessage());
                } finally {
                    try { in.close(); } catch (IOException ioe) {}
                }
            } else {
                _manager.addMessage("Torrent was not retrieved from " + _url);
            }
        } finally {
            if (file != null) file.delete();
        }
    }
}