package org.iot.mqtt.store.rocksdb.db;

import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.iot.mqtt.common.config.MqttConfig;
import org.iot.mqtt.test.utils.MixAll;
import org.rocksdb.BlockBasedTableConfig;
import org.rocksdb.BloomFilter;
import org.rocksdb.ColumnFamilyDescriptor;
import org.rocksdb.ColumnFamilyHandle;
import org.rocksdb.ColumnFamilyOptions;
import org.rocksdb.CompressionType;
import org.rocksdb.DBOptions;
import org.rocksdb.LRUCache;
import org.rocksdb.ReadOptions;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.rocksdb.RocksIterator;
import org.rocksdb.WriteBatch;
import org.rocksdb.WriteOptions;
import org.rocksdb.util.SizeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RDB {

    private static final Logger log = LoggerFactory.getLogger(RDB.class);

    private final DBOptions DB_OPTIONS = new DBOptions();
    private final ReadOptions READ_OPTIONS = new ReadOptions();
    private final WriteOptions WRITE_OPTIONS_SYNC = new WriteOptions();
    private final WriteOptions WRITE_OPTIONS_ASYNC = new WriteOptions();
    private final BloomFilter BLOOM_FILTER = new BloomFilter();
    private final BlockBasedTableConfig BLOCK_BASED_TABLE_CONFIG = new BlockBasedTableConfig();
    private final ColumnFamilyOptions COLUMN_FAMILY_OPTIONS = new ColumnFamilyOptions();
    private final List<CompressionType> COMPRESSION_TYPES = new ArrayList<>();
    private MqttConfig mqttConfig;
    private RocksDB DB;
    private Map<String,ColumnFamilyHandle> CF_HANDLES = new HashMap<>();

    public RDB(MqttConfig mqttConfig){
        this.mqttConfig = mqttConfig;
    }

    public RocksIterator newIterator(ColumnFamilyHandle cfh){
        return this.DB.newIterator(cfh,READ_OPTIONS);
    }

    public List<byte[]> getByPrefix(final ColumnFamilyHandle cfh,final byte[] prefixKey){
        List<byte[]> values = new ArrayList<>();
        try{
            RocksIterator iterator = this.newIterator(cfh);
            for(iterator.seek(prefixKey);iterator.isValid();iterator.next()){
            	if(new String(iterator.key()).startsWith(new String(prefixKey))) {
            		values.add(iterator.value());
            	}
            }
            log.debug("[RocksDB] -> succ while get by prefix,columnFamilyHandle:{}, prefixKey:{}",cfh.toString(),new String(prefixKey));
        }catch(Exception e){
            log.error("[RocksDB] ->  error while get by prefix, columnFamilyHandle:{}, prefixKey:{}, err:{}",
                    cfh.toString(), new String(prefixKey), e);
        }
        return values;
    }
    
    public List<byte[]> pollByPrefix(final ColumnFamilyHandle cfh,final byte[] prefixKey,int nums){
        List<byte[]> values = new ArrayList<>();
        int count = 0;
        try{
            RocksIterator iterator = this.newIterator(cfh);
            WriteBatch writeBatch = new WriteBatch();
            for(iterator.seek(prefixKey);iterator.isValid();iterator.next()){
            	if(new String(iterator.key()).startsWith(new String(prefixKey))) {
            		values.add(iterator.value());
            		writeBatch.delete(cfh,iterator.key());
            		count++;
            	}
            	if(count>=nums) {
            		break;
            	}
            }
            if(count > 0){
                this.DB.write(WRITE_OPTIONS_SYNC,writeBatch);
            }
            log.debug("[RocksDB] -> succ while get by prefix,columnFamilyHandle:{}, pollByPrefix:{}",cfh.toString(),new String(prefixKey));
        }catch(Exception e){
            log.error("[RocksDB] ->  error while get by prefix, columnFamilyHandle:{}, pollByPrefix:{}, err:{}",
                    cfh.toString(), new String(prefixKey), e);
        }
        return values;
    }
    
    public int getCountByPrefix(final ColumnFamilyHandle cfh,final byte[] prefixKey){
        int count = 0;
        try{
            RocksIterator iterator = this.newIterator(cfh);
            for(iterator.seek(prefixKey);iterator.isValid();iterator.next()){
            	if(new String(iterator.key()).startsWith(new String(prefixKey))) {
            		count++;
            	}
            }
            log.debug("[RocksDB] -> succ while get count by prefix,columnFamilyHandle:{}, prefixKey:{}",cfh.toString(),new String(prefixKey));
        }catch(Exception e){
            log.error("[RocksDB] ->  error while get count by prefix, columnFamilyHandle:{}, prefixKey:{}, err:{}",
                    cfh.toString(), new String(prefixKey), e);
        }
        return count;
    }

    public boolean deleteByPrefix(final ColumnFamilyHandle cfh,final byte[] prefixKey){
        return deleteByPrefix(cfh,prefixKey,false);
    }
    
    public boolean deleteByPrefix(final ColumnFamilyHandle cfh,final byte[] prefixKey,boolean sync){
        try{
            RocksIterator iterator = this.newIterator(cfh);
            int item = 0;
            WriteBatch writeBatch = new WriteBatch();
            for(iterator.seek(prefixKey);iterator.isValid();iterator.next()){
            	if(new String(iterator.key()).startsWith(new String(prefixKey))) {
            		writeBatch.delete(cfh,iterator.key());
                    item++;
            	}
            }
            if(item > 0){
                this.DB.write(sync?WRITE_OPTIONS_SYNC:WRITE_OPTIONS_ASYNC,writeBatch);
            }
            log.debug("[RocksDB] -> succ while delete by prefix,columnFamilyHandle:{}, prefixKey:{}, nums:{}",cfh.toString(),new String(prefixKey),item);
        }catch(RocksDBException e){
            log.error("[RocksDB] ->  error while delete by prefix, columnFamilyHandle:{}, prefixKey:{}, err:{}",
                    cfh.toString(), new String(prefixKey), e);
            return false;
        }
        return true;
    }

    public boolean deleteRange(final ColumnFamilyHandle cfh,final byte[] beginKey,final byte[] endKey){
        try {
            DB.deleteRange(cfh, beginKey, endKey);
            log.debug("[RocksDB] -> succ delete range, columnFamilyHandle:{}, beginKey:{}, endKey:{}",
                    cfh.toString(), new String(beginKey), new String(endKey));
        } catch (RocksDBException e) {
            log.error("[RocksDB] ->  error while delete range, columnFamilyHandle:{}, beginKey:{}, endKey:{}, err:{}",
                    cfh.toString(), new String(beginKey), new String(endKey), e.getMessage(), e);
            return false;
        }
        return true;
    }

    public byte[] get(final ColumnFamilyHandle cfh,final byte[] key){
        try {
            return DB.get(cfh, key);
        } catch (RocksDBException e) {
            log.error("[RocksDB] -> error while get, columnFamilyHandle:{}, key:{}, err:{}",
                    cfh.toString(), new String(key), e.getMessage(), e);
            return null;
        }
    }

    public boolean writeAsync(final WriteBatch writeBatch){
        return this.write(WRITE_OPTIONS_ASYNC,writeBatch);
    }

    public boolean writeSync(final WriteBatch writeBatch){
        return this.write(WRITE_OPTIONS_SYNC,writeBatch);
    }

    public boolean write(final WriteOptions writeOptions,final WriteBatch writeBatch){
        try {
            this.DB.write(writeOptions,writeBatch);
            log.debug("[RocksDB] -> success write writeBatch, size:{}", writeBatch.count());
        } catch (RocksDBException e) {
            log.error("[RocksDB] -> error while write batch, err:{}", e.getMessage(), e);
            return false;
        }
        return true;
    }

    public boolean putAsync(final ColumnFamilyHandle cfh,final byte[] key,final byte[] value){
        return this.put(cfh,WRITE_OPTIONS_ASYNC,key,value);
    }
    
    public boolean putSync(final ColumnFamilyHandle cfh,final byte[] key,final byte[] value){
        return this.put(cfh,WRITE_OPTIONS_SYNC,key,value);
    }

    public boolean delete(final ColumnFamilyHandle cfh, final byte[] key) {
        try {
            this.DB.delete(cfh, key);
            log.debug("[RocksDB] -> succ delete key, columnFamilyHandle:{}, key:{}", cfh.toString(), new String(key));
        } catch (RocksDBException e) {
            log.error("[RocksDB] -> while delete key, columnFamilyHandle:{}, key:{}, err:{}",
                    cfh.toString(), new String(key), e.getMessage(), e);
        }
        return true;
    }

    public boolean put(final ColumnFamilyHandle cfh,final WriteOptions writeOptions,final byte[] key,final byte[] value){
        try {
            this.DB.put(cfh, writeOptions, key, value);
            log.debug("[RocksDB] -> success put value");
        } catch (RocksDBException e) {
            log.error("[RocksDB] -> error while put, columnFamilyHandle:{}, key:{}, err:{}",
                    cfh.isOwningHandle(), new String(key), e.getMessage(), e);
            return false;
    }
        return true;
    }

    public void init(){
        this.DB_OPTIONS.setCreateIfMissing(true)
                .setCreateMissingColumnFamilies(true)
                .setMaxBackgroundFlushes(this.mqttConfig.getMaxBackgroundFlushes())
                .setMaxBackgroundCompactions(this.mqttConfig.getMaxBackgroundCompactions())
                .setMaxOpenFiles(this.mqttConfig.getMaxOpenFiles())
                .setRowCache(new LRUCache(1024* SizeUnit.MB,16,true,5))
                .setMaxSubcompactions(this.mqttConfig.getMaxSubcompactions());
        this.DB_OPTIONS.setBaseBackgroundCompactions(this.mqttConfig.getBaseBackGroundCompactions());
        READ_OPTIONS.setPrefixSameAsStart(true);
        WRITE_OPTIONS_SYNC.setSync(true);
        WRITE_OPTIONS_ASYNC.setSync(false);
        BLOCK_BASED_TABLE_CONFIG.setFilter(BLOOM_FILTER)
                .setCacheIndexAndFilterBlocks(true)
                .setPinL0FilterAndIndexBlocksInCache(true);

        COMPRESSION_TYPES.addAll(Arrays.asList(
                CompressionType.NO_COMPRESSION,CompressionType.NO_COMPRESSION,
                CompressionType.LZ4_COMPRESSION,CompressionType.LZ4_COMPRESSION,
                CompressionType.LZ4_COMPRESSION,CompressionType.ZSTD_COMPRESSION,
                CompressionType.ZSTD_COMPRESSION
        ));

        COLUMN_FAMILY_OPTIONS.setTableFormatConfig(BLOCK_BASED_TABLE_CONFIG)
                .useFixedLengthPrefixExtractor(this.mqttConfig.getUseFixedLengthPrefixExtractor())
                .setWriteBufferSize(this.mqttConfig.getWriteBufferSize() * SizeUnit.MB)
                .setMaxWriteBufferNumber(this.mqttConfig.getMaxWriteBufferNumber())
                .setLevel0SlowdownWritesTrigger(this.mqttConfig.getLevel0SlowdownWritesTrigger())
                .setLevel0StopWritesTrigger(10)
                .setCompressionPerLevel(COMPRESSION_TYPES)
                .setTargetFileSizeBase(this.mqttConfig.getTargetFileSizeBase() * SizeUnit.MB)
                .setMaxBytesForLevelBase(this.mqttConfig.getMaxBytesForLevelBase() * SizeUnit.MB)
                .setOptimizeFiltersForHits(true);

        long start = System.currentTimeMillis();
        boolean result = MixAll.createIfNotExistsDir(new File(mqttConfig.getRocksDbPath()));
        assert result;
        try {
            List<ColumnFamilyDescriptor> cfDescriptors = getCFDescriptors();
            List<ColumnFamilyHandle> cfHandles = new ArrayList<>();
            DB = RocksDB.open(DB_OPTIONS,mqttConfig.getRocksDbPath(),cfDescriptors,cfHandles);
            cacheCFHandles(cfHandles);
            long end = System.currentTimeMillis();
            log.info("[RocksDB] -> start RocksDB success,consumeTime:{}",(end-start));
        } catch (RocksDBException e) {
            log.error("[RocksDB] -> init RocksDB error,ex:{}",e);
            System.exit(-1);
        }

    }

    private List<ColumnFamilyDescriptor> getCFDescriptors(){
        List<ColumnFamilyDescriptor> list = new ArrayList<>();
        list.add(new ColumnFamilyDescriptor("default".getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.SESSION.getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.SUBSCRIPTION.getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.WILL_MESSAGE.getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.OFFLINE_MESSAGE.getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.REC_FLOW_MESSAGE.getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.SEND_FLOW_MESSAGE.getBytes(Charset.forName("UTF-8"))));
        list.add(new ColumnFamilyDescriptor(RDBStorePrefix.RETAIN_MESSAGE.getBytes(Charset.forName("UTF-8"))));
        return list;
    }

    private void cacheCFHandles(List<ColumnFamilyHandle> cfHandles) throws RocksDBException {
        if(cfHandles == null || cfHandles.size() == 0){
            log.error("[RocksDB] -> init columnFamilyHandle failure.");
            throw new RocksDBException("init columnFamilyHandle failure");
        }
        for (ColumnFamilyHandle cfHandle : cfHandles) {
            this.CF_HANDLES.put(new String(cfHandle.getName()),cfHandle);
        }
    }

    public void close(){
        this.DB_OPTIONS.close();
        this.WRITE_OPTIONS_SYNC.close();
        this.WRITE_OPTIONS_ASYNC.close();
        this.READ_OPTIONS.close();
        this.COLUMN_FAMILY_OPTIONS.close();
        CF_HANDLES.forEach((x,y) -> {
            y.close();
        });
        if(DB != null){
            DB.close();
        }
    }

    public ColumnFamilyHandle getColumnFamilyHandle(String cfhName){
        return this.CF_HANDLES.get(cfhName);
    }
}