/*
 * BitfieldImage.java
 *
 Copyright (C) 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;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;// sort
import java.util.HashMap;
import java.util.Random;
import java.util.Set;// sort
import java.util.TreeSet;// sort
import java.util.Iterator;// sort

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.i2p.i2psnarkxl.peermanager.utils.BTPeerIDByteDecoder;
//import java.io.IOException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.Base64;
import net.i2p.data.Destination;

import org.klomp.snarkxl.MetaInfo;
import org.klomp.snarkxl.Peer;
import org.klomp.snarkxl.Snark;
//import org.klomp.snarkxl.PeerCoordinator;
import org.klomp.snarkxl.SnarkManager;

import net.i2p.I2PAppContext;
import net.i2p.util.Log;

public class BitfieldImage extends Thread implements Comparable, Runnable{
    private Log _log = new Log(BitfieldImage.class);
    private I2PAppContext _context;
    
    private static final BitfieldImage _instance = new BitfieldImage();
    public synchronized static final BitfieldImage instance() { return _instance; }
    
    public static final java.util.List bitfieldsXL = new ArrayList();
    private static ColorModel colormodel = generateColorModel();
    private boolean stop = true;
    // Identifying properties.
    
    private boolean updateIsRunning = false;
    private boolean needRefresh = false;
    
    private byte[] bufferedImageByte = null;
    MetaInfo metainfo;
    String metainfohash;
    private byte[] bitfieldByte = null;
    
    private Thread t;
    /** Creates a new instance of Bitfields */
    public BitfieldImage( Peer _peer){
        this.metainfo = _peer.getMetaInfo();
        metainfohash = Base64.encode(metainfo.getInfoHash()) ;
        
        if(_peer != null && metainfo != null)
            synchronized(instance().bitfieldsXL) {
            if(!instance().bitfieldsXL.contains(this)){
                bitfieldByte = _peer.getBitfield().clone();
                // add to head
                instance().bitfieldsXL.add(0,this);
                //start();
            }else{
                
                updateInstance(_peer.getBitfield());
            }
            }
    }
    
    /** Creates a new instance of Bitfields */
    public BitfieldImage( MetaInfo _metainfo, byte[] _bitfield){
        this.metainfo = _metainfo;
        metainfohash = Base64.encode(metainfo.getInfoHash()) ;
        
        if(_metainfo != null && _bitfield != null)
            synchronized(instance().bitfieldsXL) {
            if(!instance().bitfieldsXL.contains(this)){
                bitfieldByte =_bitfield.clone();
                // add to head
                instance().bitfieldsXL.add(0,this);
                //start();
            }else{
                
                updateInstance(_bitfield.clone());
            }
            }
    }
    
    /** Creates a new instance of BitfieldImage
     * is only called once to create the private _instance
     * private _instance is public reachable with BitfieldImage.instance()
     */
    
    private BitfieldImage(){
        _context = I2PAppContext.getGlobalContext();
        _log = _context.logManager().getLog(BitfieldImage.class);
        
        metainfo = null;
        metainfohash = null;
        stop = false;
        
    }
    
    /** Creates a new instance of BitfieldImage
     * is only called once to create the private _instance
     * private _instance is public reachable with BitfieldImage.instance()
     */
    
    public  BitfieldImage(String _metainfohash){
        metainfo = null;
        metainfohash = _metainfohash;
        stop = false;
        
    }
    
    private void updateInstance( byte[] b){
        //have to contains this! else we are not here -> (bitfieldsXL.indexOf(this))
        BitfieldImage bitfield = ((BitfieldImage)(instance().bitfieldsXL.get(instance().bitfieldsXL.indexOf(this))));
        if(bitfield != null && b != null ){
            if(bitfield.bitfieldByte != null && b.length == bitfield.bitfieldByte.length){
                if(bitfield.needRefresh && false){// some of our clients completed. we have to start with a new byte. nothing matches more
                    bitfield.bitfieldByte = b;
                    bitfield.needRefresh = false;
                }else{
                    byte[] a = new byte[bitfield.bitfieldByte.length];
                    for(int i=0;i<bitfield.bitfieldByte.length;i++){
                        a[i] = (byte)(bitfield.bitfieldByte[i] | b[i]);
                    }
                    bitfield.bitfieldByte = a.clone();
                    a = null;
                    try{
                        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                        ImageIO.write(getImageBar(bitfield.bitfieldByte ,1,1), "png", byteArrayOutputStream);
                        bitfield.bufferedImageByte = byteArrayOutputStream.toByteArray();
                        
                        //client.bufferedImage = client.getImageBar(_peer ,1,1);//1,1 dummies real size is in methode self
                    } catch (IOException ioe) {
                        // _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
                        //throw ioe;
                    }
                }
            }else{
                //log
            }
        }
    }
    
    //public void setBufferedImage(byte[] image){
    //    bufferedImage=image;
    //}
    /*
     * The vars in the BitfieldImage.class are not the exact current values of the app
     * we have to refresh them while running to become uptodate.
     
     */
    
    // not in use yet
    private void update(){
        // do something
        boolean error = false;
        synchronized(instance().bitfieldsXL) {
            if(instance().bitfieldsXL.size() > 0 && instance().bitfieldsXL.contains(this)){//user cleanup?; delete all? ; this
                BitfieldImage bitfield = ((BitfieldImage)(instance().bitfieldsXL.get(instance().bitfieldsXL.indexOf(this))));
                if(bitfield != null){
                    
                }else{
                    error = true;
                }
            }else{
                error = true;
            }
        }
        if(error){
            //halt();
        }
    }
    
    public void refresh( String metainfo_hash){
        if(metainfo_hash == null)return;
        //synchronized(instance().bitfieldsXL) {
            BitfieldImage bitfield = getBitfieldByMetainfoHash(metainfo_hash);
            if ( bitfield != null){
                if(instance().bitfieldsXL.contains(bitfield)){// have to be else we are not here
                    //synchronized(Client.instance().clientsXL) {
                        if(Client.instance().clientsXL.size() > 0){
                            byte[] b = new byte[bitfield.bitfieldByte.length];
                            byte[] a = new byte[bitfield.bitfieldByte.length];
                            for(int i = 0;i<Client.instance().clientsXL.size();i++){
                                Client client = (Client) (Client.instance().clientsXL.get(i));
                                if(client != null && client.isConnected() && !client.isCompleted() && client.getClientMetaInfoHash().equals(metainfo_hash) && client.getBitmap() != null){
                                    byte[] c =  client.getBitmap();
                                    if(c != null && c.length == b.length ){
                                        for(int j=0;j<b.length;j++){
                                            a[j] = (byte)( b[j] | c[j] );
                                            
                                        }
                                        b = a.clone();
                                    }
                                    
                                }
                            }
                            // ************************************************* toDo make a methode and a ping that refresh when our bitfield get new pieces
                            if(true){//add our own bitfield
                                SnarkManager _manager = _manager = SnarkManager.instance();
                                Snark snark = _manager.getTorrentByMetaInfoHash(metainfo_hash);
                                boolean ourIsCompleted = false;
                                byte[] ourBitfieldByte = null;
                                if(snark != null && snark.storage != null && snark.storage.complete()){
                                    ourIsCompleted = true;
                                }else if(snark != null && snark.coordinator != null && snark.coordinator.isCompleted()){
                                    ourIsCompleted = true;
                                }else{
                                    ourBitfieldByte = _manager.getTorrentBitFieldByteByMetaInfoHash(metainfo_hash);// our own bitfield
                                    
                                }
                                if(!ourIsCompleted & ourBitfieldByte != null && ourBitfieldByte.length == bitfield.bitfieldByte.length ){
                                    byte[] c =  ourBitfieldByte;
                                    if(c != null && c.length == b.length ){
                                        for(int j=0;j<b.length;j++){
                                            a[j] = (byte)( b[j] | c[j] );
                                            
                                        }
                                        b = a;
                                    }
                                    
                                }
                            }
                            // *************************************************
                            if(a != null){
                                try{
                                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                                    ImageIO.write(getImageBar(a ,1,1), "png", byteArrayOutputStream);
                                    bitfield.bufferedImageByte = byteArrayOutputStream.toByteArray();
                                    //client.bufferedImage = client.getImageBar(_peer ,1,1);//1,1 dummies real size is in methode self
                                } catch (IOException ioe) {
                                    // _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
                                    //throw ioe;
                                    a = null;
                                    b= null;
                                    return;//todo log
                                }
                                bitfield.bitfieldByte = a.clone();
                                a = null;
                                b= null;
                            }
                        }
                    //}
                    bitfield.needRefresh = true;
                }
            }
        //}
    }
    
    
    private byte[] getRefreshWithoutUs( String metainfo_hash){
        if(metainfo_hash == null)return null;
        //synchronized(instance().bitfieldsXL) {
            BitfieldImage bitfield = getBitfieldByMetainfoHash(metainfo_hash);
            if ( bitfield != null){
                if(instance().bitfieldsXL.contains(bitfield)){// have to be else we are not here
                    //synchronized(Client.instance().clientsXL) {
                        if(Client.instance().clientsXL.size() > 0){
                            byte[] b = new byte[bitfield.bitfieldByte.length];
                            byte[] a = new byte[bitfield.bitfieldByte.length];
                            for(int i = 0;i<Client.instance().clientsXL.size();i++){
                                Client client = (Client) (Client.instance().clientsXL.get(i));
                                if(client != null && client.isConnected() && !client.isCompleted() && client.getClientMetaInfoHash().equals(metainfo_hash) && client.getBitmap() != null ){
                                    byte[] c =  client.getBitmap();
                                    if(c != null && c.length == b.length ){
                                        for(int j=0;j<b.length;j++){
                                            a[j] = (byte)( b[j] | c[j] );
                                            
                                        }
                                        b = a.clone();
                                    }
                                    
                                }
                            }
                            // ************************************************* toDo make a methode and a ping that refresh when our bitfield get new pieces
                           /* if(true){//add our own bitfield
                                SnarkManager _manager = _manager = SnarkManager.instance();
                                Snark snark = _manager.getTorrentByMetaInfoHash(metainfo_hash);
                                boolean ourIsCompleted = false;
                                byte[] ourBitfieldByte = null;
                                if(snark != null && snark.storage != null && snark.storage.complete()){
                                    ourIsCompleted = true;
                                }else if(snark != null && snark.coordinator != null && snark.coordinator.isCompleted()){
                                    ourIsCompleted = true;
                                }else{
                                    ourBitfieldByte = _manager.getTorrentBitFieldByteByMetaInfoHash(metainfo_hash);// our own bitfield
                            
                                }
                                if(!ourIsCompleted & ourBitfieldByte != null && ourBitfieldByte.length == bitfield.bitfieldByte.length ){
                                    byte[] c =  ourBitfieldByte;
                                    if(c != null && c.length == b.length ){
                                        for(int j=0;j<b.length;j++){
                                            a[j] = (byte)( b[j] | c[j] );
                            
                                        }
                                        b = a;
                                    }
                            
                                }
                            }*/
                            // *************************************************
                            if(a != null){
                             /*   try{
                                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                                    ImageIO.write(getImageBar(a ,1,1), "png", byteArrayOutputStream);
                                    bitfield.bufferedImageByte = byteArrayOutputStream.toByteArray();
                                    //client.bufferedImage = client.getImageBar(_peer ,1,1);//1,1 dummies real size is in methode self
                                } catch (IOException ioe) {
                                    // _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64());
                                    //throw ioe;
                                    a = null;
                                    b= null;
                                    return;//todo log
                                }*/
                                //bitfield.bitfieldByte = a.clone();
                                
                                return  a.clone();
                            }
                        }
                    //}
                    bitfield.needRefresh = true;
                }
            }
        //}
        return null;
    }
    
// methode need not to be syncronized, we do nothing important here with the objects! have not to be thread safe.
    public BitfieldImage getBitfieldByMetainfoHash( String metainfo_hash){
        //boolean test = true;//dummy
        BitfieldImage bitfield;
        if(metainfo_hash != null){
            //if(cur.getPeerID().getAddress() !=null){// need for compare/equals
            
            synchronized(instance().bitfieldsXL) {
                bitfield = new BitfieldImage( metainfo_hash);//NEVER,NEVER,NEVER return this Object! just compare it!
                if(instance().bitfieldsXL.contains(bitfield)){
                    int index = instance().bitfieldsXL.indexOf(bitfield);
                    bitfield = null;
                    return (BitfieldImage)instance().bitfieldsXL.get(index);
                }
            }
        }
        bitfield = null;
        return null;
    }
    
    private boolean isUpdateRunning(){
        return updateIsRunning;
    }
    
    
    /*
     * remove the client from the Clientslist
     * the thread care about itself to cleanup and stopping
     * toDo: just check if the global threads count truly decrease. Murphy?!
     * not in use yet
     */
    private void removeFromList(){
        int weakup = 0;
        while(isUpdateRunning() ){//toDo make a methode
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e2) {
                e2.printStackTrace();
            }
            weakup ++;
            /*
             * we don't want stay forever
             */
            if(weakup >= 10)break;// one sec for a update should be more than enough toDo: log warn, something goes wrong
        }
        instance().bitfieldsXL.remove(this);
    }
    
    /**
     * Two Bitfields are equal when they have the same torrent.
     *
     */
    public boolean equals(Object o) {
        BitfieldImage p = (BitfieldImage)o;
        
        boolean isTorrent = metainfohash.equals(p.metainfohash);
        if(isTorrent){
            return true;
        }else{
            return false;
        }
    }
    /**
     * Compares the BitfieldImage's.
     */
    public int compareTo(Object o) {
        BitfieldImage p = (BitfieldImage)o;
        
        boolean isTorrent = metainfohash.equals(p.metainfohash);
        if( isTorrent){
            return 0;
        }else{
            return 1;
        }
    }
    //toDo this tooks to long for a on the fly request, make a update thread for our own bitfield and deliver a static picture byte[]
    public int getCompletedPiecesByMetainfoHash(String metainfo_hash){
        BitfieldImage bit_field = getBitfieldByMetainfoHash(metainfo_hash);
        if(bit_field == null)return 0;
        int count = 0;
        int index = 0;
        int mask = 0;
        for(int i = 0; i<bit_field.bitfieldByte.length*8;i++){
            index = i/8;
            mask = 128 >> (i % 8);
            if((bit_field.bitfieldByte[index] & mask) != 0)
                count++;
        }
        return count;
    }
    
    
    public byte[] getAvailableImageBarByte(){
      /*  byte[] bufImageByte = null;
        synchronized(instance().bitfieldsXL) {
                if(instance().bitfieldsXL.contains(this)){
                    bufImageByte =((BitfieldImage) instance().bitfieldsXL.get(instance().bitfieldsXL.indexOf(this))).bufferedImage;
                }
            }*/
        if(bufferedImageByte == null)return null;
        return bufferedImageByte.clone();
    }
    
    public byte[] getAvailableBitfieldByteByMetainfoHash(String metainfo_hash){
//if we are incomplete it's not yet without our own! todo: methode without our own! that's more effective for superseeding' * DONE
        if(false){
            BitfieldImage bitfield;
            if(metainfo_hash != null){
                //if(cur.getPeerID().getAddress() !=null){// need for compare/equals
                
                synchronized(instance().bitfieldsXL) {
                    bitfield = new BitfieldImage( metainfo_hash);//NEVER,NEVER,NEVER return this Object! just compare it!
                    if(instance().bitfieldsXL.contains(bitfield)){
                        int index = instance().bitfieldsXL.indexOf(bitfield);
                        bitfield = null;
                        return ((BitfieldImage)instance().bitfieldsXL.get(index)).bitfieldByte;
                    }
                }
            }
        }else{//todo: DONE
            if(metainfo_hash != null){
                return getRefreshWithoutUs(metainfo_hash).clone();
            }
        }
        return null;
    }
    
    private BufferedImage getImageBarNull(int width, int height){
        
        BufferedImage bufImage= new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        
        Graphics2D g2d = bufImage.createGraphics();
        //if(isCompleted()){
        //    g2d.setColor(Color.blue);
        //}else
        g2d.setColor(Color.white);
        
        g2d.fill(new Rectangle2D.Float(0, 0, bufImage.getWidth(), bufImage.getHeight()));
        g2d.dispose();
        //g2.setBackground(Color.blue);
        return bufImage;// Image Toolkit.getDefaultToolkit().createImage(bufImage.getSource());
    }
    
    private BufferedImage getImageBar(byte[] b, int width, int height){
        //GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        //GraphicsDevice gs = ge.getDefaultScreenDevice();
        //GraphicsConfiguration gc = gs.getDefaultConfiguration();
        
        //BufferedImage bufferedImage = gc.createCompatibleImage(width, height, Transparency.BITMASK);
        //BufferedImage bufferedImage = gc.createCompatibleImage(width, height, Transparency.OPAQUE);
        // Create a buffered image that supports transparency
        
        //byte[] pixels = new byte[w * h];
        // Generate the pixel data;
        //byte[] pixels = generatePixels(width, height, loc);
        // Create a data buffer using the byte buffer of pixel data.
        // The pixel data is not copied; the data buffer uses the byte buffer array.
        // DataBuffer dbuf = new DataBufferByte(pixels, width*height, 0);
        // The number of banks should be 1
        //    int numBanks = dbuf.getNumBanks(); // 1
        // Prepare a sample model that specifies a storage 4-bits of
        // pixel datavd in an 8-bit data element
        //int bitMasks[] = new int[]{(byte)0xf};
        //SampleModel sampleModel = new SinglePixelPackedSampleModel(
        //   DataBuffer.TYPE_BYTE, width, height, bitMasks);
        
        // Create a raster using the sample model and data buffer
        //WritableRaster raster = Raster.createWritableRaster(sampleModel, dbuf, null);
        
        // Combine the color model and raster into a buffered image
        // image = new BufferedImage(colorModel, raster, false, null);//new java.util.Hashtable());
        if(b == null)return getImageBarNull(width , height);
        
        byte[] pixels = b;
        DataBuffer dbuf = new DataBufferByte(pixels, pixels.length*1, 0);
        int numBanks = dbuf.getNumBanks(); // 1
        int bitMasks[] = new int[]{(byte)0xf};//0xf
        SampleModel sampleModel = new SinglePixelPackedSampleModel(
                DataBuffer.TYPE_BYTE, pixels.length*1, 1, bitMasks);
        WritableRaster raster = Raster.createWritableRaster(sampleModel, dbuf, null);
        
        BufferedImage bufImage = new BufferedImage(colormodel, raster, false, null);//new java.util.Hashtable());
        
        /*
        Graphics2D g2d = bufImage.createGraphics();
        g2d.setColor(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());
    }
    
    //same methode like above but need it to be threadsafe
    
    public BufferedImage getOurImageBar(byte[] b){
        
        if(b == null)return getImageBarNull(1 , 1);
        
        byte[] pixels = b;
        DataBuffer dbuf = new DataBufferByte(pixels, pixels.length*1, 0);
        int numBanks = dbuf.getNumBanks(); // 1
        int bitMasks[] = new int[]{(byte)0xf};//0xf
        SampleModel sampleModel = new SinglePixelPackedSampleModel(
                DataBuffer.TYPE_BYTE, pixels.length*1, 1, bitMasks);
        WritableRaster raster = Raster.createWritableRaster(sampleModel, dbuf, null);
        
        BufferedImage bufImage = new BufferedImage(colormodel, raster, false, null);//new java.util.Hashtable());
        
        
        return bufImage;//
    }
    //same methode like above but need it to be threadsafe
    
    public BufferedImage getOurImageBarSuperseed(byte[] b){
        
        if(b == null)return getImageBarNull(1 , 1);
        
        byte[] pixels = b;
        DataBuffer dbuf = new DataBufferByte(pixels, pixels.length*1, 0);
        int numBanks = dbuf.getNumBanks(); // 1
        int bitMasks[] = new int[]{(byte)0xf};//0xf
        SampleModel sampleModel = new SinglePixelPackedSampleModel(
                DataBuffer.TYPE_BYTE, pixels.length*1, 1, bitMasks);
        WritableRaster raster = Raster.createWritableRaster(sampleModel, dbuf, null);
        
        BufferedImage bufImage = new BufferedImage(colormodel, raster, false, null);//new java.util.Hashtable());
        
        
        return bufImage;//
    }
    //same methode like above but need it to be threadsafe
    
    public BufferedImage getOurImageBarSuperseedXOR(byte[] b){
        
        if(b == null)return getImageBarNull(1 , 1);
        
        byte[] pixels = b;
        DataBuffer dbuf = new DataBufferByte(pixels, pixels.length*1, 0);
        int numBanks = dbuf.getNumBanks(); // 1
        int bitMasks[] = new int[]{(byte)0xf};//0xf
        SampleModel sampleModel = new SinglePixelPackedSampleModel(
                DataBuffer.TYPE_BYTE, pixels.length*1, 1, bitMasks);
        WritableRaster raster = Raster.createWritableRaster(sampleModel, dbuf, null);
        
        BufferedImage bufImage = new BufferedImage(colormodel, raster, false, null);//new java.util.Hashtable());
        
        
        return bufImage;//
    }
    
    
    private static final ColorModel generateColorModel() {
        // Generate 16-color model
        byte[] r = new byte[9];
        byte[] g = new byte[9];
        byte[] b = new byte[9];
        
        b[8] =  (byte)180;
        b[7] =  (byte)190;
        b[6] =  (byte)200;
        b[5] =  (byte)210;
        b[4] =  (byte)220;
        b[3] =  (byte)230;
        b[2] =  (byte)240;
        b[1] =  (byte)250;
        b[0] =  (byte)254;
        
        g[8] =  (byte)110;
        g[7] =  (byte)120;
        g[6] =  (byte)140;
        g[5] =  (byte)160;
        g[4] =  (byte)180;
        g[3] =  (byte)200;
        g[2] =  (byte)220;
        g[1] =  (byte)240;
        g[0] =  (byte)254;
        
        r[8] =  (byte)50;
        r[7] =  (byte)60;
        r[6] =  (byte)90;
        r[5] =  (byte)130;
        r[4] =  (byte)160;
        r[3] =  (byte)180;
        r[2] =  (byte)210;
        r[1] =  (byte)240;
        r[0] =  (byte)254;
        
        
/*        r[8] =  (byte)128; g[8] =  (byte)128; b[8] =  (byte)0;
        r[9] =  (byte)144; g[9] =  (byte)144; b[9] =  (byte)0;
        r[10] = (byte)160; g[10] = (byte)160; b[10] = (byte)0;
        r[11] = (byte)176; g[11] = (byte)176; b[11] = (byte)0;
        r[12] = (byte)192; g[12] = (byte)192; b[12] = (byte)0;
        r[13] = (byte)208; g[13] = (byte)208; b[13] = (byte)0;
        r[14] = (byte)226; g[14] = (byte)226; b[14] = (byte)0;
        r[15] = (byte)242; g[15] = (byte)242; b[15] = (byte)0;
 */
        return new IndexColorModel(1, 9, r, g, b);
    }
}
