package org.bdware.sc.db;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bdware.sc.index.TimeSerialIndex;
import org.bdware.sc.util.JsonUtil;
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.rocksdb.RocksIterator;

import java.io.File;
import java.util.*;

public class MultiIndexTimeRocksDBUtil implements MultiIndexTimeDBUtilIntf {
    private static final Logger LOGGER = LogManager.getLogger(MultiIndexTimeRocksDBUtil.class);
    private final Map<String, TimeSerialIndex> secondaryIndex;
    static String primaryKey = "_DB_primary_";
    public String dbPath;
    public String tableName;
    Random random = new Random();
    private RocksDB db;
    private TimeSerialIndex primaryIndex;

    public MultiIndexTimeRocksDBUtil(String path, String tableName) {
        secondaryIndex = new HashMap<>();
        dbPath = path;
        this.tableName = tableName;
        setupDB();
    }

    private static void deleteJelck(File file) {
        if (file.exists()) {
            LOGGER.trace("delete file" + file.getAbsolutePath() + ": " + file.delete());
        }
    }

    private void setupDB() {
        File file = new File(dbPath + "/" + tableName);
        Options options = new Options();
        options.setCreateIfMissing(true);
        File lockFile = new File(file, "LOCK");
        File timeIndex = new File(dbPath + "/" + tableName + "/" + primaryIndex + ".timeindex");
        LOGGER.trace("create directory " + file.getAbsolutePath() + ": " + file.mkdirs());
        LOGGER.trace("delete file" + lockFile.getAbsolutePath() + ": " + lockFile.delete());
        try {
            LOGGER.debug("OpenDB:" + file.getAbsolutePath());
            db = RocksDB.open(options, file.getAbsolutePath());
        } catch (RocksDBException e) {
            e.printStackTrace();
        }
        primaryIndex = new TimeSerialIndex(timeIndex.getAbsolutePath());
        secondaryIndex.put(primaryKey, primaryIndex);
        for (File f : timeIndex.getParentFile().listFiles()) {
            if (!f.getName().endsWith(".timeindex")) continue;
            if (f.getName().equals(timeIndex.getName())) continue;
            TimeSerialIndex index =
                    new TimeSerialIndex(f.getAbsolutePath());
            secondaryIndex.putIfAbsent(f.getName().substring(0,
                    f.getName().length() - ".timeindex".length()), index);
        }
    }

    public synchronized void put(String label, String val) {
        try {
            TimeSerialIndex index = getIndex(label);
            long key = random.nextLong();
            index.index(key);
            if (index != primaryIndex) primaryIndex.index(key);

            db.put(longToByte(key), val.getBytes());
        } catch (RocksDBException e) {
            e.printStackTrace();
        }
    }

    private long byteToLong(byte[] bytes) {
        long ret = 0;
        for (byte aByte : bytes) {
            ret <<= 8;
            ret |= aByte;
        }
        return ret;
    }

    private byte[] longToByte(long l) {
        byte[] ret = new byte[8];
        for (int i = 56; i >= 0; i -= 8) {
            byte val = (byte) (l & 0xff);
            ret[i / 8] = val;
            l >>= 8;
        }
        return ret;
    }

    public synchronized long queryOffset(String label, long begin) {
        TimeSerialIndex index = getIndex(label);
        return index.findNearest(begin);
    }

    public synchronized List<String> queryByDateAsString(String label, long begin, long end) {
        List<String> ret = new ArrayList<>();
        TimeSerialIndex index = getIndex(label);
        long offset = index.findNearest(begin);
        long size = index.findNearest(end);
        int len = (int) (size - offset + 1);
        List<Long> data = index.request(offset, len);
        for (Long l : data) {
            try {
                String t = new String(db.get(longToByte(l)));
                if (!t.isEmpty()) {
                    ret.add(t);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return ret;
    }

    public synchronized List<JsonObject> queryByDateAsJson(String label, long begin, long end) {
        List<JsonObject> ret = new ArrayList<>();
        TimeSerialIndex index = getIndex(label);
        long offset = index.findNearest(begin);
        long size = index.findNearest(end);
        int len = (int) (size - offset + 1);
        List<Long> data = index.request(offset, len);
        for (Long l : data) {
            String t = null;
            try {
                t = new String(db.get(longToByte(l)));
                if (!t.isEmpty()) {
                    JsonObject jo = JsonUtil.parseStringAsJsonObject(t);
                    jo.addProperty("key", l.toString());
                    ret.add(jo);
                }
            } catch (Exception e) {
                LOGGER.error("parse db json error:" + t);
            }
        }
        return ret;
    }

    public long size() {
        return primaryIndex.size();
    }

    public long size(String label) {
        try {
            if (label == null || label.length() == 0) {
                return primaryIndex.size();
            }
            if (secondaryIndex.containsKey(label)) {
                TimeSerialIndex index = getIndex(label);
                return index.size();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0L;
    }

    public List<JsonObject> queryByOffset(String label, long offset, int count) {
        List<JsonObject> ret = new ArrayList<>();
        TimeSerialIndex index = getIndex(label);
        List<TimeSerialIndex.IndexEntry> data = index.requestIndexEntry(offset, count);
        for (TimeSerialIndex.IndexEntry entry : data) {
            try {
                String t = new String(db.get(longToByte(entry.value)));
                JsonObject jo;
                if (!t.isEmpty()) {
                    try {
                        jo = JsonUtil.parseStringAsJsonObject(t);
                    } catch (Exception e) {
                        jo = new JsonObject();
                        jo.addProperty("data", t);
                    }
                } else {
                    jo = new JsonObject();
                }
                jo.addProperty("key", entry.value);
                jo.addProperty("timestamp", entry.key);
                ret.add(jo);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return ret;
    }

    public String get(String key) {
        try {
            return new String(db.get(longToByte(Long.parseLong(key))));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private TimeSerialIndex getIndex(String label) {
        if (null == label || label.length() == 0) {
            return primaryIndex;
        }
        if (secondaryIndex.containsKey(label)) {
            return secondaryIndex.get(label);
        } else {
            TimeSerialIndex index =
                    new TimeSerialIndex(dbPath + "/" + tableName + "/" + label + ".timeindex");
            secondaryIndex.putIfAbsent(label, index);
            return index;
        }
    }

    // TODO remove this method, using index by type instead
    public Map<String, Integer> querySort(String label, long date) {
        List<String> ret = new ArrayList<>();
        Map<String, Integer> map = new HashMap<>();
        Map<String, String> data2;
        TimeSerialIndex index = getIndex(label);
        long offset = index.findNearest(date);
        int len = (int) (index.size() - offset);
        List<Long> data = index.request(offset, len);
        String action;
        for (Long l : data) {
            String t = null;
            try {
                t = new String(db.get(longToByte(l)));
                data2 = JsonUtil.fromJson(t, new TypeToken<Map<String, String>>() {
                }.getType());
                if (null != data2) {
                    action = data2.get("action");
                    if (map.containsKey(action)) {
                        int count = map.get(action) + 1;
                        map.put(action, count);
                    } else {
                        map.put(action, 1);
                    }
                }
            } catch (Exception e) {
                LOGGER.error("parse db json error:" + t);
            }
        }
        return map;
    }

    public JsonArray countInInterval(String label, long start, long interval, long end) {
        TimeSerialIndex index = getIndex(label);
        long offset = index.findNearest(start);
        JsonArray array = new JsonArray();
        do {
            start += interval;
            long offset2 = index.findNearest(start);
            array.add(offset2 - offset);
            offset = offset2;
        } while (start < end);
        return array;
    }

    public void rebuildIndex(String labelField) {
        try {
            List<Long> times = new ArrayList<>();
            Map<Long, BytesPair> memoryIndex = new HashMap<>();
            RocksIterator iter = db.newIterator();
            iter.seekToFirst();
            for (; iter.isValid(); iter.next()) {
                JsonObject jo = JsonUtil.parseStringAsJsonObject(new String(iter.value()));
                Long time = jo.get("date").getAsLong();
                if (time < 0) {
                    continue;
                }
                times.add(time);
                memoryIndex.put(time, new BytesPair(iter.key(), iter.value()));
            }

            times.sort(Long::compareTo);
            for (Long time : times) {
                BytesPair dataKV = memoryIndex.get(time);
                JsonObject jo = JsonUtil.fromJson(new String(dataKV.value), JsonObject.class);
                TimeSerialIndex tsi = getIndex(jo.get(labelField).getAsString());
                long key = byteToLong(dataKV.key);
                tsi.manullyIndex(time, key);
                primaryIndex.manullyIndex(time, key);
            }
            // System.out.println("leak size = " + (dataKVs.size() - memoryIndex.size()));
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 每一条data都是json串
        // 转为JsonObject_obj后，获取obj.date，obj.labelField, key，
        // 按date排序后，
        // 把每个TimeIndex都primaryIndex.manuellyIndex(obj.data,key);
        // manuellyIndex可参考LenVarTimeIndex写法
    }

    public void close() {
        db.close();
    }

    public List<String> getIndexStartWith(String prefix) {
        List<String> ret = new ArrayList<>();
        if (prefix == null) {
            prefix = "";
        }
        for (String key : secondaryIndex.keySet()) {
            if (key.startsWith(prefix))
                ret.add(key);
        }
        ret.remove(primaryKey);
        return ret;
    }

    public List<String> getAllIndexKey() {
        Set<String> data = secondaryIndex.keySet();
        List<String> ret = new ArrayList<>();
        ret.addAll(data);
        ret.remove(primaryKey);
        return ret;
    }

    // TODO
    static class BytesPair {
        byte[] key, value;

        BytesPair(byte[] k, byte[] v) {
            key = k;
            value = v;
        }
    }
}
