package org.klomp.snark;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import net.i2p.data.DataHelper;
import net.i2p.util.RandomSource;

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

/**
 * Simple state for the download of the metainfo, shared between
 * Peer and ExtensionHandler.
 *
 * Nothing is synchronized here!
 * Caller must synchronize on this for everything!
 *
 * Reference: BEP 9
 *
 * @since 0.8.4
 * author zzz
 */
class MagnetState {
    public static final int CHUNK_SIZE = 16*1024;

    private final byte[] infohash;
    private boolean complete;
    /** if false, nothing below is valid */
    private boolean isInitialized;

    private int metaSize;
    private int totalChunks;
    /** bitfield for the metainfo chunks - will remain null if we start out complete */
    private BitField requested;
    private BitField have;
    /** bitfield for the metainfo */
    private byte[] metainfoBytes;
    /** only valid when finished */
    private MetaInfo metainfo;

    /**
     *  @param meta null for new magnet
     */
    public MagnetState(byte[] iHash, MetaInfo meta) {
        infohash = iHash;
        if (meta != null) {
            metainfo = meta;
            initialize(meta.getInfoBytesLength());
            complete = true;
        }
    }

    /**
     *  Call this for a new magnet when you have the size
     *  @throws IllegalArgumentException
     */
    public void initialize(int size) {
        if (isInitialized)
            throw new IllegalArgumentException("already set");
        isInitialized = true;
        metaSize = size;
        totalChunks = (size + (CHUNK_SIZE - 1)) / CHUNK_SIZE;
        if (metainfo != null) {
            metainfoBytes = metainfo.getInfoBytes();
        } else {
            // we don't need these if complete
            have = new BitField(totalChunks);
            requested = new BitField(totalChunks);
            metainfoBytes = new byte[metaSize];
        }
    }

    /**
     *  Call this for a new magnet when the download is complete.
     *  @throws IllegalArgumentException
     */
    public void setMetaInfo(MetaInfo meta) {
        metainfo = meta;
    }

    /**
     *  @throws IllegalArgumentException
     */
    public MetaInfo getMetaInfo() {
        if (!complete)
            throw new IllegalArgumentException("not complete");
        return metainfo;
    }

    /**
     *  @throws IllegalArgumentException
     */
    public int getSize() {
        if (!isInitialized)
            throw new IllegalArgumentException("not initialized");
        return metaSize;
    }

    public boolean isInitialized() {
        return isInitialized;
    }

    public boolean isComplete() {
        return complete;
    }

    public int chunkSize(int chunk) {
        return Math.min(CHUNK_SIZE, metaSize - (chunk * CHUNK_SIZE));
    }

    /** @return chunk count */
    public int chunksRemaining() {
        if (!isInitialized)
            throw new IllegalArgumentException("not initialized");
        if (complete)
            return 0;
        return totalChunks - have.count();
    }

    /** @return chunk number */
    public int getNextRequest() {
        if (!isInitialized)
            throw new IllegalArgumentException("not initialized");
        if (complete)
            throw new IllegalArgumentException("complete");
        int rand = RandomSource.getInstance().nextInt(totalChunks);
        for (int i = 0; i < totalChunks; i++) {
            int chk = (i + rand) % totalChunks; 
            if (!(have.get(chk) || requested.get(chk))) {
                requested.set(chk);
                return chk; 
            }
        }
        // all requested - end game
        for (int i = 0; i < totalChunks; i++) {
            int chk = (i + rand) % totalChunks; 
            if (!have.get(chk))
                return chk; 
        }
        throw new IllegalArgumentException("complete");
    }

    /**
     *  @throws IllegalArgumentException
     */
    public byte[] getChunk(int chunk) {
        if (!complete)
            throw new IllegalArgumentException("not complete");
        if (chunk < 0 || chunk >= totalChunks)
            throw new IllegalArgumentException("bad chunk number");
        int size = chunkSize(chunk);
        byte[] rv = new byte[size];
        System.arraycopy(metainfoBytes, chunk * CHUNK_SIZE, rv, 0, size);
        // use meta.getInfoBytes() so we don't save it in memory
        return rv;
    }

    /**
     *  @return true if this was the last piece
     *  @throws NullPointerException IllegalArgumentException, IOException, ...
     */
    public boolean saveChunk(int chunk, byte[] data, int off, int length) throws Exception {
        if (!isInitialized)
            throw new IllegalArgumentException("not initialized");
        if (chunk < 0 || chunk >= totalChunks)
            throw new IllegalArgumentException("bad chunk number");
        if (have.get(chunk))
            return false;  // shouldn't happen if synced
        int size = chunkSize(chunk);
        if (size != length)
            throw new IllegalArgumentException("bad chunk length");
        System.arraycopy(data, off, metainfoBytes, chunk * CHUNK_SIZE, size);
        have.set(chunk);
        boolean done = have.complete();
        if (done) {
            metainfo = buildMetaInfo();
            complete = true;
        }
        return done;
    }

    /**
     *  @return true if this was the last piece
     *  @throws NullPointerException IllegalArgumentException, IOException, ...
     */
    private MetaInfo buildMetaInfo() throws Exception {
        // top map has nothing in it but the info map (no announce)
        Map<String, BEValue> map = new HashMap<String, BEValue>();
        InputStream is = new ByteArrayInputStream(metainfoBytes);
        BDecoder dec = new BDecoder(is);
        BEValue bev = dec.bdecodeMap();
        map.put("info", bev);
        MetaInfo newmeta = new MetaInfo(map);
        if (!DataHelper.eq(newmeta.getInfoHash(), infohash)) {
            // Disaster. Start over. ExtensionHandler will catch
            // the IOE and disconnect the peer, hopefully we will
            // find a new peer.
            // TODO: Count fails and give up eventually
            have = new BitField(totalChunks);
            requested = new BitField(totalChunks);
            throw new IOException("info hash mismatch");
        }
        return newmeta;
    }
}
