package jelectrum;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Block;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.core.StoredBlock;
import org.bitcoinj.core.Transaction;
import java.util.concurrent.atomic.AtomicInteger;
import java.text.DecimalFormat;

import java.util.Collection;
import java.util.Set;

public class BlockMadScan
{
    public static void main(String args[]) throws Exception
    {
        new BlockMadScan(new Config(args[0]));

    }

    private Jelectrum jelly;
    private LinkedBlockingQueue<Sha256Hash> queue;
    private Semaphore sem;
    private AtomicInteger blocks_scanned;
    private AtomicInteger transactions_scanned;
    private String rescan_operation="zing-" + System.currentTimeMillis();
    private TXUtil tx_util;

    private LRUCache<String, Set<Sha256Hash> > addr_to_tx_cache;

    public BlockMadScan(Config config)
        throws Exception
    {
        jelly = new Jelectrum(config);

        tx_util = jelly.getDB().getTXUtil();
        queue = new LinkedBlockingQueue<Sha256Hash>();
        sem = new Semaphore(0);
        blocks_scanned = new AtomicInteger(0);
        transactions_scanned = new AtomicInteger(0);
        addr_to_tx_cache = new LRUCache<String, Set<Sha256Hash> >(250000);


        BlockStore block_store = jelly.getBlockStore();

        //StoredBlock head = block_store.getChainHead();

        //StoredBlock curr_block = head;

        //Sha256Hash genisis_hash = jelly.getNetworkParameters().getGenesisBlock().getHash();
        int queued=0;

        new RateThread("1-minute", 60000L).start();
        new RateThread("5-minute", 60000L * 5L).start();
        new RateThread("1-hour", 60000L * 60L).start();

        for(int i=0; i<500; i++)
        {
            new WorkerThread().start();
        }

        int h =0;
        while(true)
        {
            Sha256Hash curr_hash = jelly.getBlockChainCache().getBlockHashAtHeight(h);
            if (curr_hash == null) break;
            queue.put(curr_hash);
            queued++;
            h++;
            
            if  (h % 10000 == 0)
            {
                System.out.println("Block: " + h);

            }
        }

        /*while(true)
        {
            Sha256Hash curr_hash = curr_block.getHeader().getHash();
            queue.put(curr_hash);

            if  (curr_block.getHeight() % 10000 == 0)
            {
                System.out.println("Block: " + curr_block.getHeight());
            }
            queued++;

            if (curr_hash.equals(genisis_hash)) break;

            curr_block = curr_block.getPrev(block_store);

        }*/
        System.out.println("Enqueued " + queued + " blocks");

        while(queued>0)
        {
            sem.acquire();
            queued--;
            if (queued % 1000 == 0) System.out.println("" + queued + " blocks left");
        }





    }

    public Set<Sha256Hash> getAddressToTxSet(String addr)
    {
      /*synchronized(addr_to_tx_cache)
      {
        if (addr_to_tx_cache.containsKey(addr)) return addr_to_tx_cache.get(addr);
      }*/
      Set<Sha256Hash> set = jelly.getDB().getAddressToTxSet(addr);
      /*synchronized(addr_to_tx_cache)
      {
        addr_to_tx_cache.put(addr, set);
      }*/
      return set;
    }

    public class WorkerThread extends Thread
    {
        public WorkerThread()
        {
            setName("WorkerThread");
            setDaemon(true);
        }
        public void run()
        {
            while(true)
            {
                Sha256Hash blk_hash = null;
                try
                {
                    blk_hash = queue.take();
                    String op = jelly.getDB().getBlockRescanMap().get(blk_hash);
                    if ((op == null) || (!op.equals(rescan_operation)))
                    {
                        Block blk = jelly.getDB().getBlockMap().get(blk_hash).getBlock(jelly.getNetworkParameters());
                        for(Transaction tx : blk.getTransactions())
                        {
                          SerializedTransaction tx2 = jelly.getDB().getTransactionMap().get(tx.getHash());
                          if (tx2 == null)
                          {
                            System.out.println("Transaction map does not contain " + tx.getHash());
                            System.exit(-1);
                          }
                          Collection<String> addresses = tx_util.getAllAddresses(tx, true, null);
                          for(String addr : addresses)
                          {
                            Set<Sha256Hash> tx_set = getAddressToTxSet(addr);
                            if (!tx_set.contains(tx.getHash()))
                            {
                              System.out.println("Address list for " + addr + " does not contain " + tx.getHash());
                              System.out.println(tx_set);
                              System.exit(-1);
                            }
                          }

                          Set<Sha256Hash> blk_set = jelly.getDB().getTxToBlockMap(tx.getHash());
                          if (!blk_set.contains(blk.getHash()))
                          {
                            System.out.println("TX Block list doesn't contain " + blk.getHash() + " for " + tx.getHash());
                            System.exit(-1);
                          }


                          transactions_scanned.incrementAndGet();
                        }
                        jelly.getDB().getBlockRescanMap().put(blk_hash, rescan_operation);
                    }
                    sem.release(1);
                    blocks_scanned.incrementAndGet();


                }
                catch(Throwable t)
                {
                    t.printStackTrace();
                    if (blk_hash != null) queue.offer(blk_hash);
                }
            }

        }


    }


    public class RateThread extends Thread
    {
        private long delay;
        private String name;

        public RateThread(String name, long delay)
        {
            this.name = name;
            this.delay = delay;
            setDaemon(true);
            setName("BlockMadScane/RateThread/"+name);

        }

        public void run()
        {
            long transactions = 0;
            long blocks = 0;
            long last_run = System.currentTimeMillis();
            DecimalFormat df =new DecimalFormat("0.000");
            while(true)
            {
                try{Thread.sleep(delay);}catch(Exception e){}

                long now = System.currentTimeMillis();
                long blocks_now = blocks_scanned.get();
                long transactions_now = transactions_scanned.get();

                double sec = (now - last_run) / 1000.0;

                double block_rate = (blocks_now - blocks) / sec;
                double tx_rate = (transactions_now - transactions) / sec;

                System.out.println(name + " Block rate: " + df.format(block_rate) + "/s   Transaction rate: " + df.format(tx_rate) + "/s");

                blocks = blocks_now;
                transactions = transactions_now;
                last_run= now;





            }

        }

    }


}