package focusedCrawler.util.persistence.rocksdb;

import java.io.Closeable;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.rocksdb.RocksIterator;

import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;

import focusedCrawler.util.persistence.HashtableDb;
import focusedCrawler.util.persistence.Tuple;
import focusedCrawler.util.persistence.TupleIterator;

public class RocksDBHashtable<T> implements HashtableDb<T>, Closeable {
    
    private static final ThreadLocal<Kryo> KRYOS = new ThreadLocal<Kryo>() {
        protected Kryo initialValue() {
            Kryo kryo = new Kryo();
            return kryo;
        };
    };
    
    private Class<T> contentClass;
    private Options options;
    private RocksDB db;
    
    static {
        RocksDB.loadLibrary();
    }
    
    public RocksDBHashtable(String path, Class<T> contentClass) {
        this.contentClass = contentClass;
        this.options = new Options();
        this.options.setCreateIfMissing(true);
        try {
          this.db = RocksDB.open(options, path);
        } catch (RocksDBException e) {
            throw new RuntimeException("Failed to open database at "+path, e);
        }
    }

    @Override
    public void put(List<Tuple<T>> tuples) {
        if(tuples == null || tuples.size() == 0){
            return;
        }
        for(Tuple<T> tuple : tuples) {
            put(tuple.getKey(), tuple.getValue());
        }
    }

    @Override
    public void put(String key, T value) {
        try {
            byte[] valueBytes = serializeObject(value);
            db.put(key.getBytes(), valueBytes);
        } catch (RocksDBException e) {
            throw new RuntimeException("failed to write value to database: "+key);
        }
    }

    private byte[] serializeObject(T value) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Output output = new Output(baos);
        KRYOS.get().writeObject(output, value);
        output.flush();
        return baos.toByteArray();
    }
    
    private T unserializeObject(byte[] value) {
        Input input = new Input(value);
        return KRYOS.get().readObject(input, contentClass);
    }
    

    @Override
    public List<Tuple<T>> listElements() {
        List<Tuple<T>> items = new ArrayList<>();;
        try(RocksDBIterator it = new RocksDBIterator()) {
            while(it.hasNext()) {
                items.add(it.next());
            }
            return items;
        }
    }

    @Override
    public T get(String key) {
        byte[] bytes = key.getBytes();
        try {
            byte[] value = db.get(bytes);
            if(value == null) {
                return null;
            }
            return unserializeObject(value);
        } catch (RocksDBException e) {
            throw new RuntimeException("Failed to get value from database for key: "+key, e);
        }
    }

    @Override
    public void close() {
        synchronized(db) {
            if (db != null) {
                db.close();
                db = null;
                options.close();
                options = null;
            }
        }
    }

    @Override
    public TupleIterator<T> iterator() {
        return new RocksDBIterator();
    }
    
    public class RocksDBIterator implements TupleIterator<T>, Closeable {

        final private RocksIterator cursor;
        private boolean hasNext;
        private boolean isOpen;
        private byte[] value;
        private byte[] key;

        public RocksDBIterator() {
            this.cursor = db.newIterator();
            this.cursor.seekToFirst();
            this.isOpen = true;
            readNextTuple(true);
        }

        private void readNextTuple(boolean firstEntry) {
            if(!firstEntry) {
                cursor.next();
            }
            if(cursor.isValid()) {
                this.hasNext = true;
                this.key = cursor.key();
                this.value = cursor.value();
            } else {
                this.close();
            }
        }

        @Override
        public void close() {
            if(this.isOpen) {
                cursor.close();
                this.isOpen = false;
                this.hasNext = false;
            }
        }

        @Override
        public boolean hasNext() {
            return hasNext;
        }

        @Override
        public Tuple<T> next() {
            if (!hasNext) {
                return null;
            }
            T value = unserializeObject(this.value);
            Tuple<T> tuple = new Tuple<T>(new String(key), value);
            readNextTuple(false);
            return tuple;
        }
        
        public void remove() {
            throw new UnsupportedOperationException("remove() not yet supported by "+getClass().getName());
        }

    }

}

@@ -3119,32 +3119,45 @@
 ride%0A    public 
+synchronized 
 void close() %7B%0A 
@@ -3159,39 +3159,8 @@
 ) %7B%0A
-        synchronized(db) %7B%0A    
     
@@ -3173,36 +3173,32 @@
  (db != null) %7B%0A
-    
             db.c
@@ -3217,20 +3217,16 @@
         
-    
 db = nul
@@ -3220,36 +3220,32 @@
      db = null;%0A
-    
             opti
@@ -3265,28 +3265,24 @@
             
- 
-   
 options = nu
@@ -3281,38 +3281,24 @@
 ons = null;%0A
-            %7D%0A
         %7D%0A  

Fix NPE in RocksDBHashtable.close()