package utils;

import org.h2.jdbcx.JdbcDataSource;

import java.sql.*;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import proto.types.block.Block;
import static java.lang.String.format;

public class DBUtils {
    private final static org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger(DBUtils.class);
    private static JdbcDataSource eds = null;
    private static String txTableName = "BLOCKSMAP";
    static private ExecutorService worker = Executors.newSingleThreadExecutor();
    private static HashMap<Integer, Connection> rc = new HashMap<>();
    private static HashMap<Integer, Connection> wc = new HashMap<>();
    private static final Object newWriteNotifier = new Object();

    public DBUtils(int workers) {
        try {
            Class.forName("org.h2.Driver");
        } catch (ClassNotFoundException e) {
            logger.error("", e);
            return;
        }
        eds = new JdbcDataSource();
        eds.setURL("jdbc:h2:./TDB");
        eds.setUser("sa");
        eds.setPassword("sa");
        for (int i = 0; i < workers ; i++) {
            createWriteConn(i);
            createReadConn(i);

        }
    }

    static public void shutdown() {

        try {
            for (Connection c : rc.values()) {
                c.close();
            }
            for (Connection c : wc.values()) {
                c.close();
            }
        } catch (SQLException e) {
            logger.error("Unable to shutdown connection");
            return;
        }
        worker.shutdownNow();
//        if (rc.size() == 0 && wc.size() == 0) {

//        }
    }

    static private String getTableName(int channel) {
        return txTableName + "_" + channel;
    }

    static public void initTables(int channel) throws SQLException {
        Connection conn = wc.get(channel);
        Statement stmt = conn.createStatement();
        stmt.execute("DROP INDEX IF EXISTS id");
        stmt.execute("DROP TABLE IF EXISTS " +
                getTableName(channel));
        stmt.execute("CREATE TABLE IF NOT EXISTS " +
                getTableName(channel) +
                " (pid INTEGER not NULL, " +
                "bid INTEGER not NULL, " +
                "height INTEGER not NULL)"
                );
        stmt.execute("CREATE INDEX IF NOT EXISTS id ON " + getTableName(channel) + " (pid, bid)");
        stmt.close();
//        conn.close();
        logger.info(format("successfully created a table for [channel=%d]", channel));
    }

    static public Connection getConnection() throws SQLException {
        return eds.getConnection();

    }

    static private void createWriteConn(int channel) {
        if (!wc.containsKey(channel)) {
            try {
                wc.put(channel, eds.getConnection());
            } catch (SQLException e) {
                logger.error(format("unable to create write connection [channel=%d]", channel));
            }
        }
    }

    static private void createReadConn(int channel) {
        if (!rc.containsKey(channel)) {
            try {
                rc.put(channel, eds.getConnection());
            } catch (SQLException e) {
                logger.error(format("unable to create read connection [channel=%d]", channel));
            }
        }
    }

    static private void writeBlockToTable(int channel, int pid, int bid, int height) {
        synchronized (newWriteNotifier) {
            try {
                Statement stmt = wc.get(channel).createStatement();
                stmt.executeUpdate(format("INSERT INTO %s VALUES(%d, %d, %d)", getTableName(channel),
                        pid, bid, height));
                stmt.close();
            } catch (SQLException e) {
                logger.error(format("unable to create statement for tx [channel=%d]", channel), e);
            }
            newWriteNotifier.notifyAll();
        }

    }

    static public void writeBlockToTable(Block b) {
        int pid = b.getHeader().getBid().getPid();
        int bid = b.getId().getBid();
        int height = b.getHeader().getHeight();
        int channel = b.getHeader().getM().getChannel();
        worker.execute(() -> {
            writeBlockToTable(channel, pid, bid, height);
            logger.debug(format("have write to DB [c=%d, h=%d, pid=%d, bid=%d", channel, height, pid, bid));
        });
    }

    static public int getBlockRecord(int worker, int pid, int bid, boolean blocking) throws InterruptedException {
        if (!blocking) return getBlockRecord(worker, pid, bid);
        int h = getBlockRecord(worker, pid, bid);
        synchronized (newWriteNotifier) {
            while (h == -1) {
                newWriteNotifier.wait();
                h = getBlockRecord(worker, pid, bid);
            }
        }
        return h;


    }

    static private int getBlockRecord(int worker, int pid, int bid) {
        try {
            Statement stmt = rc.get(worker).createStatement();
            ResultSet rs = stmt.executeQuery(format("SELECT height FROM %s WHERE pid=%d AND bid=%d",
                    getTableName(worker), pid, bid));
            if (!rs.next()) return -1;
            return rs.getInt("height");
        } catch (SQLException e) {
            logger.error(format("unable to create statement for read [channel=%d]", worker), e);
            return -1;
        }
    }
}
