/* Storage - Class used to store and retrieve pieces.
   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.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Random;

import net.i2p.crypto.SHA1;

import net.i2p.I2PAppContext;
import net.i2p.util.Log;
import org.i2p.i2psnarkxl.BitfieldImage; // XL

import org.klomp.snarkxl.SnarkManager;//need for add maessage
/**
 * Maintains pieces on disk. Can be used to store and retrieve pieces.
 */
public class Storage {
    private Log _log = new Log(Storage.class);
    private I2PAppContext _context;
    
    private MetaInfo metainfo;
    private long[] lengths;
    private RandomAccessFile[] rafs;
    private String[] names;
    
    private final StorageListener listener;
    
    private BitField bitfield; // BitField to represent the pieces
    private int needed; // Number of pieces needed
    
    public byte[] bitfieldSuperseed;// becomes filled with send pieces
    //XL
    public boolean isSuperseed = false;
    public boolean uploadNeedsRefresh = false;
    public boolean isForceStopseed = false;
    // some of the following List's would be removed, but yet i nedd them for some stduies
    public java.util.List messageHavePieceXL = new ArrayList();
    public java.util.List messagePieceXL = new ArrayList();
    public java.util.List randomPieceXL = new ArrayList(); //this NEVER CLEAR!!
    public java.util.List overflowPieceXL = new ArrayList(); //this NEVER CLEAR!
    public java.util.List sendPieceXL = new ArrayList();
    public java.util.List sendoutPieceXL = new ArrayList();
    public java.util.List xorPieceXL = new ArrayList();
    
    public java.util.List toSendRandomPieceXL = new ArrayList();
    //public static final java.util.List toSendOverflowPieceXL = new ArrayList();
    
    //boolean isSuperseed = false; see _manager
    // XXX - Not always set correctly
    int piece_size;
    int pieces;
    boolean changed;
    
    /** The default piece size. */
    private static int MIN_PIECE_SIZE = 256*1024;
    private static int MAX_PIECE_SIZE = 1024*1024;
    /** The maximum number of pieces in a torrent. */
    private static long MAX_PIECES = 100*1024/20;
    
    /**
     * Creates a new storage based on the supplied MetaInfo.  This will
     * try to create and/or check all needed files in the MetaInfo.
     *
     * @exception IOException when creating and/or checking files fails.
     */
    public Storage(MetaInfo metainfo, StorageListener listener)
    throws IOException {
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(Storage.class);
        this.metainfo = metainfo;
        this.listener = listener;
        needed = metainfo.getPieces();
        bitfield = new BitField(needed);
        //xl
        bitfieldSuperseed = new byte[bitfield.getFieldBytes().length];
        makeRandomList();
    }
    
    /**
     * Creates a storage from the existing file or directory together
     * with an appropriate MetaInfo file as can be announced on the
     * given announce String location.
     */
    public Storage(File baseFile, String announce, StorageListener listener)
    throws IOException {
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(Storage.class);
        this.listener = listener;
        // Create names, rafs and lengths arrays.
        getFiles(baseFile);
        
        long total = 0;
        ArrayList lengthsList = new ArrayList();
        for (int i = 0; i < lengths.length; i++) {
            long length = lengths[i];
            total += length;
            lengthsList.add(new Long(length));
        }
        
        piece_size = MIN_PIECE_SIZE;
        pieces = (int) ((total - 1)/piece_size) + 1;
        while (pieces > MAX_PIECES && piece_size < MAX_PIECE_SIZE) {
            piece_size = piece_size*2;
            pieces = (int) ((total - 1)/piece_size) +1;
        }
        
        // Note that piece_hashes and the bitfield will be filled after
        // the MetaInfo is created.
        byte[] piece_hashes = new byte[20*pieces];
        bitfield = new BitField(pieces);
        //xl
        bitfieldSuperseed = new byte[bitfield.getFieldBytes().length];
        
        needed = 0;
        
        List files = new ArrayList();
        for (int i = 0; i < names.length; i++) {
            List file = new ArrayList();
            StringTokenizer st = new StringTokenizer(names[i], File.separator);
            while (st.hasMoreTokens()) {
                String part = st.nextToken();
                file.add(part);
            }
            files.add(file);
        }
        
        String name = baseFile.getName();
        if (files.size() == 1) // FIXME: ...and if base file not a directory or should this be the only check?
            // this makes a bad metainfo if the directory has only one file in it
        {
            files = null;
            lengthsList = null;
        }
        
        // Note that the piece_hashes are not correctly setup yet.
        metainfo = new MetaInfo(announce, baseFile.getName(), null, files,
                lengthsList, piece_size, piece_hashes, total);
        makeRandomList();// after metainfo!!!
        
    }
    //todo make a superseed.class and move it there
    private void makeRandomList(){
        java.util.List randList = new ArrayList();
        for(int i = 0; i < metainfo.getPieces(); i++)
            randList.add(""+i);
        Collections.shuffle(randList);
        randomPieceXL.addAll(randList);// keep this allways to restore the toSend..List
        //resetBitfieldSuperseed();
        //toSendRandomPieceXL.addAll(randomPieceXL);// our working list that decrease with every piece that we've had announced
        randList = null;
        //----------------------------------------------- not in use
        if(false){
            java.util.List rangeList = new ArrayList();
            for(int i = 0; i < metainfo.getPieces() ; i++){
                if(!(i/8 >= bitfield.getFieldBytes().length))
                    rangeList.add(""+i);
                else
                    overflowPieceXL.add(""+i);
            }
            while(rangeList.size() != 0){
                //Random randB = new Random(System.currentTimeMillis());
                int index =  (int) (Math.random() *  rangeList.size());
                // if(index > metainfo.getPieces()) index = 0;
                randomPieceXL.add(rangeList.get(index));
                rangeList.remove(index);
                if(rangeList.size() == 1){
                    randomPieceXL.add(rangeList.get(0));
                    rangeList.remove(0);
                }
            }
        }
    }
    //todo make own class for superseed and remove this
    public void resetBitfieldSuperseed(){
        bitfieldSuperseed = new byte[bitfield.getFieldBytes().length];
        synchronized(messageHavePieceXL){
            messageHavePieceXL.clear();
        }
        synchronized(messagePieceXL){
            messagePieceXL.clear();
        }
        //randomPieceXL.clear(); //this NEVER CLEAR!!
        synchronized(sendPieceXL){
            sendPieceXL.clear();
        }
        updateToSendList(false);
    }
    //todo make own class for superseed and remove this
    // current this returns exaxct our own bitfield if we are incomplete
    // todo: make in BitfieldImage.class a methode that exclude our own. else this makes no reason! we could just ..
    // .. test if we are completed .. else we use our bitfield.clone() as byte[] bitfieldSuperseed
    public void resetBitfieldSuperseedWithAvailablePieces(String metainfo_hash){
        byte[] b = null;
        synchronized(BitfieldImage.instance().bitfieldsXL) {
            b = BitfieldImage.instance().getAvailableBitfieldByteByMetainfoHash(metainfo_hash);
        }
        if(b != null && b.length == bitfieldSuperseed.length){
            byte[] a =  bitfield.getFieldBytes();//ourBitfieldByte;
            byte[] c = new byte[bitfieldSuperseed.length];//
            for(int j=0;j<b.length;j++){// make sure that we only add pieces we have allready!
                c[j] = (byte)( a[j] & b[j] );
            }
            bitfieldSuperseed = c.clone();
            a = null;
            b = null;
            c = null;
        }//else log error - warn user?
        synchronized(messageHavePieceXL){
            messageHavePieceXL.clear();
        }
        synchronized(messagePieceXL){
            messagePieceXL.clear();
        }
        //randomPieceXL.clear(); //this NEVER CLEAR!!
        synchronized(sendPieceXL){
            sendPieceXL.clear();
        }
        
        updateToSendList(true);
        
    }
    
    private void updateToSendList(boolean withXOR){
        synchronized(toSendRandomPieceXL){
            toSendRandomPieceXL.clear();
            toSendRandomPieceXL.addAll(randomPieceXL);// our working list that decrease with every piece that we've had announced
            if(needed == 0){//to upload all pieces you have to switch "OFF" superseed
                //first piece we keep from sucking pigs
                toSendRandomPieceXL.remove("0"); 
                //toSendRandomPieceXL.remove("1");
            }
        }
        
        byte[] b = null;
        
        if(withXOR)
            b = getXORSuperseedBitField().clone();
        else
            b = bitfield.getFieldBytes();//ourBitfieldByte;
        
        if(b != null){
            for(int i = 0; i < b.length*8; i++){
                if(i< toSendRandomPieceXL.size()){
                    int value = Integer.valueOf((String)toSendRandomPieceXL.get(i));//todo numberformat exception?
                    if(!hasClientBitmapPiece(value,b)){
                        synchronized(toSendRandomPieceXL){
                            toSendRandomPieceXL.remove(i);
                        }
                        i--;
                        continue;
                    }
                }
            }
        }
        uploadNeedsRefresh = false;
        SnarkManager.instance().addMessage("Superseed: Uploadlist for '" + metainfo.getName() + "' refreshed." );
        
    }
    
// Creates piece hases for a new storage.
    public void create() throws IOException {
//    if (true) {
        fast_digestCreate();
//    } else {
//        orig_digestCreate();
//    }
    }
    
//todo make own class for superseed and remove this
//returns that what we have and not send right now
    public byte[] getXORSuperseedBitField(){
        byte[] a =  bitfield.getFieldBytes();//ourBitfieldByte;
        byte[] b = bitfieldSuperseed;
        byte[] c = new byte[bitfieldSuperseed.length];// the rest what we have but is not send yet
        for(int j=0;j<b.length;j++){
            c[j] = (byte)( a[j] ^ b[j] );
            
        }
        a = null;
        b = null;
        return c.clone();
    }
    
    public int getRandomPiece(byte[] bitmapByte){
        if(bitmapByte == null || //todo throw exception for (bitmapByte == null)?
                (isForceStopseed && needed == 0)) return -1;// only seeder could force a upload stop
        boolean autocleanup = false;
        boolean autorefill = false;
        if(toSendRandomPieceXL.size() > 0 ){
            byte[] b = bitmapByte;
            synchronized(toSendRandomPieceXL){
                for(int i = 0; i < b.length*8; i++){
                    if(i<toSendRandomPieceXL.size()){
                        int value = Integer.valueOf((String)toSendRandomPieceXL.get(i));//todo numberformat exception?
                        if(hasClientBitmapPiece(value,bitmapByte) || !hasClientBitmapPiece(value,getXORSuperseedBitField().clone())){
                            if(autocleanup || !hasClientBitmapPiece(value,getXORSuperseedBitField().clone())){
                                //todo we need to update the xor bitfield too
                                //mmh or not! because it's recover from that in the fill routine -autorefill-!
                                //and that we need when clients left or in case of sucking pigs
                                toSendRandomPieceXL.remove(i);
                                i--;
                            }
                            continue;
                        }else{
                            if(!hasClientBitmapPiece(value,bitmapByte)){
                                toSendRandomPieceXL.remove(i);
                                return value;
                            }
                        }
                    }
                }
            }
        }else{// all pieces were once send or available by peers
            // what now?? wait for user refreshing the superseed or automtic switch to updateToSendList(true)
            // updateToSendList could causes in high cpu usage if the xor bitfield is empty (while forever on every next piece request)
            
            /*
             * keep for remembering
             * this is a good way to get on every request a new random list on the fly
             * but using a lot of cpu resources
             * for torrents with 2 - 3000 pieces and a leechercount less then 10 it might go
             * but for !our.isCompleted() it's fine.
             */
            if(autorefill || needed > 0){//this works even pretty good!
                java.util.List randList = new ArrayList();
                byte[] b = getXORSuperseedBitField().clone();
                for(int i = 0; i < b.length*8; i++){
                    if(i >= metainfo.getPieces())break;
                    int index = i/8;
                    int mask = 128 >> (i % 8);
                    
                    if((b[index] & mask) != 0 && !sendPieceXL.contains(""+i) && !xorPieceXL.contains("" + i) && !hasClientBitmapPiece(i,bitmapByte)){
                        randList.add(""+i);
                    }
                }
                if(randList.size() > 0){
                    Collections.shuffle(randList);
                    xorPieceXL.add(randList.get(0));
                    if(toSendRandomPieceXL.size() == 0){//should be if we here
                        synchronized(toSendRandomPieceXL){
                            toSendRandomPieceXL.addAll(xorPieceXL);
                            toSendRandomPieceXL.remove(0);
                        }
                    }
                    return Integer.valueOf((String)randList.get(0));
                }
            }
            if(true){
                if(!uploadNeedsRefresh && (toSendRandomPieceXL.size() == 0)){
                    uploadNeedsRefresh = true;
                    SnarkManager.instance().addMessage("Superseed: Uploadlist for '" + metainfo.getName() + "' is empty and needs a refresh!" );
                }
                
            }
            
            
            /*
             *
             */
            if(true)
                return -1;
        }
        
        //--- end of methode
        //---------------------------------- not in use ---
        /*
        if(false){
            boolean hasMorePieces = overflowPieceXL.size() > 0;
            //last pieces problem!
            synchronized (sendPieceXL){
                synchronized (xorPieceXL){
                    if(hasMorePieces && needed == 0){
                        for(int i = 0; i < overflowPieceXL.size(); i++){
                            int value = Integer.valueOf((String)overflowPieceXL.get(i));
                            if(!sendPieceXL.contains("" + value)){
                                if(!xorPieceXL.contains("" + value)){
                                    xorPieceXL.add("" + value);
                                    //sendPieceXL.add("" + value);
                                    return value;
                                }
                            }
                        }
                    }
         
                    byte[] b = getXORSuperseedBitField().clone();
                    for(int i = 0; i < b.length; i++){
                        int value = Integer.valueOf((String)randomPieceXL.get(i));//todo numberformat exception?
                        if(!sendPieceXL.contains(""+value) && !(value < 0) && !(value/8 >= b.length)&& !hasClientBitmapPiece(value,bitmapByte)){
                            if(!xorPieceXL.contains("" + value)){
                                int index = value/8;
                                int mask = 128 >> (value % 8);
                                if( (b[index] & mask) != 0) {
                                    //randomPieceXL.get(i);
                                    xorPieceXL.add(""+value);
                                    return value;
                                }
                            }
                        }
                    }
                    xorPieceXL.clear();
                }
            }
        }
         */
        return -1;
    }
    
    public void addNewPieceToSendList(int newPiece){
        if(!toSendRandomPieceXL.contains("" + newPiece)){
            toSendRandomPieceXL.add("" + newPiece);
            //_log.error("add new piece: " + newPiece);
        }
        
        
    }
    public boolean hasClientBitmapPiece(int rp, byte[] bitmapByte){
        if(rp == -1 || bitmapByte == null) return false;//todo throw exception
        if(!(rp < 0) && !(rp/8 >= bitmapByte.length))
            if(true){
            byte[] b = bitmapByte.clone();
            int index = rp/8;
            int mask = 128 >> (rp % 8);
            if( (b[index] & mask) != 0) {// client hasn't this piece, so we could send that we have the piece'
                return true;
            }
            
            }
        return false;
    }
/*
  private void orig_digestCreate() throws IOException {
    // Calculate piece_hashes
    MessageDigest digest = null;
    try
      {
        digest = MessageDigest.getInstance("SHA");
      }
    catch(NoSuchAlgorithmException nsa)
      {
        throw new InternalError(nsa.toString());
      }
 
    byte[] piece_hashes = metainfo.getPieceHashes();
 
    byte[] piece = new byte[piece_size];
    for (int i = 0; i < pieces; i++)
      {
        int length = getUncheckedPiece(i, piece);
        digest.update(piece, 0, length);
        byte[] hash = digest.digest();
        for (int j = 0; j < 20; j++)
          piece_hashes[20 * i + j] = hash[j];
 
        bitfield.set(i);
 
        if (listener != null)
          listener.storageChecked(this, i, true);
      }
 
    if (listener != null)
      listener.storageAllChecked(this);
 
    // Reannounce to force recalculating the info_hash.
    metainfo = metainfo.reannounce(metainfo.getAnnounce());
  }
 */
    
    private void fast_digestCreate() throws IOException {
        // Calculate piece_hashes
        SHA1 digest = new SHA1();
        
        byte[] piece_hashes = metainfo.getPieceHashes();
        
        byte[] piece = new byte[piece_size];
        for (int i = 0; i < pieces; i++) {
            int length = getUncheckedPiece(i, piece);
            digest.update(piece, 0, length);
            byte[] hash = digest.digest();
            for (int j = 0; j < 20; j++)
                piece_hashes[20 * i + j] = hash[j];
            
            bitfield.set(i);
            
            if (listener != null)
                listener.storageChecked(this, i, true);
        }
        
        if (listener != null)
            listener.storageAllChecked(this);
        
        // Reannounce to force recalculating the info_hash.
        metainfo = metainfo.reannounce(metainfo.getAnnounce());
    }
    
    private void getFiles(File base) throws IOException {
        ArrayList files = new ArrayList();
        addFiles(files, base);
        
        int size = files.size();
        names = new String[size];
        lengths = new long[size];
        rafs = new RandomAccessFile[size];
        
        int i = 0;
        Iterator it = files.iterator();
        while (it.hasNext()) {
            File f = (File)it.next();
            names[i] = f.getPath();
            if (base.isDirectory() && names[i].startsWith(base.getPath()))
                names[i] = names[i].substring(base.getPath().length() + 1);
            lengths[i] = f.length();
            rafs[i] = new RandomAccessFile(f, "r");
            i++;
        }
    }
    
    private static void addFiles(List l, File f) {
        if (!f.isDirectory())
            l.add(f);
        else {
            File[] files = f.listFiles();
            if (files == null) {
                Snark.debug("WARNING: Skipping '" + f
                        + "' not a normal file.", Snark.WARNING);
                return;
            }
            for (int i = 0; i < files.length; i++)
                addFiles(l, files[i]);
        }
    }
    
    /**
     * Returns the MetaInfo associated with this Storage.
     */
    public MetaInfo getMetaInfo() {
        return metainfo;
    }
    
    /**
     * How many pieces are still missing from this storage.
     */
//fwd toDo: rename this more java like as getNeeded()
    public int needed() {
        return needed;
    }
    
//fwd need some familary methods (rewrite java like methods) toDo check this for doublette and replace/remove all code java like!
    public int getCompleted() {
        BitField bitfield = getBitField();
        return bitfield.count();
    }
    
    /**
     * Whether or not this storage contains all pieces if the MetaInfo.
     */
//toDo: rename as isComplete()
    public boolean complete() {
        return needed == 0;
    }
    
    /**
     * The BitField that tells which pieces this storage contains.
     * Do not change this since this is the current state of the storage.
     */
    public BitField getBitField() {
        return bitfield;
    }
    
    /**
     * The BitField that tells which pieces we have send/upload.
     *
     */
//todo make own class for superseed and remove this
    public byte[] getSuperseedBitField() {
        return bitfieldSuperseed.clone();
    }
    
    /**
     * Creates (and/or checks) all files from the metainfo file list.
     */
    public void check(String rootDir) throws IOException {
        File base = new File(rootDir, filterName(metainfo.getName()));
        // look for saved bitfield and timestamp in the config file
        long savedTime = SnarkManager.instance().getSavedTorrentTime(metainfo);
        BitField savedBitField = SnarkManager.instance().getSavedTorrentBitField(metainfo);
        boolean useSavedBitField = savedTime > 0 && savedBitField != null;
        
        List files = metainfo.getFiles();
        if (files == null) {
            // Create base as file.
            Snark.debug("Creating/Checking file: " + base, Snark.NOTICE);
            if (!base.createNewFile() && !base.exists())
                throw new IOException("Could not create file " + base);
            
            lengths = new long[1];
            rafs = new RandomAccessFile[1];
            names = new String[1];
            lengths[0] = metainfo.getTotalLength();
            if (useSavedBitField) {
                long lm = base.lastModified();
                if (lm <= 0 || lm > savedTime)
                    useSavedBitField = false;
            }
            if (base.exists() && ((useSavedBitField && savedBitField.complete()) || !base.canWrite()))
                rafs[0] = new RandomAccessFile(base, "r");
            else
                rafs[0] = new RandomAccessFile(base, "rw");
            names[0] = base.getName();
        } else {
            // Create base as dir.
            Snark.debug("Creating/Checking directory: " + base, Snark.NOTICE);
            if (!base.mkdir() && !base.isDirectory())
                throw new IOException("Could not create directory " + base);
            
            List  ls = metainfo.getLengths();
            int size = files.size();
            long total = 0;
            lengths = new long[size];
            rafs = new RandomAccessFile[size];
            names = new String[size];
            for (int i = 0; i < size; i++) {
                File f = createFileFromNames(base, (List)files.get(i));
                lengths[i] = ((Long)ls.get(i)).longValue();
                total += lengths[i];
                if (useSavedBitField) {
                    long lm = base.lastModified();
                    if (lm <= 0 || lm > savedTime)
                        useSavedBitField = false;
                }
                if (f.exists() && ((useSavedBitField && savedBitField.complete()) || !f.canWrite()))
                    rafs[i] = new RandomAccessFile(f, "r");
                else
                    rafs[i] = new RandomAccessFile(f, "rw");
                names[i] = f.getName();
            }
            
            // Sanity check for metainfo file.
            long metalength = metainfo.getTotalLength();
            if (total != metalength)
                throw new IOException("File lengths do not add up "
                        + total + " != " + metalength);
        }
        if (useSavedBitField) {
            bitfield = savedBitField;
            needed = metainfo.getPieces() - bitfield.count();
            Snark.debug("Found saved state and files unchanged, skipping check", Snark.NOTICE);
        } else {
            checkCreateFiles();
            SnarkManager.instance().saveTorrentStatus(metainfo, bitfield);
        }
    }
    
    /**
     * Reopen the file descriptors for a restart
     * Do existence check but no length check or data reverification
     */
    public void reopen(String rootDir) throws IOException {
        File base = new File(rootDir, filterName(metainfo.getName()));
        
        List files = metainfo.getFiles();
        if (files == null) {
            // Reopen base as file.
            Snark.debug("Reopening file: " + base, Snark.NOTICE);
            if (!base.exists())
                throw new IOException("Could not reopen file " + base);
            
            if (complete() || !base.canWrite())  // hope we can get away with this, if we are only seeding...
                rafs[0] = new RandomAccessFile(base, "r");
            else
                rafs[0] = new RandomAccessFile(base, "rw");
        } else {
            // Reopen base as dir.
            Snark.debug("Reopening directory: " + base, Snark.NOTICE);
            if (!base.isDirectory())
                throw new IOException("Could not reopen directory " + base);
            
            int size = files.size();
            for (int i = 0; i < size; i++) {
                File f = getFileFromNames(base, (List)files.get(i));
                if (!f.exists())
                    throw new IOException("Could not reopen file " + f);
                if (complete() || !f.canWrite()) // see above re: only seeding
                    rafs[i] = new RandomAccessFile(f, "r");
                else
                    rafs[i] = new RandomAccessFile(f, "rw");
            }
            
        }
    }
    
    /**
     * Removes 'suspicious' characters from the give file name.
     */
    private static String filterName(String name) {
        // XXX - Is this enough?
        return name.replace(File.separatorChar, '_');
    }
    
    private File createFileFromNames(File base, List names) throws IOException {
        File f = null;
        Iterator it = names.iterator();
        while (it.hasNext()) {
            String name = filterName((String)it.next());
            if (it.hasNext()) {
                // Another dir in the hierarchy.
                f = new File(base, name);
                if (!f.mkdir() && !f.isDirectory())
                    throw new IOException("Could not create directory " + f);
                base = f;
            } else {
                // The final element (file) in the hierarchy.
                f = new File(base, name);
                if (!f.createNewFile() && !f.exists())
                    throw new IOException("Could not create file " + f);
            }
        }
        return f;
    }
    
    public static File getFileFromNames(File base, List names) {
        Iterator it = names.iterator();
        while (it.hasNext()) {
            String name = filterName((String)it.next());
            base = new File(base, name);
        }
        return base;
    }
    
    private void checkCreateFiles() throws IOException {
        // Whether we are resuming or not,
        // if any of the files already exists we assume we are resuming.
        boolean resume = false;
        
        // Make sure all files are available and of correct length
        for (int i = 0; i < rafs.length; i++) {
            long length = rafs[i].length();
            if(length == lengths[i]) {
                if (listener != null)
                    listener.storageAllocated(this, length);
                resume = true; // XXX Could dynamicly check
            } else if (length == 0)
                allocateFile(i);
            else {
                Snark.debug("File '" + names[i] + "' exists, but has wrong length - repairing corruption", Snark.ERROR);
                rafs[i].setLength(lengths[i]);
            }
        }
        
        // Check which pieces match and which don't
        if (resume) {
            pieces = metainfo.getPieces();
            byte[] piece = new byte[metainfo.getPieceLength(0)];
            for (int i = 0; i < pieces; i++) {
                int length = getUncheckedPiece(i, piece);
                boolean correctHash = metainfo.checkPiece(i, piece, 0, length);
                if (correctHash) {
                    bitfield.set(i);
                    needed--;
                }
                
                if (listener != null)
                    listener.storageChecked(this, i, correctHash);
            }
        }
        
        if (listener != null) {
            listener.storageAllChecked(this);
            if (needed <= 0)
                listener.storageCompleted(this);
        }
    }
    
    private void allocateFile(int nr) throws IOException {
        // XXX - Is this the best way to make sure we have enough space for
        // the whole file?
        listener.storageCreateFile(this, names[nr], lengths[nr]);
        final int ZEROBLOCKSIZE = metainfo.getPieceLength(0);
        byte[] zeros = new byte[ZEROBLOCKSIZE];
        int i;
        for (i = 0; i < lengths[nr]/ZEROBLOCKSIZE; i++) {
            rafs[nr].write(zeros);
            if (listener != null)
                listener.storageAllocated(this, ZEROBLOCKSIZE);
        }
        int size = (int)(lengths[nr] - i*ZEROBLOCKSIZE);
        rafs[nr].write(zeros, 0, size);
        if (listener != null)
            listener.storageAllocated(this, size);
    }
    
    
    /**
     * Closes the Storage and makes sure that all RandomAccessFiles are
     * closed. The Storage is unusable after this.
     */
    public void close() throws IOException {
        if (rafs == null) return;
        for (int i = 0; i < rafs.length; i++) {
            try {
                synchronized(rafs[i]) {
                    rafs[i].close();
                }
            } catch (IOException ioe) {
                I2PSnarkUtil.instance().debug("Error closing " + rafs[i], Snark.ERROR, ioe);
                // gobble gobble
            }
        }
        changed = false;
    }
    
    /**
     * Returns a byte array containing a portion of the requested piece or null if
     * the storage doesn't contain the piece yet.
     */
    public byte[] getPiece(int piece, int off, int len) throws IOException {
        if (!bitfield.get(piece))
            return null;
        
        //Catch a common place for OOMs esp. on 1MB pieces
        byte[] bs;
        try {
            bs = new byte[len];
        } catch (OutOfMemoryError oom) {
            I2PSnarkUtil.instance().debug("Out of memory, can't honor request for piece " + piece, Snark.WARNING, oom);
            return null;
        }
        getUncheckedPiece(piece, bs, off, len);
        return bs;
    }
    
    /**
     * Put the piece in the Storage if it is correct.
     *
     * @return true if the piece was correct (sha metainfo hash
     * matches), otherwise false.
     * @exception IOException when some storage related error occurs.
     */
    public boolean putPiece(int piece, byte[] ba) throws IOException {
        // First check if the piece is correct.
        // Copy the array first to be paranoid.
        byte[] bs = (byte[]) ba.clone();
        int length = bs.length;
        boolean correctHash = metainfo.checkPiece(piece, bs, 0, length);
        if (listener != null)
            listener.storageChecked(this, piece, correctHash);
        if (!correctHash)
            return false;
        
        boolean complete;
        synchronized(bitfield) {
            if (bitfield.get(piece))
                return true; // No need to store twice.
            else {
                bitfield.set(piece);
                needed--;
                complete = needed == 0;
            }
            
        }
        
        // Early typecast, avoid possibly overflowing a temp integer
        long start = (long) piece * (long) metainfo.getPieceLength(0);
        int i = 0;
        long raflen = lengths[i];
        while (start > raflen) {
            i++;
            start -= raflen;
            raflen = lengths[i];
        }
        
        int written = 0;
        int off = 0;
        while (written < length) {
            int need = length - written;
            int len = (start + need < raflen) ? need : (int)(raflen - start);
            synchronized(rafs[i]) {
                rafs[i].seek(start);
                rafs[i].write(bs, off + written, len);
            }
            written += len;
            if (need - len > 0) {
                i++;
                raflen = lengths[i];
                start = 0;
            }
        }
        
        changed = true;
        if (complete) {
//    listener.storageCompleted(this);
            // do we also need to close all of the files and reopen
            // them readonly?
            
            // Do a complete check to be sure.
            // Temporarily resets the 'needed' variable and 'bitfield', then call
            // checkCreateFiles() which will set 'needed' and 'bitfield'
            // and also call listener.storageCompleted() if the double-check
            // was successful.
            // Todo: set a listener variable so the web shows "checking" and don't
            // have the user panic when completed amount goes to zero temporarily?
            needed = metainfo.getPieces();
            bitfield = new BitField(needed);
            checkCreateFiles();
            if (needed > 0) {
                listener.setWantedPieces(this);
                Snark.debug("WARNING: Not really done, missing " + needed
                        + " pieces", Snark.WARNING);
            } else {
                SnarkManager.instance().saveTorrentStatus(metainfo, bitfield);
            }
        }
        
        return true;
    }
    
    private int getUncheckedPiece(int piece, byte[] bs)
    throws IOException {
        return getUncheckedPiece(piece, bs, 0, metainfo.getPieceLength(piece));
    }
    
    private int getUncheckedPiece(int piece, byte[] bs, int off, int length)
    throws IOException {
        // XXX - copy/paste code from putPiece().
        
        // Early typecast, avoid possibly overflowing a temp integer
        long start = ((long) piece * (long) metainfo.getPieceLength(0)) + off;
        
        int i = 0;
        long raflen = lengths[i];
        while (start > raflen) {
            i++;
            start -= raflen;
            raflen = lengths[i];
        }
        
        int read = 0;
        while (read < length) {
            int need = length - read;
            int len = (start + need < raflen) ? need : (int)(raflen - start);
            synchronized(rafs[i]) {
                rafs[i].seek(start);
                rafs[i].readFully(bs, read, len);
            }
            read += len;
            if (need - len > 0) {
                i++;
                raflen = lengths[i];
                start = 0;
            }
        }
        
        return length;
    }
}
